Compare commits

...

80 commits

Author SHA1 Message Date
Patrick Mulligan
d7bb1d9bc5 feat(nip05): add Lightning Address support for zaps
Some checks failed
Docker Compose Actions Workflow / test (push) Has been cancelled
Adds /.well-known/lnurlp/:username endpoint that:
1. Looks up username in NIP-05 database
2. Gets LNURL-pay info from Lightning.Pub for that user
3. Returns standard LUD-16 response for wallet compatibility

This makes NIP-05 addresses (alice@domain) work seamlessly as
Lightning Addresses for receiving payments and NIP-57 zaps.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-04 15:21:53 -05:00
Patrick Mulligan
794eb06964 feat(extensions): add NIP-05 identity extension
Implements Nostr NIP-05 for human-readable identity verification:
- Username claiming and management (username@domain)
- /.well-known/nostr.json endpoint per spec
- Optional relay hints in JSON response
- Admin controls for identity management

RPC methods:
- nip05.claim - Claim a username
- nip05.release - Release your username
- nip05.updateRelays - Update relay hints
- nip05.getMyIdentity - Get your identity
- nip05.lookup - Look up by username
- nip05.lookupByPubkey - Look up by pubkey
- nip05.listIdentities - List all (admin)
- nip05.deactivate/reactivate - Admin controls

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-04 15:21:53 -05:00
Patrick Mulligan
2586059391 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:21:53 -05:00
Patrick Mulligan
370acb1f40 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:21:53 -05:00
Patrick Mulligan
70a21602e7 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:21:52 -05:00
Patrick Mulligan
b1bc60538c 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:21:52 -05:00
Patrick Mulligan
be4c3bcf2a 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:21:52 -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
68 changed files with 7368 additions and 1078 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 { DataSource } from "typeorm"
import { User } from "./build/src/services/storage/entity/User.js" import { User } from "./build/src/services/storage/entity/User.js"
import { UserReceivingInvoice } from "./build/src/services/storage/entity/UserReceivingInvoice.js" import { UserReceivingInvoice } from "./build/src/services/storage/entity/UserReceivingInvoice.js"
@ -22,11 +33,13 @@ import { AppUserDevice } from "./build/src/services/storage/entity/AppUserDevice
import { UserAccess } from "./build/src/services/storage/entity/UserAccess.js" import { UserAccess } from "./build/src/services/storage/entity/UserAccess.js"
import { AdminSettings } from "./build/src/services/storage/entity/AdminSettings.js" import { AdminSettings } from "./build/src/services/storage/entity/AdminSettings.js"
import { TransactionSwap } from "./build/src/services/storage/entity/TransactionSwap.js" import { 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 { Initial1703170309875 } from './build/src/services/storage/migrations/1703170309875-initial.js'
import { LspOrder1718387847693 } from './build/src/services/storage/migrations/1718387847693-lsp_order.js' import { LspOrder1718387847693 } from './build/src/services/storage/migrations/1718387847693-lsp_order.js'
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 { 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 { 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 { PaymentIndex1721760297610 } from './build/src/services/storage/migrations/1721760297610-payment_index.js'
import { DebitAccess1726496225078 } from './build/src/services/storage/migrations/1726496225078-debit_access.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 { 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 { UserOffer1733502626042 } from './build/src/services/storage/migrations/1733502626042-user_offer.js'
import { ManagementGrant1751307732346 } from './build/src/services/storage/migrations/1751307732346-management_grant.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 { 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 { 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' 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 { 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 { 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 { 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({ export default new DataSource({
type: "better-sqlite3", type: "better-sqlite3",
database: "db.sqlite", database: "db.sqlite",
// logging: true, // logging: true,
migrations: [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189, CreateInviteTokenTable1721751414878, migrations: [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189,
PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264, DebitToPub1727105758354, UserCbUrl1727112281043, TrackedProvider1720814323679, CreateInviteTokenTable1721751414878, PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264,
UserOffer1733502626042, ManagementGrant1751307732346, InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611, DebitToPub1727105758354, UserCbUrl1727112281043, UserOffer1733502626042, ManagementGrant1751307732346, ManagementGrantBanned1751989251513,
AppUserDevice1753285173175, UserAccess1759426050669, AddBlindToUserOffer1760000000000, ApplicationAvatarUrl1761000001000, AdminSettings1761683639419, TxSwap1762890527098, InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611, AppUserDevice1753285173175,
TxSwapAddress1764779178945, ClinkRequester1765497600000, TrackedProviderHeight1766504040000], UserAccess1759426050669, AddBlindToUserOffer1760000000000, ApplicationAvatarUrl1761000001000, AdminSettings1761683639419, TxSwap1762890527098,
TxSwapAddress1764779178945, ClinkRequester1765497600000, TrackedProviderHeight1766504040000, SwapsServiceUrl1768413055036,
InvoiceSwaps1769529793283, InvoiceSwapsFixes1769805357459, ApplicationUserTopicId1770038768784, SwapTimestamps1771347307798
],
entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment, entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment,
UserBasicAuth, UserEphemeralKey, Product, UserToUserPayment, Application, ApplicationUser, UserToUserPayment, LspOrder, LndNodeInfo, UserBasicAuth, UserEphemeralKey, Product, UserToUserPayment, Application, ApplicationUser, UserToUserPayment, LspOrder, LndNodeInfo,
TrackedProvider, InviteToken, DebitAccess, UserOffer, ManagementGrant, AppUserDevice, UserAccess, AdminSettings, TransactionSwap], TrackedProvider, InviteToken, DebitAccess, UserOffer, ManagementGrant, AppUserDevice, UserAccess, AdminSettings, TransactionSwap, InvoiceSwap],
// synchronize: true, // 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 { DataSource } from "typeorm"
import { BalanceEvent } from "./build/src/services/storage/entity/BalanceEvent.js" import { BalanceEvent } from "./build/src/services/storage/entity/BalanceEvent.js"
import { ChannelBalanceEvent } from "./build/src/services/storage/entity/ChannelsBalanceEvent.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 { ChannelRouting1709316653538 } from './build/src/services/storage/migrations/1709316653538-channel_routing.js'
import { HtlcCount1724266887195 } from './build/src/services/storage/migrations/1724266887195-htlc_count.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 { 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({ export default new DataSource({
type: "better-sqlite3", type: "better-sqlite3",
database: "metrics.sqlite", database: "metrics.sqlite",
entities: [BalanceEvent, ChannelBalanceEvent, ChannelRouting, RootOperation, ChannelEvent], 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__ __request__ body
- This methods has an __empty__ __response__ body - This methods has an __empty__ __response__ body
- BumpTx
- auth type: __Admin__
- input: [BumpTx](#BumpTx)
- This methods has an __empty__ __response__ body
- CloseChannel - CloseChannel
- auth type: __Admin__ - auth type: __Admin__
- input: [CloseChannelRequest](#CloseChannelRequest) - 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) - input: [MessagingToken](#MessagingToken)
- This methods has an __empty__ __response__ body - This methods has an __empty__ __response__ body
- GetAdminInvoiceSwapQuotes
- auth type: __Admin__
- input: [InvoiceSwapRequest](#InvoiceSwapRequest)
- output: [InvoiceSwapQuoteList](#InvoiceSwapQuoteList)
- GetAdminTransactionSwapQuotes - GetAdminTransactionSwapQuotes
- auth type: __Admin__ - auth type: __Admin__
- input: [TransactionSwapRequest](#TransactionSwapRequest) - 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) - input: [AppsMetricsRequest](#AppsMetricsRequest)
- output: [AppsMetrics](#AppsMetrics) - output: [AppsMetrics](#AppsMetrics)
- GetAssetsAndLiabilities
- auth type: __Admin__
- input: [AssetsAndLiabilitiesReq](#AssetsAndLiabilitiesReq)
- output: [AssetsAndLiabilities](#AssetsAndLiabilities)
- GetBundleMetrics - GetBundleMetrics
- auth type: __Metrics__ - auth type: __Metrics__
- input: [LatestBundleMetricReq](#LatestBundleMetricReq) - 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) - input: [LinkNPubThroughTokenRequest](#LinkNPubThroughTokenRequest)
- This methods has an __empty__ __response__ body - This methods has an __empty__ __response__ body
- ListAdminSwaps - ListAdminInvoiceSwaps
- auth type: __Admin__ - auth type: __Admin__
- This methods has an __empty__ __request__ body - 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 - ListChannels
- auth type: __Admin__ - auth type: __Admin__
- This methods has an __empty__ __request__ body - This methods has an __empty__ __request__ body
- output: [LndChannels](#LndChannels) - output: [LndChannels](#LndChannels)
- ListSwaps - ListTxSwaps
- auth type: __User__ - auth type: __User__
- This methods has an __empty__ __request__ body - This methods has an __empty__ __request__ body
- output: [SwapsList](#SwapsList) - output: [TxSwapsList](#TxSwapsList)
- LndGetInfo - LndGetInfo
- auth type: __Admin__ - 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) - input: [PayAddressRequest](#PayAddressRequest)
- output: [PayAddressResponse](#PayAddressResponse) - output: [PayAddressResponse](#PayAddressResponse)
- PayAdminInvoiceSwap
- auth type: __Admin__
- input: [PayAdminInvoiceSwapRequest](#PayAdminInvoiceSwapRequest)
- output: [AdminInvoiceSwapResponse](#AdminInvoiceSwapResponse)
- PayAdminTransactionSwap - PayAdminTransactionSwap
- auth type: __Admin__ - auth type: __Admin__
- input: [PayAdminTransactionSwapRequest](#PayAdminTransactionSwapRequest) - input: [PayAdminTransactionSwapRequest](#PayAdminTransactionSwapRequest)
- output: [AdminSwapResponse](#AdminSwapResponse) - output: [AdminTxSwapResponse](#AdminTxSwapResponse)
- PayInvoice - PayInvoice
- auth type: __User__ - 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__ __request__ body
- This methods has an __empty__ __response__ body - This methods has an __empty__ __response__ body
- RefundAdminInvoiceSwap
- auth type: __Admin__
- input: [RefundAdminInvoiceSwapRequest](#RefundAdminInvoiceSwapRequest)
- output: [AdminInvoiceSwapResponse](#AdminInvoiceSwapResponse)
- ResetDebit - ResetDebit
- auth type: __User__ - auth type: __User__
- input: [DebitOperation](#DebitOperation) - 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__ __request__ body
- This methods has an __empty__ __response__ 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 - CloseChannel
- auth type: __Admin__ - auth type: __Admin__
- http method: __post__ - 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) - input: [MessagingToken](#MessagingToken)
- This methods has an __empty__ __response__ body - 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 - GetAdminTransactionSwapQuotes
- auth type: __Admin__ - auth type: __Admin__
- http method: __post__ - 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) - input: [AppsMetricsRequest](#AppsMetricsRequest)
- output: [AppsMetrics](#AppsMetrics) - output: [AppsMetrics](#AppsMetrics)
- GetAssetsAndLiabilities
- auth type: __Admin__
- http method: __post__
- http route: __/api/admin/assets/liabilities__
- input: [AssetsAndLiabilitiesReq](#AssetsAndLiabilitiesReq)
- output: [AssetsAndLiabilities](#AssetsAndLiabilities)
- GetBundleMetrics - GetBundleMetrics
- auth type: __Metrics__ - auth type: __Metrics__
- http method: __post__ - http method: __post__
@ -743,7 +794,7 @@ The nostr server will send back a message response, and inside the body there wi
- GetTransactionSwapQuotes - GetTransactionSwapQuotes
- auth type: __User__ - auth type: __User__
- http method: __post__ - http method: __post__
- http route: __/api/user/swap/quote__ - http route: __/api/user/swap/transaction/quote__
- input: [TransactionSwapRequest](#TransactionSwapRequest) - input: [TransactionSwapRequest](#TransactionSwapRequest)
- output: [TransactionSwapQuoteList](#TransactionSwapQuoteList) - 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) - input: [LinkNPubThroughTokenRequest](#LinkNPubThroughTokenRequest)
- This methods has an __empty__ __response__ body - This methods has an __empty__ __response__ body
- ListAdminSwaps - ListAdminInvoiceSwaps
- auth type: __Admin__ - auth type: __Admin__
- http method: __post__ - http method: __post__
- http route: __/api/admin/swap/list__ - http route: __/api/admin/swap/invoice/list__
- This methods has an __empty__ __request__ body - 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 - ListChannels
- auth type: __Admin__ - 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 - This methods has an __empty__ __request__ body
- output: [LndChannels](#LndChannels) - output: [LndChannels](#LndChannels)
- ListSwaps - ListTxSwaps
- auth type: __User__ - auth type: __User__
- http method: __post__ - http method: __post__
- http route: __/api/user/swap/list__ - http route: __/api/user/swap/transaction/list__
- This methods has an __empty__ __request__ body - This methods has an __empty__ __request__ body
- output: [SwapsList](#SwapsList) - output: [TxSwapsList](#TxSwapsList)
- LndGetInfo - LndGetInfo
- auth type: __Admin__ - 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) - input: [PayAddressRequest](#PayAddressRequest)
- output: [PayAddressResponse](#PayAddressResponse) - output: [PayAddressResponse](#PayAddressResponse)
- PayAdminInvoiceSwap
- auth type: __Admin__
- http method: __post__
- http route: __/api/admin/swap/invoice/pay__
- input: [PayAdminInvoiceSwapRequest](#PayAdminInvoiceSwapRequest)
- output: [AdminInvoiceSwapResponse](#AdminInvoiceSwapResponse)
- PayAdminTransactionSwap - PayAdminTransactionSwap
- auth type: __Admin__ - auth type: __Admin__
- http method: __post__ - http method: __post__
- http route: __/api/admin/swap/transaction/pay__ - http route: __/api/admin/swap/transaction/pay__
- input: [PayAdminTransactionSwapRequest](#PayAdminTransactionSwapRequest) - input: [PayAdminTransactionSwapRequest](#PayAdminTransactionSwapRequest)
- output: [AdminSwapResponse](#AdminSwapResponse) - output: [AdminTxSwapResponse](#AdminTxSwapResponse)
- PayAppUserInvoice - PayAppUserInvoice
- auth type: __App__ - 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__ __request__ body
- This methods has an __empty__ __response__ 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 - RequestNPubLinkingToken
- auth type: __App__ - auth type: __App__
- http method: __post__ - http method: __post__
@ -1098,7 +1170,10 @@ The nostr server will send back a message response, and inside the body there wi
- __name__: _string_ - __name__: _string_
- __price_sats__: _number_ - __price_sats__: _number_
### AdminSwapResponse ### AdminInvoiceSwapResponse
- __tx_id__: _string_
### AdminTxSwapResponse
- __network_fee__: _number_ - __network_fee__: _number_
- __tx_id__: _string_ - __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 - __include_operations__: _boolean_ *this field is optional
- __to_unix__: _number_ *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 ### AuthApp
- __app__: _[Application](#Application)_ - __app__: _[Application](#Application)_
- __auth_token__: _string_ - __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 - __nextRelay__: _string_ *this field is optional
- __type__: _string_ - __type__: _string_
### BumpTx
- __output_index__: _number_
- __sat_per_vbyte__: _number_
- __txid__: _string_
### BundleData ### BundleData
- __available_chunks__: ARRAY of: _number_ - __available_chunks__: ARRAY of: _number_
- __base_64_data__: ARRAY of: _string_ - __base_64_data__: ARRAY of: _string_
@ -1331,6 +1426,36 @@ The nostr server will send back a message response, and inside the body there wi
- __token__: _string_ - __token__: _string_
- __url__: _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 ### LatestBundleMetricReq
- __limit__: _number_ *this field is optional - __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 ### LinkNPubThroughTokenRequest
- __token__: _string_ - __token__: _string_
### LiquidityAssetProvider
- __pubkey__: _string_
- __tracked__: _[TrackedLiquidityProvider](#TrackedLiquidityProvider)_ *this field is optional
### LiveDebitRequest ### LiveDebitRequest
- __debit__: _[LiveDebitRequest_debit](#LiveDebitRequest_debit)_ - __debit__: _[LiveDebitRequest_debit](#LiveDebitRequest_debit)_
- __npub__: _string_ - __npub__: _string_
@ -1353,6 +1482,10 @@ The nostr server will send back a message response, and inside the body there wi
- __latest_balance__: _number_ - __latest_balance__: _number_
- __operation__: _[UserOperation](#UserOperation)_ - __operation__: _[UserOperation](#UserOperation)_
### LndAssetProvider
- __pubkey__: _string_
- __tracked__: _[TrackedLndProvider](#TrackedLndProvider)_ *this field is optional
### LndChannels ### LndChannels
- __open_channels__: ARRAY of: _[OpenChannel](#OpenChannel)_ - __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_ - __service_fee__: _number_
- __txId__: _string_ - __txId__: _string_
### PayAdminInvoiceSwapRequest
- __no_claim__: _boolean_ *this field is optional
- __sat_per_v_byte__: _number_
- __swap_operation_id__: _string_
### PayAdminTransactionSwapRequest ### PayAdminTransactionSwapRequest
- __address__: _string_ - __address__: _string_
- __swap_operation_id__: _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 ### ProvidersDisruption
- __disruptions__: ARRAY of: _[ProviderDisruption](#ProviderDisruption)_ - __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 ### RelaysMigration
- __relays__: ARRAY of: _string_ - __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_ - __page__: _number_
- __request_id__: _number_ *this field is optional - __request_id__: _number_ *this field is optional
### SwapOperation ### TrackedLiquidityProvider
- __address_paid__: _string_ - __balance__: _number_
- __failure_reason__: _string_ *this field is optional - __invoices__: ARRAY of: _[AssetOperation](#AssetOperation)_
- __operation_payment__: _[UserOperation](#UserOperation)_ *this field is optional - __payments__: ARRAY of: _[AssetOperation](#AssetOperation)_
- __swap_operation_id__: _string_
### SwapsList ### TrackedLndProvider
- __quotes__: ARRAY of: _[TransactionSwapQuote](#TransactionSwapQuote)_ - __channels_balance__: _number_
- __swaps__: ARRAY of: _[SwapOperation](#SwapOperation)_ - __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 ### TransactionSwapQuote
- __chain_fee_sats__: _number_ - __chain_fee_sats__: _number_
- __completed_at_unix__: _number_
- __expires_at_block_height__: _number_
- __invoice_amount_sats__: _number_ - __invoice_amount_sats__: _number_
- __paid_at_unix__: _number_
- __service_fee_sats__: _number_ - __service_fee_sats__: _number_
- __service_url__: _string_ - __service_url__: _string_
- __swap_fee_sats__: _number_ - __swap_fee_sats__: _number_
@ -1665,6 +1827,16 @@ The nostr server will send back a message response, and inside the body there wi
### TransactionSwapRequest ### TransactionSwapRequest
- __transaction_amount_sats__: _number_ - __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 ### UpdateChannelPolicyRequest
- __policy__: _[ChannelPolicy](#ChannelPolicy)_ - __policy__: _[ChannelPolicy](#ChannelPolicy)_
- __update__: _[UpdateChannelPolicyRequest_update](#UpdateChannelPolicyRequest_update)_ - __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_ - __nmanage__: _string_
- __noffer__: _string_ - __noffer__: _string_
- __service_fee_bps__: _number_ - __service_fee_bps__: _number_
- __topic_id__: _string_
- __userId__: _string_ - __userId__: _string_
- __user_identifier__: _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__ - __BUNDLE_METRIC__
- __USAGE_METRIC__ - __USAGE_METRIC__
### TrackedOperationType
- __ROOT__
- __USER__
### UserOperationType ### UserOperationType
- __INCOMING_INVOICE__ - __INCOMING_INVOICE__
- __INCOMING_TX__ - __INCOMING_TX__

View file

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

View file

@ -79,6 +79,13 @@ const (
USAGE_METRIC SingleMetricType = "USAGE_METRIC" USAGE_METRIC SingleMetricType = "USAGE_METRIC"
) )
type TrackedOperationType string
const (
ROOT TrackedOperationType = "ROOT"
USER TrackedOperationType = "USER"
)
type UserOperationType string type UserOperationType string
const ( const (
@ -123,7 +130,10 @@ type AddProductRequest struct {
Name string `json:"name"` Name string `json:"name"`
Price_sats int64 `json:"price_sats"` Price_sats int64 `json:"price_sats"`
} }
type AdminSwapResponse struct { type AdminInvoiceSwapResponse struct {
Tx_id string `json:"tx_id"`
}
type AdminTxSwapResponse struct {
Network_fee int64 `json:"network_fee"` Network_fee int64 `json:"network_fee"`
Tx_id string `json:"tx_id"` Tx_id string `json:"tx_id"`
} }
@ -160,6 +170,21 @@ type AppsMetricsRequest struct {
Include_operations bool `json:"include_operations"` Include_operations bool `json:"include_operations"`
To_unix int64 `json:"to_unix"` 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 { type AuthApp struct {
App *Application `json:"app"` App *Application `json:"app"`
Auth_token string `json:"auth_token"` Auth_token string `json:"auth_token"`
@ -188,6 +213,11 @@ type BeaconData struct {
Nextrelay string `json:"nextRelay"` Nextrelay string `json:"nextRelay"`
Type string `json:"type"` 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 { type BundleData struct {
Available_chunks []int64 `json:"available_chunks"` Available_chunks []int64 `json:"available_chunks"`
Base_64_data []string `json:"base_64_data"` Base_64_data []string `json:"base_64_data"`
@ -356,6 +386,36 @@ type HttpCreds struct {
Token string `json:"token"` Token string `json:"token"`
Url string `json:"url"` 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 { type LatestBundleMetricReq struct {
Limit int64 `json:"limit"` Limit int64 `json:"limit"`
} }
@ -365,6 +425,10 @@ type LatestUsageMetricReq struct {
type LinkNPubThroughTokenRequest struct { type LinkNPubThroughTokenRequest struct {
Token string `json:"token"` Token string `json:"token"`
} }
type LiquidityAssetProvider struct {
Pubkey string `json:"pubkey"`
Tracked *TrackedLiquidityProvider `json:"tracked"`
}
type LiveDebitRequest struct { type LiveDebitRequest struct {
Debit *LiveDebitRequest_debit `json:"debit"` Debit *LiveDebitRequest_debit `json:"debit"`
Npub string `json:"npub"` Npub string `json:"npub"`
@ -378,6 +442,10 @@ type LiveUserOperation struct {
Latest_balance int64 `json:"latest_balance"` Latest_balance int64 `json:"latest_balance"`
Operation *UserOperation `json:"operation"` Operation *UserOperation `json:"operation"`
} }
type LndAssetProvider struct {
Pubkey string `json:"pubkey"`
Tracked *TrackedLndProvider `json:"tracked"`
}
type LndChannels struct { type LndChannels struct {
Open_channels []OpenChannel `json:"open_channels"` Open_channels []OpenChannel `json:"open_channels"`
} }
@ -559,6 +627,11 @@ type PayAddressResponse struct {
Service_fee int64 `json:"service_fee"` Service_fee int64 `json:"service_fee"`
Txid string `json:"txId"` 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 { type PayAdminTransactionSwapRequest struct {
Address string `json:"address"` Address string `json:"address"`
Swap_operation_id string `json:"swap_operation_id"` Swap_operation_id string `json:"swap_operation_id"`
@ -609,6 +682,18 @@ type ProviderDisruption struct {
type ProvidersDisruption struct { type ProvidersDisruption struct {
Disruptions []ProviderDisruption `json:"disruptions"` 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 { type RelaysMigration struct {
Relays []string `json:"relays"` Relays []string `json:"relays"`
} }
@ -665,19 +750,31 @@ type SingleMetricReq struct {
Page int64 `json:"page"` Page int64 `json:"page"`
Request_id int64 `json:"request_id"` Request_id int64 `json:"request_id"`
} }
type SwapOperation struct { type TrackedLiquidityProvider struct {
Address_paid string `json:"address_paid"` Balance int64 `json:"balance"`
Failure_reason string `json:"failure_reason"` Invoices []AssetOperation `json:"invoices"`
Operation_payment *UserOperation `json:"operation_payment"` Payments []AssetOperation `json:"payments"`
Swap_operation_id string `json:"swap_operation_id"`
} }
type SwapsList struct { type TrackedLndProvider struct {
Quotes []TransactionSwapQuote `json:"quotes"` Channels_balance int64 `json:"channels_balance"`
Swaps []SwapOperation `json:"swaps"` 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 { type TransactionSwapQuote struct {
Chain_fee_sats int64 `json:"chain_fee_sats"` 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"` 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_fee_sats int64 `json:"service_fee_sats"`
Service_url string `json:"service_url"` Service_url string `json:"service_url"`
Swap_fee_sats int64 `json:"swap_fee_sats"` Swap_fee_sats int64 `json:"swap_fee_sats"`
@ -690,6 +787,16 @@ type TransactionSwapQuoteList struct {
type TransactionSwapRequest struct { type TransactionSwapRequest struct {
Transaction_amount_sats int64 `json:"transaction_amount_sats"` 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 { type UpdateChannelPolicyRequest struct {
Policy *ChannelPolicy `json:"policy"` Policy *ChannelPolicy `json:"policy"`
Update *UpdateChannelPolicyRequest_update `json:"update"` Update *UpdateChannelPolicyRequest_update `json:"update"`
@ -732,6 +839,7 @@ type UserInfo struct {
Nmanage string `json:"nmanage"` Nmanage string `json:"nmanage"`
Noffer string `json:"noffer"` Noffer string `json:"noffer"`
Service_fee_bps int64 `json:"service_fee_bps"` Service_fee_bps int64 `json:"service_fee_bps"`
Topic_id string `json:"topic_id"`
Userid string `json:"userId"` Userid string `json:"userId"`
User_identifier string `json:"user_identifier"` User_identifier string `json:"user_identifier"`
} }
@ -830,6 +938,18 @@ type NPubLinking_state struct {
Linking_token *string `json:"linking_token"` Linking_token *string `json:"linking_token"`
Unlinked *Empty `json:"unlinked"` 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 type UpdateChannelPolicyRequest_update_type string
const ( const (

View file

@ -545,12 +545,12 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => {
callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) callsMetrics.push({ ...opInfo, ...opStats, ...ctx })
} }
break break
case 'ListSwaps': case 'ListTxSwaps':
if (!methods.ListSwaps) { if (!methods.ListTxSwaps) {
throw new Error('method ListSwaps not found' ) throw new Error('method ListTxSwaps not found' )
} else { } else {
opStats.validate = opStats.guard 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() opStats.handle = process.hrtime.bigint()
callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) callsMetrics.push({ ...opInfo, ...opStats, ...ctx })
} }
@ -693,6 +693,28 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => {
opts.metricsCallback([{ ...info, ...stats, ...ctx }, ...callsMetrics]) 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 } } 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') if (!opts.allowNotImplementedMethods && !methods.CloseChannel) throw new Error('method: CloseChannel is not implemented')
app.post('/api/admin/channel/close', async (req, res) => { app.post('/api/admin/channel/close', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'CloseChannel', batch: false, nostr: false, batchSize: 0} 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 }]) opts.metricsCallback([{ ...info, ...stats, ...authContext }])
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } } catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
}) })
if (!opts.allowNotImplementedMethods && !methods.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') if (!opts.allowNotImplementedMethods && !methods.GetAdminTransactionSwapQuotes) throw new Error('method: GetAdminTransactionSwapQuotes is not implemented')
app.post('/api/admin/swap/transaction/quote', async (req, res) => { app.post('/api/admin/swap/transaction/quote', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'GetAdminTransactionSwapQuotes', batch: false, nostr: false, batchSize: 0} 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 }]) opts.metricsCallback([{ ...info, ...stats, ...authContext }])
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } } catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
}) })
if (!opts.allowNotImplementedMethods && !methods.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') if (!opts.allowNotImplementedMethods && !methods.GetBundleMetrics) throw new Error('method: GetBundleMetrics is not implemented')
app.post('/api/reports/bundle', async (req, res) => { app.post('/api/reports/bundle', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'GetBundleMetrics', batch: false, nostr: false, batchSize: 0} 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 } } 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') 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 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 } 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 = {} let authCtx: Types.AuthContext = {}
@ -1607,20 +1673,39 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => {
opts.metricsCallback([{ ...info, ...stats, ...authContext }]) opts.metricsCallback([{ ...info, ...stats, ...authContext }])
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } } catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
}) })
if (!opts.allowNotImplementedMethods && !methods.ListAdminSwaps) throw new Error('method: ListAdminSwaps is not implemented') if (!opts.allowNotImplementedMethods && !methods.ListAdminInvoiceSwaps) throw new Error('method: ListAdminInvoiceSwaps is not implemented')
app.post('/api/admin/swap/list', async (req, res) => { app.post('/api/admin/swap/invoice/list', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'ListAdminSwaps', batch: false, nostr: false, batchSize: 0} 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 } 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 = {} let authCtx: Types.AuthContext = {}
try { 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']) const authContext = await opts.AdminAuthGuard(req.headers['authorization'])
authCtx = authContext authCtx = authContext
stats.guard = process.hrtime.bigint() stats.guard = process.hrtime.bigint()
stats.validate = stats.guard stats.validate = stats.guard
const query = req.query const query = req.query
const params = req.params 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() stats.handle = process.hrtime.bigint()
res.json({status: 'OK', ...response}) res.json({status: 'OK', ...response})
opts.metricsCallback([{ ...info, ...stats, ...authContext }]) opts.metricsCallback([{ ...info, ...stats, ...authContext }])
@ -1645,20 +1730,20 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => {
opts.metricsCallback([{ ...info, ...stats, ...authContext }]) opts.metricsCallback([{ ...info, ...stats, ...authContext }])
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } } catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
}) })
if (!opts.allowNotImplementedMethods && !methods.ListSwaps) throw new Error('method: ListSwaps is not implemented') if (!opts.allowNotImplementedMethods && !methods.ListTxSwaps) throw new Error('method: ListTxSwaps is not implemented')
app.post('/api/user/swap/list', async (req, res) => { app.post('/api/user/swap/transaction/list', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'ListSwaps', batch: false, nostr: false, batchSize: 0} 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 } 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 = {} let authCtx: Types.AuthContext = {}
try { 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']) const authContext = await opts.UserAuthGuard(req.headers['authorization'])
authCtx = authContext authCtx = authContext
stats.guard = process.hrtime.bigint() stats.guard = process.hrtime.bigint()
stats.validate = stats.guard stats.validate = stats.guard
const query = req.query const query = req.query
const params = req.params 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() stats.handle = process.hrtime.bigint()
res.json({status: 'OK', ...response}) res.json({status: 'OK', ...response})
opts.metricsCallback([{ ...info, ...stats, ...authContext }]) opts.metricsCallback([{ ...info, ...stats, ...authContext }])
@ -1793,6 +1878,28 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => {
opts.metricsCallback([{ ...info, ...stats, ...authContext }]) opts.metricsCallback([{ ...info, ...stats, ...authContext }])
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } } catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
}) })
if (!opts.allowNotImplementedMethods && !methods.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') if (!opts.allowNotImplementedMethods && !methods.PayAdminTransactionSwap) throw new Error('method: PayAdminTransactionSwap is not implemented')
app.post('/api/admin/swap/transaction/pay', async (req, res) => { app.post('/api/admin/swap/transaction/pay', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'PayAdminTransactionSwap', batch: false, nostr: false, batchSize: 0} 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 }]) opts.metricsCallback([{ ...info, ...stats, ...authContext }])
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } } catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
}) })
if (!opts.allowNotImplementedMethods && !methods.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') if (!opts.allowNotImplementedMethods && !methods.RequestNPubLinkingToken) throw new Error('method: RequestNPubLinkingToken is not implemented')
app.post('/api/app/user/npub/token', async (req, res) => { app.post('/api/app/user/npub/token', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'RequestNPubLinkingToken', batch: false, nostr: false, batchSize: 0} 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' } 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)> => { CloseChannel: async (request: Types.CloseChannelRequest): Promise<ResultError | ({ status: 'OK' }& Types.CloseChannelResponse)> => {
const auth = await params.retrieveAdminAuth() const auth = await params.retrieveAdminAuth()
if (auth === null) throw new Error('retrieveAdminAuth() returned null') if (auth === null) throw new Error('retrieveAdminAuth() returned null')
@ -273,6 +284,20 @@ export default (params: ClientParams) => ({
} }
return { status: 'ERROR', reason: 'invalid response' } 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)> => { GetAdminTransactionSwapQuotes: async (request: Types.TransactionSwapRequest): Promise<ResultError | ({ status: 'OK' }& Types.TransactionSwapQuoteList)> => {
const auth = await params.retrieveAdminAuth() const auth = await params.retrieveAdminAuth()
if (auth === null) throw new Error('retrieveAdminAuth() returned null') if (auth === null) throw new Error('retrieveAdminAuth() returned null')
@ -343,6 +368,20 @@ export default (params: ClientParams) => ({
} }
return { status: 'ERROR', reason: 'invalid response' } 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)> => { GetBundleMetrics: async (request: Types.LatestBundleMetricReq): Promise<ResultError | ({ status: 'OK' }& Types.BundleMetrics)> => {
const auth = await params.retrieveMetricsAuth() const auth = await params.retrieveMetricsAuth()
if (auth === null) throw new Error('retrieveMetricsAuth() returned null') if (auth === null) throw new Error('retrieveMetricsAuth() returned null')
@ -620,7 +659,7 @@ export default (params: ClientParams) => ({
GetTransactionSwapQuotes: async (request: Types.TransactionSwapRequest): Promise<ResultError | ({ status: 'OK' }& Types.TransactionSwapQuoteList)> => { GetTransactionSwapQuotes: async (request: Types.TransactionSwapRequest): Promise<ResultError | ({ status: 'OK' }& Types.TransactionSwapQuoteList)> => {
const auth = await params.retrieveUserAuth() const auth = await params.retrieveUserAuth()
if (auth === null) throw new Error('retrieveUserAuth() returned null') 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 } }) 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 === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') { if (data.status === 'OK') {
@ -781,16 +820,30 @@ export default (params: ClientParams) => ({
} }
return { status: 'ERROR', reason: 'invalid response' } 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() const auth = await params.retrieveAdminAuth()
if (auth === null) throw new Error('retrieveAdminAuth() returned null') 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 } }) const { data } = await axios.post(params.baseUrl + finalRoute, {}, { headers: { 'authorization': auth } })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') { if (data.status === 'OK') {
const result = data const result = data
if(!params.checkResult) return { status: 'OK', ...result } if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.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 } if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
} }
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
@ -809,16 +862,16 @@ export default (params: ClientParams) => ({
} }
return { status: 'ERROR', reason: 'invalid response' } 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() const auth = await params.retrieveUserAuth()
if (auth === null) throw new Error('retrieveUserAuth() returned null') 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 } }) const { data } = await axios.post(params.baseUrl + finalRoute, {}, { headers: { 'authorization': auth } })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') { if (data.status === 'OK') {
const result = data const result = data
if(!params.checkResult) return { status: 'OK', ...result } if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.SwapsListValidate(result) const error = Types.TxSwapsListValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
} }
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
@ -909,7 +962,21 @@ export default (params: ClientParams) => ({
} }
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
}, },
PayAdminTransactionSwap: async (request: Types.PayAdminTransactionSwapRequest): Promise<ResultError | ({ status: 'OK' }& Types.AdminSwapResponse)> => { 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() const auth = await params.retrieveAdminAuth()
if (auth === null) throw new Error('retrieveAdminAuth() returned null') if (auth === null) throw new Error('retrieveAdminAuth() returned null')
let finalRoute = '/api/admin/swap/transaction/pay' let finalRoute = '/api/admin/swap/transaction/pay'
@ -918,7 +985,7 @@ export default (params: ClientParams) => ({
if (data.status === 'OK') { if (data.status === 'OK') {
const result = data const result = data
if(!params.checkResult) return { status: 'OK', ...result } if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.AdminSwapResponseValidate(result) const error = Types.AdminTxSwapResponseValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
} }
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
@ -962,6 +1029,20 @@ export default (params: ClientParams) => ({
} }
return { status: 'ERROR', reason: 'invalid response' } 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)> => { RequestNPubLinkingToken: async (request: Types.RequestNPubLinkingTokenRequest): Promise<ResultError | ({ status: 'OK' }& Types.RequestNPubLinkingTokenResponse)> => {
const auth = await params.retrieveAppAuth() const auth = await params.retrieveAppAuth()
if (auth === null) throw new Error('retrieveAppAuth() returned null') if (auth === null) throw new Error('retrieveAppAuth() returned null')

View file

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

View file

@ -427,12 +427,12 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => {
callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) callsMetrics.push({ ...opInfo, ...opStats, ...ctx })
} }
break break
case 'ListSwaps': case 'ListTxSwaps':
if (!methods.ListSwaps) { if (!methods.ListTxSwaps) {
throw new Error('method not defined: ListSwaps') throw new Error('method not defined: ListTxSwaps')
} else { } else {
opStats.validate = opStats.guard 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() opStats.handle = process.hrtime.bigint()
callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) callsMetrics.push({ ...opInfo, ...opStats, ...ctx })
} }
@ -575,6 +575,22 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => {
opts.metricsCallback([{ ...info, ...stats, ...ctx }, ...callsMetrics]) 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 } }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
break break
case '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': case 'CloseChannel':
try { try {
if (!methods.CloseChannel) throw new Error('method: CloseChannel is not implemented') 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 }]) opts.metricsCallback([{ ...info, ...stats, ...authContext }])
}catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
break break
case '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': case 'GetAdminTransactionSwapQuotes':
try { try {
if (!methods.GetAdminTransactionSwapQuotes) throw new Error('method: GetAdminTransactionSwapQuotes is not implemented') 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 }]) opts.metricsCallback([{ ...info, ...stats, ...authContext }])
}catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
break break
case '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': case 'GetBundleMetrics':
try { try {
if (!methods.GetBundleMetrics) throw new Error('method: GetBundleMetrics is not implemented') 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 }]) opts.metricsCallback([{ ...info, ...stats, ...authContext }])
}catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
break break
case 'ListAdminSwaps': case 'ListAdminInvoiceSwaps':
try { 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) const authContext = await opts.NostrAdminAuthGuard(req.appId, req.authIdentifier)
stats.guard = process.hrtime.bigint() stats.guard = process.hrtime.bigint()
authCtx = authContext authCtx = authContext
stats.validate = stats.guard 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() stats.handle = process.hrtime.bigint()
res({status: 'OK', ...response}) res({status: 'OK', ...response})
opts.metricsCallback([{ ...info, ...stats, ...authContext }]) opts.metricsCallback([{ ...info, ...stats, ...authContext }])
@ -1148,14 +1209,14 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => {
opts.metricsCallback([{ ...info, ...stats, ...authContext }]) opts.metricsCallback([{ ...info, ...stats, ...authContext }])
}catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
break break
case 'ListSwaps': case 'ListTxSwaps':
try { 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) const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier)
stats.guard = process.hrtime.bigint() stats.guard = process.hrtime.bigint()
authCtx = authContext authCtx = authContext
stats.validate = stats.guard 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() stats.handle = process.hrtime.bigint()
res({status: 'OK', ...response}) res({status: 'OK', ...response})
opts.metricsCallback([{ ...info, ...stats, ...authContext }]) opts.metricsCallback([{ ...info, ...stats, ...authContext }])
@ -1254,6 +1315,22 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => {
opts.metricsCallback([{ ...info, ...stats, ...authContext }]) opts.metricsCallback([{ ...info, ...stats, ...authContext }])
}catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
break break
case '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': case 'PayAdminTransactionSwap':
try { try {
if (!methods.PayAdminTransactionSwap) throw new Error('method: PayAdminTransactionSwap is not implemented') 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 }]) opts.metricsCallback([{ ...info, ...stats, ...authContext }])
}catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
break break
case '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': case 'ResetDebit':
try { try {
if (!methods.ResetDebit) throw new Error('method: ResetDebit is not implemented') 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; 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) { rpc AddApp(structs.AddAppRequest) returns (structs.AuthApp) {
option (auth_type) = "Admin"; option (auth_type) = "Admin";
option (http_method) = "post"; option (http_method) = "post";
@ -175,6 +189,34 @@ service LightningPub {
option (nostr) = true; 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) { rpc GetAdminTransactionSwapQuotes(structs.TransactionSwapRequest) returns (structs.TransactionSwapQuoteList) {
option (auth_type) = "Admin"; option (auth_type) = "Admin";
option (http_method) = "post"; option (http_method) = "post";
@ -182,17 +224,17 @@ service LightningPub {
option (nostr) = true; option (nostr) = true;
} }
rpc PayAdminTransactionSwap(structs.PayAdminTransactionSwapRequest) returns (structs.AdminSwapResponse) { rpc PayAdminTransactionSwap(structs.PayAdminTransactionSwapRequest) returns (structs.AdminTxSwapResponse) {
option (auth_type) = "Admin"; option (auth_type) = "Admin";
option (http_method) = "post"; option (http_method) = "post";
option (http_route) = "/api/admin/swap/transaction/pay"; option (http_route) = "/api/admin/swap/transaction/pay";
option (nostr) = true; option (nostr) = true;
} }
rpc ListAdminSwaps(structs.Empty) returns (structs.SwapsList) { rpc ListAdminTxSwaps(structs.Empty) returns (structs.TxSwapsList) {
option (auth_type) = "Admin"; option (auth_type) = "Admin";
option (http_method) = "post"; option (http_method) = "post";
option (http_route) = "/api/admin/swap/list"; option (http_route) = "/api/admin/swap/transaction/list";
option (nostr) = true; option (nostr) = true;
} }
@ -520,14 +562,14 @@ service LightningPub {
rpc GetTransactionSwapQuotes(structs.TransactionSwapRequest) returns (structs.TransactionSwapQuoteList){ rpc GetTransactionSwapQuotes(structs.TransactionSwapRequest) returns (structs.TransactionSwapQuoteList){
option (auth_type) = "User"; option (auth_type) = "User";
option (http_method) = "post"; option (http_method) = "post";
option (http_route) = "/api/user/swap/quote"; option (http_route) = "/api/user/swap/transaction/quote";
option (nostr) = true; option (nostr) = true;
} }
rpc ListSwaps(structs.Empty) returns (structs.SwapsList){ rpc ListTxSwaps(structs.Empty) returns (structs.TxSwapsList){
option (auth_type) = "User"; option (auth_type) = "User";
option (http_method) = "post"; option (http_method) = "post";
option (http_route) = "/api/user/swap/list"; option (http_route) = "/api/user/swap/transaction/list";
option (nostr) = true; option (nostr) = true;
} }

View file

@ -19,6 +19,68 @@ message EncryptionExchangeRequest {
string deviceId = 2; 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 { message UserHealthState {
string downtime_reason = 1; string downtime_reason = 1;
} }
@ -37,6 +99,8 @@ message ErrorStats {
ErrorStat past1m = 5; ErrorStat past1m = 5;
} }
message MetricsFile { message MetricsFile {
} }
@ -541,6 +605,7 @@ message UserInfo{
string callback_url = 10; string callback_url = 10;
string bridge_url = 11; string bridge_url = 11;
string nmanage = 12; string nmanage = 12;
string topic_id = 13;
} }
@ -833,6 +898,56 @@ message MessagingToken {
string firebase_messaging_token = 2; 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 { message TransactionSwapRequest {
int64 transaction_amount_sats = 2; int64 transaction_amount_sats = 2;
} }
@ -851,27 +966,31 @@ message TransactionSwapQuote {
int64 chain_fee_sats = 5; int64 chain_fee_sats = 5;
int64 service_fee_sats = 7; int64 service_fee_sats = 7;
string service_url = 8; string service_url = 8;
int64 expires_at_block_height = 9;
int64 paid_at_unix = 10;
int64 completed_at_unix = 11;
} }
message TransactionSwapQuoteList { message TransactionSwapQuoteList {
repeated TransactionSwapQuote quotes = 1; repeated TransactionSwapQuote quotes = 1;
} }
message AdminSwapResponse { message AdminTxSwapResponse {
string tx_id = 1; string tx_id = 1;
int64 network_fee = 2; int64 network_fee = 2;
} }
message SwapOperation { message TxSwapOperation {
string swap_operation_id = 1; TransactionSwapQuote quote = 1;
optional UserOperation operation_payment = 2; optional UserOperation operation_payment = 2;
optional string failure_reason = 3; optional string failure_reason = 3;
string address_paid = 4; optional string address_paid = 4;
optional string tx_id = 5;
} }
message SwapsList { message TxSwapsList {
repeated SwapOperation swaps = 1; repeated TxSwapOperation swaps = 1;
repeated TransactionSwapQuote quotes = 2;
} }
message CumulativeFees { message CumulativeFees {
@ -886,3 +1005,18 @@ message BeaconData {
optional string nextRelay = 4; optional string nextRelay = 4;
optional CumulativeFees fees = 5; 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;
}
}

731
src/extensions/README.md Normal file
View file

@ -0,0 +1,731 @@
# Lightning.Pub Extension System
A modular extension system that allows third-party functionality to be added to Lightning.Pub without modifying core code.
## Table of Contents
- [Overview](#overview)
- [Architecture](#architecture)
- [Creating an Extension](#creating-an-extension)
- [Extension Lifecycle](#extension-lifecycle)
- [ExtensionContext API](#extensioncontext-api)
- [Database Isolation](#database-isolation)
- [RPC Methods](#rpc-methods)
- [HTTP Routes](#http-routes)
- [Event Handling](#event-handling)
- [Configuration](#configuration)
- [Examples](#examples)
---
## Overview
The extension system provides:
- **Modularity**: Extensions are self-contained modules with their own code and data
- **Isolation**: Each extension gets its own SQLite database
- **Integration**: Extensions can register RPC methods, handle events, and interact with Lightning.Pub's payment and Nostr systems
- **Lifecycle Management**: Automatic discovery, loading, and graceful shutdown
### Built-in Extensions
| Extension | Description |
|-----------|-------------|
| `marketplace` | NIP-15 Nostr marketplace for selling products via Lightning |
| `withdraw` | LNURL-withdraw (LUD-03) for vouchers, faucets, and gifts |
---
## Architecture
```
┌─────────────────────────────────────────────────────────────────┐
│ Lightning.Pub │
├─────────────────────────────────────────────────────────────────┤
│ Extension Loader │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Extension A │ │ Extension B │ │ Extension C │ ... │
│ │ ┌───────┐ │ │ ┌───────┐ │ │ ┌───────┐ │ │
│ │ │Context│ │ │ │Context│ │ │ │Context│ │ │
│ │ └───────┘ │ │ └───────┘ │ │ └───────┘ │ │
│ │ ┌───────┐ │ │ ┌───────┐ │ │ ┌───────┐ │ │
│ │ │ DB │ │ │ │ DB │ │ │ │ DB │ │ │
│ │ └───────┘ │ │ └───────┘ │ │ └───────┘ │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ Payment Manager │ Nostr Transport │ Application Manager │
└─────────────────────────────────────────────────────────────────┘
```
### Key Components
| Component | File | Description |
|-----------|------|-------------|
| `ExtensionLoader` | `loader.ts` | Discovers, loads, and manages extensions |
| `ExtensionContext` | `context.ts` | Bridge between extensions and Lightning.Pub |
| `ExtensionDatabase` | `database.ts` | Isolated SQLite database per extension |
---
## Creating an Extension
### Directory Structure
```
src/extensions/
└── my-extension/
├── index.ts # Main entry point (required)
├── types.ts # TypeScript interfaces
├── migrations.ts # Database migrations
└── managers/ # Business logic
└── myManager.ts
```
### Minimal Extension
```typescript
// src/extensions/my-extension/index.ts
import { Extension, ExtensionInfo, ExtensionContext, ExtensionDatabase } from '../types.js'
export default class MyExtension implements Extension {
readonly info: ExtensionInfo = {
id: 'my-extension', // Must match directory name
name: 'My Extension',
version: '1.0.0',
description: 'Does something useful',
author: 'Your Name',
minPubVersion: '1.0.0' // Minimum Lightning.Pub version
}
async initialize(ctx: ExtensionContext, db: ExtensionDatabase): Promise<void> {
// Run migrations
await db.execute(`
CREATE TABLE IF NOT EXISTS my_table (
id TEXT PRIMARY KEY,
data TEXT
)
`)
// Register RPC methods
ctx.registerMethod('my-extension.doSomething', async (req, appId) => {
return { result: 'done' }
})
ctx.log('info', 'Extension initialized')
}
async shutdown(): Promise<void> {
// Cleanup resources
}
}
```
### Extension Interface
```typescript
interface Extension {
// Required: Extension metadata
readonly info: ExtensionInfo
// Required: Called once when extension is loaded
initialize(ctx: ExtensionContext, db: ExtensionDatabase): Promise<void>
// Optional: Called when Lightning.Pub shuts down
shutdown?(): Promise<void>
// Optional: Health check for monitoring
healthCheck?(): Promise<boolean>
}
interface ExtensionInfo {
id: string // Unique identifier (lowercase, no spaces)
name: string // Display name
version: string // Semver version
description: string // Short description
author: string // Author name
minPubVersion?: string // Minimum Lightning.Pub version
dependencies?: string[] // Other extension IDs required
}
```
---
## Extension Lifecycle
```
┌──────────────┐
│ Discover │ Scan extensions directory for index.ts files
└──────┬───────┘
┌──────────────┐
│ Load │ Import module, instantiate class
└──────┬───────┘
┌──────────────┐
│ Initialize │ Create database, call initialize()
└──────┬───────┘
┌──────────────┐
│ Ready │ Extension is active, handling requests
└──────┬───────┘
▼ (on shutdown)
┌──────────────┐
│ Shutdown │ Call shutdown(), close database
└──────────────┘
```
### States
| State | Description |
|-------|-------------|
| `loading` | Extension is being loaded |
| `ready` | Extension is active and healthy |
| `error` | Initialization failed |
| `stopped` | Extension has been shut down |
---
## ExtensionContext API
The `ExtensionContext` is passed to your extension during initialization. It provides access to Lightning.Pub functionality.
### Application Management
```typescript
// Get information about an application
const app = await ctx.getApplication(applicationId)
// Returns: { id, name, nostr_public, balance_sats } | null
```
### Payment Operations
```typescript
// Create a Lightning invoice
const invoice = await ctx.createInvoice(amountSats, {
memo: 'Payment for service',
expiry: 3600, // seconds
metadata: { order_id: '123' } // Returned in payment callback
})
// Returns: { id, paymentRequest, paymentHash, expiry }
// Pay a Lightning invoice
const result = await ctx.payInvoice(applicationId, bolt11Invoice, maxFeeSats)
// Returns: { paymentHash, feeSats }
```
### Nostr Operations
```typescript
// Send encrypted DM (NIP-44)
const eventId = await ctx.sendEncryptedDM(applicationId, recipientPubkey, content)
// Publish a Nostr event (signed by application's key)
const eventId = await ctx.publishNostrEvent({
kind: 30017,
pubkey: appPubkey,
created_at: Math.floor(Date.now() / 1000),
tags: [['d', 'identifier']],
content: JSON.stringify(data)
})
```
### RPC Method Registration
```typescript
// Register a method that can be called via RPC
ctx.registerMethod('my-extension.methodName', async (request, applicationId, userPubkey?) => {
// request: The RPC request payload
// applicationId: The calling application's ID
// userPubkey: The user's Nostr pubkey (if authenticated)
return { result: 'success' }
})
```
### Event Subscriptions
```typescript
// Subscribe to payment received events
ctx.onPaymentReceived(async (payment) => {
// payment: { invoiceId, paymentHash, amountSats, metadata }
if (payment.metadata?.extension === 'my-extension') {
// Handle payment for this extension
}
})
// Subscribe to incoming Nostr events
ctx.onNostrEvent(async (event, applicationId) => {
// event: { id, pubkey, kind, tags, content, created_at }
// applicationId: The application this event is for
if (event.kind === 4) { // DM
// Handle incoming message
}
})
```
### Logging
```typescript
ctx.log('debug', 'Detailed debugging info')
ctx.log('info', 'Normal operation info')
ctx.log('warn', 'Warning message')
ctx.log('error', 'Error occurred', errorObject)
```
---
## Database Isolation
Each extension gets its own SQLite database file at:
```
{databaseDir}/{extension-id}.db
```
### Database Interface
```typescript
interface ExtensionDatabase {
// Execute write queries (INSERT, UPDATE, DELETE, CREATE)
execute(sql: string, params?: any[]): Promise<{ changes?: number; lastId?: number }>
// Execute read queries (SELECT)
query<T>(sql: string, params?: any[]): Promise<T[]>
// Run multiple statements in a transaction
transaction<T>(fn: () => Promise<T>): Promise<T>
}
```
### Migration Pattern
```typescript
// migrations.ts
export interface Migration {
version: number
name: string
up: (db: ExtensionDatabase) => Promise<void>
}
export const migrations: Migration[] = [
{
version: 1,
name: 'create_initial_tables',
up: async (db) => {
await db.execute(`
CREATE TABLE items (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
created_at INTEGER NOT NULL
)
`)
}
},
{
version: 2,
name: 'add_status_column',
up: async (db) => {
await db.execute(`ALTER TABLE items ADD COLUMN status TEXT DEFAULT 'active'`)
}
}
]
// Run migrations in initialize()
export async function runMigrations(db: ExtensionDatabase): Promise<void> {
const result = await db.query<{ value: string }>(
`SELECT value FROM _extension_meta WHERE key = 'migration_version'`
).catch(() => [])
const currentVersion = result.length > 0 ? parseInt(result[0].value, 10) : 0
for (const migration of migrations) {
if (migration.version > currentVersion) {
console.log(`Running migration ${migration.version}: ${migration.name}`)
await migration.up(db)
await db.execute(
`INSERT INTO _extension_meta (key, value) VALUES ('migration_version', ?)
ON CONFLICT(key) DO UPDATE SET value = excluded.value`,
[String(migration.version)]
)
}
}
}
```
---
## RPC Methods
Extensions register RPC methods that can be called by clients.
### Naming Convention
Methods should be namespaced with the extension ID:
```
{extension-id}.{methodName}
```
Examples:
- `marketplace.createStall`
- `withdraw.createLink`
### Method Handler Signature
```typescript
type RpcMethodHandler = (
request: any, // The request payload
applicationId: string, // The calling application
userPubkey?: string // The authenticated user (if any)
) => Promise<any>
```
### Example
```typescript
ctx.registerMethod('my-extension.createItem', async (req, appId, userPubkey) => {
// Validate request
if (!req.name) {
throw new Error('Name is required')
}
// Create item
const item = await this.manager.create(appId, req)
// Return response
return { item }
})
```
---
## HTTP Routes
Some extensions need HTTP endpoints (e.g., LNURL protocol). Extensions can define routes that the main application mounts.
### Defining Routes
```typescript
interface HttpRoute {
method: 'GET' | 'POST'
path: string
handler: (req: HttpRequest) => Promise<HttpResponse>
}
interface HttpRequest {
params: Record<string, string> // URL path params
query: Record<string, string> // Query string params
body?: any // POST body
headers: Record<string, string>
}
interface HttpResponse {
status: number
body: any
headers?: Record<string, string>
}
```
### Example
```typescript
class MyExtension implements Extension {
getHttpRoutes(): HttpRoute[] {
return [
{
method: 'GET',
path: '/api/v1/my-extension/:id',
handler: async (req) => {
const item = await this.getItem(req.params.id)
return {
status: 200,
body: item,
headers: { 'Content-Type': 'application/json' }
}
}
}
]
}
}
```
---
## Event Handling
### Payment Callbacks
When you create an invoice with metadata, you'll receive that metadata back in the payment callback:
```typescript
// Creating invoice with metadata
const invoice = await ctx.createInvoice(1000, {
metadata: {
extension: 'my-extension',
order_id: 'order-123'
}
})
// Handling payment
ctx.onPaymentReceived(async (payment) => {
if (payment.metadata?.extension === 'my-extension') {
const orderId = payment.metadata.order_id
await this.handlePayment(orderId, payment)
}
})
```
### Nostr Events
Subscribe to Nostr events for your application:
```typescript
ctx.onNostrEvent(async (event, applicationId) => {
// Filter by event kind
if (event.kind === 4) { // Encrypted DM
await this.handleDirectMessage(event, applicationId)
}
})
```
---
## Configuration
### Loader Configuration
```typescript
interface ExtensionLoaderConfig {
extensionsDir: string // Directory containing extensions
databaseDir: string // Directory for extension databases
enabledExtensions?: string[] // Whitelist (if set, only these load)
disabledExtensions?: string[] // Blacklist
}
```
### Usage
```typescript
import { createExtensionLoader } from './extensions'
const loader = createExtensionLoader({
extensionsDir: './src/extensions',
databaseDir: './data/extensions',
disabledExtensions: ['experimental-ext']
}, mainHandler)
await loader.loadAll()
// Call extension methods
const result = await loader.callMethod(
'marketplace.createStall',
{ name: 'My Shop', currency: 'sat', shipping_zones: [] },
applicationId,
userPubkey
)
// Dispatch events
loader.dispatchPaymentReceived(paymentData)
loader.dispatchNostrEvent(event, applicationId)
// Shutdown
await loader.shutdown()
```
---
## Examples
### Example: Simple Counter Extension
```typescript
// src/extensions/counter/index.ts
import { Extension, ExtensionInfo, ExtensionContext, ExtensionDatabase } from '../types.js'
export default class CounterExtension implements Extension {
readonly info: ExtensionInfo = {
id: 'counter',
name: 'Simple Counter',
version: '1.0.0',
description: 'A simple counter for each application',
author: 'Example'
}
private db!: ExtensionDatabase
async initialize(ctx: ExtensionContext, db: ExtensionDatabase): Promise<void> {
this.db = db
await db.execute(`
CREATE TABLE IF NOT EXISTS counters (
application_id TEXT PRIMARY KEY,
count INTEGER NOT NULL DEFAULT 0
)
`)
ctx.registerMethod('counter.increment', async (req, appId) => {
await db.execute(
`INSERT INTO counters (application_id, count) VALUES (?, 1)
ON CONFLICT(application_id) DO UPDATE SET count = count + 1`,
[appId]
)
const result = await db.query<{ count: number }>(
'SELECT count FROM counters WHERE application_id = ?',
[appId]
)
return { count: result[0]?.count || 0 }
})
ctx.registerMethod('counter.get', async (req, appId) => {
const result = await db.query<{ count: number }>(
'SELECT count FROM counters WHERE application_id = ?',
[appId]
)
return { count: result[0]?.count || 0 }
})
ctx.registerMethod('counter.reset', async (req, appId) => {
await db.execute(
'UPDATE counters SET count = 0 WHERE application_id = ?',
[appId]
)
return { count: 0 }
})
}
}
```
### Example: Payment-Triggered Extension
```typescript
// src/extensions/donations/index.ts
import { Extension, ExtensionContext, ExtensionDatabase } from '../types.js'
export default class DonationsExtension implements Extension {
readonly info = {
id: 'donations',
name: 'Donations',
version: '1.0.0',
description: 'Accept donations with thank-you messages',
author: 'Example'
}
private db!: ExtensionDatabase
private ctx!: ExtensionContext
async initialize(ctx: ExtensionContext, db: ExtensionDatabase): Promise<void> {
this.db = db
this.ctx = ctx
await db.execute(`
CREATE TABLE IF NOT EXISTS donations (
id TEXT PRIMARY KEY,
application_id TEXT NOT NULL,
amount_sats INTEGER NOT NULL,
donor_pubkey TEXT,
message TEXT,
created_at INTEGER NOT NULL
)
`)
// Create donation invoice
ctx.registerMethod('donations.createInvoice', async (req, appId) => {
const invoice = await ctx.createInvoice(req.amount_sats, {
memo: req.message || 'Donation',
metadata: {
extension: 'donations',
donor_pubkey: req.donor_pubkey,
message: req.message
}
})
return { invoice: invoice.paymentRequest }
})
// Handle successful payments
ctx.onPaymentReceived(async (payment) => {
if (payment.metadata?.extension !== 'donations') return
// Record donation
await db.execute(
`INSERT INTO donations (id, application_id, amount_sats, donor_pubkey, message, created_at)
VALUES (?, ?, ?, ?, ?, ?)`,
[
payment.paymentHash,
payment.metadata.application_id,
payment.amountSats,
payment.metadata.donor_pubkey,
payment.metadata.message,
Math.floor(Date.now() / 1000)
]
)
// Send thank-you DM if donor has pubkey
if (payment.metadata.donor_pubkey) {
await ctx.sendEncryptedDM(
payment.metadata.application_id,
payment.metadata.donor_pubkey,
`Thank you for your donation of ${payment.amountSats} sats!`
)
}
})
// List donations
ctx.registerMethod('donations.list', async (req, appId) => {
const donations = await db.query(
`SELECT * FROM donations WHERE application_id = ? ORDER BY created_at DESC LIMIT ?`,
[appId, req.limit || 50]
)
return { donations }
})
}
}
```
---
## Best Practices
1. **Namespace your methods**: Always prefix RPC methods with your extension ID
2. **Use migrations**: Never modify existing migration files; create new ones
3. **Handle errors gracefully**: Throw descriptive errors, don't return error objects
4. **Clean up in shutdown**: Close connections, cancel timers, etc.
5. **Log appropriately**: Use debug for verbose info, error for failures
6. **Validate inputs**: Check request parameters before processing
7. **Use transactions**: For multi-step database operations
8. **Document your API**: Include types and descriptions for RPC methods
---
## Troubleshooting
### Extension not loading
1. Check that directory name matches `info.id`
2. Verify `index.ts` has a default export
3. Check for TypeScript/import errors in logs
### Database errors
1. Check migration syntax
2. Verify column types match queries
3. Look for migration version conflicts
### RPC method not found
1. Verify method is registered in `initialize()`
2. Check method name includes extension prefix
3. Ensure extension status is `ready`
### Payment callbacks not firing
1. Verify `metadata.extension` matches your extension ID
2. Check that `onPaymentReceived` is registered in `initialize()`
3. Confirm invoice was created through the extension

309
src/extensions/context.ts Normal file
View file

@ -0,0 +1,309 @@
import {
ExtensionContext,
ExtensionDatabase,
ExtensionInfo,
ApplicationInfo,
CreateInvoiceOptions,
CreatedInvoice,
PaymentReceivedData,
NostrEvent,
UnsignedNostrEvent,
RpcMethodHandler,
LnurlPayInfo
} from './types.js'
/**
* Main Handler interface (from Lightning.Pub)
* This is a minimal interface - the actual MainHandler has more methods
*/
export interface MainHandlerInterface {
// Application management
applicationManager: {
getById(id: string): Promise<any>
}
// Payment operations
paymentManager: {
createInvoice(params: {
applicationId: string
amountSats: number
memo?: string
expiry?: number
metadata?: Record<string, any>
}): Promise<{
id: string
paymentRequest: string
paymentHash: string
expiry: number
}>
payInvoice(params: {
applicationId: string
paymentRequest: string
maxFeeSats?: number
}): Promise<{
paymentHash: string
feeSats: number
}>
/**
* Get LNURL-pay info for a user by their Nostr pubkey
* This enables Lightning Address (LUD-16) and zap (NIP-57) support
*/
getLnurlPayInfoByPubkey(pubkeyHex: string, options?: {
metadata?: string
description?: string
}): Promise<LnurlPayInfo>
}
// Nostr operations
sendNostrEvent(event: any): Promise<string | null>
sendEncryptedDM(applicationId: string, recipientPubkey: string, content: string): Promise<string>
}
/**
* Callback registries for extension events
*/
interface CallbackRegistries {
paymentReceived: Array<(payment: PaymentReceivedData) => Promise<void>>
nostrEvent: Array<(event: NostrEvent, applicationId: string) => Promise<void>>
}
/**
* Registered RPC method
*/
interface RegisteredMethod {
extensionId: string
handler: RpcMethodHandler
}
/**
* Extension Context Implementation
*
* Provides the interface for extensions to interact with Lightning.Pub.
* Each extension gets its own context instance.
*/
export class ExtensionContextImpl implements ExtensionContext {
private callbacks: CallbackRegistries = {
paymentReceived: [],
nostrEvent: []
}
constructor(
private extensionInfo: ExtensionInfo,
private database: ExtensionDatabase,
private mainHandler: MainHandlerInterface,
private methodRegistry: Map<string, RegisteredMethod>
) {}
/**
* Get information about an application
*/
async getApplication(applicationId: string): Promise<ApplicationInfo | null> {
try {
const app = await this.mainHandler.applicationManager.getById(applicationId)
if (!app) return null
return {
id: app.id,
name: app.name,
nostr_public: app.nostr_public,
balance_sats: app.balance || 0
}
} catch (e) {
this.log('error', `Failed to get application ${applicationId}:`, e)
return null
}
}
/**
* Create a Lightning invoice
*/
async createInvoice(amountSats: number, options: CreateInvoiceOptions = {}): Promise<CreatedInvoice> {
// Note: In practice, this needs an applicationId. Extensions typically
// get this from the RPC request context. For now, we'll need to handle
// this in the actual implementation.
throw new Error('createInvoice requires applicationId from request context')
}
/**
* Create invoice with explicit application ID
* This is the internal method used by extensions
*/
async createInvoiceForApp(
applicationId: string,
amountSats: number,
options: CreateInvoiceOptions = {}
): Promise<CreatedInvoice> {
const result = await this.mainHandler.paymentManager.createInvoice({
applicationId,
amountSats,
memo: options.memo,
expiry: options.expiry,
metadata: {
...options.metadata,
extension: this.extensionInfo.id
}
})
return {
id: result.id,
paymentRequest: result.paymentRequest,
paymentHash: result.paymentHash,
expiry: result.expiry
}
}
/**
* Pay a Lightning invoice
*/
async payInvoice(
applicationId: string,
paymentRequest: string,
maxFeeSats?: number
): Promise<{ paymentHash: string; feeSats: number }> {
return this.mainHandler.paymentManager.payInvoice({
applicationId,
paymentRequest,
maxFeeSats
})
}
/**
* Send an encrypted DM via Nostr
*/
async sendEncryptedDM(
applicationId: string,
recipientPubkey: string,
content: string
): Promise<string> {
return this.mainHandler.sendEncryptedDM(applicationId, recipientPubkey, content)
}
/**
* Publish a Nostr event
*/
async publishNostrEvent(event: UnsignedNostrEvent): Promise<string | null> {
return this.mainHandler.sendNostrEvent(event)
}
/**
* Get LNURL-pay info for a user by pubkey
* Enables Lightning Address and zap support
*/
async getLnurlPayInfo(pubkeyHex: string, options?: {
metadata?: string
description?: string
}): Promise<LnurlPayInfo> {
return this.mainHandler.paymentManager.getLnurlPayInfoByPubkey(pubkeyHex, options)
}
/**
* Subscribe to payment received callbacks
*/
onPaymentReceived(callback: (payment: PaymentReceivedData) => Promise<void>): void {
this.callbacks.paymentReceived.push(callback)
}
/**
* Subscribe to incoming Nostr events
*/
onNostrEvent(callback: (event: NostrEvent, applicationId: string) => Promise<void>): void {
this.callbacks.nostrEvent.push(callback)
}
/**
* Register an RPC method
*/
registerMethod(name: string, handler: RpcMethodHandler): void {
const fullName = name.startsWith(`${this.extensionInfo.id}.`)
? name
: `${this.extensionInfo.id}.${name}`
if (this.methodRegistry.has(fullName)) {
throw new Error(`RPC method ${fullName} already registered`)
}
this.methodRegistry.set(fullName, {
extensionId: this.extensionInfo.id,
handler
})
this.log('debug', `Registered RPC method: ${fullName}`)
}
/**
* Get the extension's database
*/
getDatabase(): ExtensionDatabase {
return this.database
}
/**
* Log a message
*/
log(level: 'debug' | 'info' | 'warn' | 'error', message: string, ...args: any[]): void {
const prefix = `[Extension:${this.extensionInfo.id}]`
switch (level) {
case 'debug':
console.debug(prefix, message, ...args)
break
case 'info':
console.info(prefix, message, ...args)
break
case 'warn':
console.warn(prefix, message, ...args)
break
case 'error':
console.error(prefix, message, ...args)
break
}
}
// ===== Internal Methods (called by ExtensionLoader) =====
/**
* Dispatch payment received event to extension callbacks
*/
async dispatchPaymentReceived(payment: PaymentReceivedData): Promise<void> {
for (const callback of this.callbacks.paymentReceived) {
try {
await callback(payment)
} catch (e) {
this.log('error', 'Error in payment callback:', e)
}
}
}
/**
* Dispatch Nostr event to extension callbacks
*/
async dispatchNostrEvent(event: NostrEvent, applicationId: string): Promise<void> {
for (const callback of this.callbacks.nostrEvent) {
try {
await callback(event, applicationId)
} catch (e) {
this.log('error', 'Error in Nostr event callback:', e)
}
}
}
/**
* Get registered callbacks for external access
*/
getCallbacks(): CallbackRegistries {
return this.callbacks
}
}
/**
* Create an extension context
*/
export function createExtensionContext(
extensionInfo: ExtensionInfo,
database: ExtensionDatabase,
mainHandler: MainHandlerInterface,
methodRegistry: Map<string, RegisteredMethod>
): ExtensionContextImpl {
return new ExtensionContextImpl(extensionInfo, database, mainHandler, methodRegistry)
}

148
src/extensions/database.ts Normal file
View file

@ -0,0 +1,148 @@
import Database from 'better-sqlite3'
import path from 'path'
import fs from 'fs'
import { ExtensionDatabase } from './types.js'
/**
* Extension Database Implementation
*
* Provides isolated SQLite database access for each extension.
* Uses better-sqlite3 for synchronous, high-performance access.
*/
export class ExtensionDatabaseImpl implements ExtensionDatabase {
private db: Database.Database
private extensionId: string
constructor(extensionId: string, databaseDir: string) {
this.extensionId = extensionId
// Ensure database directory exists
if (!fs.existsSync(databaseDir)) {
fs.mkdirSync(databaseDir, { recursive: true })
}
// Create database file for this extension
const dbPath = path.join(databaseDir, `${extensionId}.db`)
this.db = new Database(dbPath)
// Enable WAL mode for better concurrency
this.db.pragma('journal_mode = WAL')
// Enable foreign keys
this.db.pragma('foreign_keys = ON')
// Create metadata table for tracking migrations
this.db.exec(`
CREATE TABLE IF NOT EXISTS _extension_meta (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
)
`)
}
/**
* Execute a write query (INSERT, UPDATE, DELETE, CREATE, etc.)
*/
async execute(sql: string, params: any[] = []): Promise<{ changes?: number; lastId?: number }> {
try {
const stmt = this.db.prepare(sql)
const result = stmt.run(...params)
return {
changes: result.changes,
lastId: result.lastInsertRowid as number
}
} catch (e) {
console.error(`[Extension:${this.extensionId}] Database execute error:`, e)
throw e
}
}
/**
* Execute a read query (SELECT)
*/
async query<T = any>(sql: string, params: any[] = []): Promise<T[]> {
try {
const stmt = this.db.prepare(sql)
return stmt.all(...params) as T[]
} catch (e) {
console.error(`[Extension:${this.extensionId}] Database query error:`, e)
throw e
}
}
/**
* Execute multiple statements in a transaction
*/
async transaction<T>(fn: () => Promise<T>): Promise<T> {
const runTransaction = this.db.transaction(() => {
// Note: better-sqlite3 transactions are synchronous
// We wrap the async function but it executes synchronously
return fn()
})
return runTransaction() as T
}
/**
* Get a metadata value
*/
async getMeta(key: string): Promise<string | null> {
const rows = await this.query<{ value: string }>(
'SELECT value FROM _extension_meta WHERE key = ?',
[key]
)
return rows.length > 0 ? rows[0].value : null
}
/**
* Set a metadata value
*/
async setMeta(key: string, value: string): Promise<void> {
await this.execute(
`INSERT INTO _extension_meta (key, value) VALUES (?, ?)
ON CONFLICT(key) DO UPDATE SET value = excluded.value`,
[key, value]
)
}
/**
* Get current migration version
*/
async getMigrationVersion(): Promise<number> {
const version = await this.getMeta('migration_version')
return version ? parseInt(version, 10) : 0
}
/**
* Set migration version
*/
async setMigrationVersion(version: number): Promise<void> {
await this.setMeta('migration_version', String(version))
}
/**
* Close the database connection
*/
close(): void {
this.db.close()
}
/**
* Get the underlying database for advanced operations
* (Use with caution - bypasses isolation)
*/
getUnderlyingDb(): Database.Database {
return this.db
}
}
/**
* Create an extension database instance
*/
export function createExtensionDatabase(
extensionId: string,
databaseDir: string
): ExtensionDatabaseImpl {
return new ExtensionDatabaseImpl(extensionId, databaseDir)
}

56
src/extensions/index.ts Normal file
View file

@ -0,0 +1,56 @@
/**
* Lightning.Pub Extension System
*
* This module provides the extension infrastructure for Lightning.Pub.
* Extensions can add functionality like marketplaces, subscriptions,
* tipping, and more.
*
* Usage:
*
* ```typescript
* import { createExtensionLoader, ExtensionLoaderConfig } from './extensions'
*
* const config: ExtensionLoaderConfig = {
* extensionsDir: './extensions',
* databaseDir: './data/extensions'
* }
*
* const loader = createExtensionLoader(config, mainHandler)
* await loader.loadAll()
*
* // Call extension methods
* const result = await loader.callMethod(
* 'marketplace.createStall',
* { name: 'My Shop', currency: 'sat', shipping_zones: [...] },
* applicationId
* )
* ```
*/
// Export types
export {
Extension,
ExtensionInfo,
ExtensionContext,
ExtensionDatabase,
ExtensionModule,
ExtensionConstructor,
LoadedExtension,
ExtensionLoaderConfig,
ApplicationInfo,
CreateInvoiceOptions,
CreatedInvoice,
PaymentReceivedData,
NostrEvent,
UnsignedNostrEvent,
RpcMethodHandler
} from './types.js'
// Export loader
export { ExtensionLoader, createExtensionLoader } from './loader.js'
// Export database utilities
export { ExtensionDatabaseImpl, createExtensionDatabase } from './database.js'
// Export context utilities
export { ExtensionContextImpl, createExtensionContext, MainHandlerInterface } from './context.js'

406
src/extensions/loader.ts Normal file
View file

@ -0,0 +1,406 @@
import path from 'path'
import fs from 'fs'
import {
Extension,
ExtensionInfo,
ExtensionModule,
LoadedExtension,
ExtensionLoaderConfig,
RpcMethodHandler,
PaymentReceivedData,
NostrEvent
} from './types.js'
import { ExtensionDatabaseImpl, createExtensionDatabase } from './database.js'
import { ExtensionContextImpl, createExtensionContext, MainHandlerInterface } from './context.js'
/**
* Registered RPC method entry
*/
interface RegisteredMethod {
extensionId: string
handler: RpcMethodHandler
}
/**
* Extension Loader
*
* Discovers, loads, and manages Lightning.Pub extensions.
* Provides lifecycle management and event dispatching.
*/
export class ExtensionLoader {
private config: ExtensionLoaderConfig
private mainHandler: MainHandlerInterface
private extensions: Map<string, LoadedExtension> = new Map()
private contexts: Map<string, ExtensionContextImpl> = new Map()
private methodRegistry: Map<string, RegisteredMethod> = new Map()
private initialized = false
constructor(config: ExtensionLoaderConfig, mainHandler: MainHandlerInterface) {
this.config = config
this.mainHandler = mainHandler
}
/**
* Discover and load all extensions
*/
async loadAll(): Promise<void> {
if (this.initialized) {
throw new Error('Extension loader already initialized')
}
console.log('[Extensions] Loading extensions from:', this.config.extensionsDir)
// Ensure directories exist
if (!fs.existsSync(this.config.extensionsDir)) {
console.log('[Extensions] Extensions directory does not exist, creating...')
fs.mkdirSync(this.config.extensionsDir, { recursive: true })
this.initialized = true
return
}
if (!fs.existsSync(this.config.databaseDir)) {
fs.mkdirSync(this.config.databaseDir, { recursive: true })
}
// Discover extensions
const extensionDirs = await this.discoverExtensions()
console.log(`[Extensions] Found ${extensionDirs.length} extension(s)`)
// Load extensions in dependency order
const loadOrder = await this.resolveDependencies(extensionDirs)
for (const extDir of loadOrder) {
try {
await this.loadExtension(extDir)
} catch (e) {
console.error(`[Extensions] Failed to load extension from ${extDir}:`, e)
}
}
this.initialized = true
console.log(`[Extensions] Loaded ${this.extensions.size} extension(s)`)
}
/**
* Discover extension directories
*/
private async discoverExtensions(): Promise<string[]> {
const entries = fs.readdirSync(this.config.extensionsDir, { withFileTypes: true })
const extensionDirs: string[] = []
for (const entry of entries) {
if (!entry.isDirectory()) continue
const extDir = path.join(this.config.extensionsDir, entry.name)
const indexPath = path.join(extDir, 'index.ts')
const indexJsPath = path.join(extDir, 'index.js')
// Check for index file
if (fs.existsSync(indexPath) || fs.existsSync(indexJsPath)) {
// Check enabled/disabled lists
if (this.config.disabledExtensions?.includes(entry.name)) {
console.log(`[Extensions] Skipping disabled extension: ${entry.name}`)
continue
}
if (this.config.enabledExtensions &&
!this.config.enabledExtensions.includes(entry.name)) {
console.log(`[Extensions] Skipping non-enabled extension: ${entry.name}`)
continue
}
extensionDirs.push(extDir)
}
}
return extensionDirs
}
/**
* Resolve extension dependencies and return load order
*/
private async resolveDependencies(extensionDirs: string[]): Promise<string[]> {
// For now, simple alphabetical order
// TODO: Implement proper dependency resolution with topological sort
return extensionDirs.sort()
}
/**
* Load a single extension
*/
private async loadExtension(extensionDir: string): Promise<void> {
const dirName = path.basename(extensionDir)
console.log(`[Extensions] Loading extension: ${dirName}`)
// Determine index file path
let indexPath = path.join(extensionDir, 'index.js')
if (!fs.existsSync(indexPath)) {
indexPath = path.join(extensionDir, 'index.ts')
}
// Dynamic import
const moduleUrl = `file://${indexPath}`
const module = await import(moduleUrl) as ExtensionModule
if (!module.default) {
throw new Error(`Extension ${dirName} has no default export`)
}
// Instantiate extension
const ExtensionClass = module.default
const instance = new ExtensionClass() as Extension
if (!instance.info) {
throw new Error(`Extension ${dirName} has no info property`)
}
const info = instance.info
// Validate extension ID matches directory name
if (info.id !== dirName) {
console.warn(
`[Extensions] Extension ID '${info.id}' doesn't match directory '${dirName}'`
)
}
// Check for duplicate
if (this.extensions.has(info.id)) {
throw new Error(`Extension ${info.id} already loaded`)
}
// Create isolated database
const database = createExtensionDatabase(info.id, this.config.databaseDir)
// Create context
const context = createExtensionContext(
info,
database,
this.mainHandler,
this.methodRegistry
)
// Track as loading
const loaded: LoadedExtension = {
info,
instance,
database,
status: 'loading',
loadedAt: Date.now()
}
this.extensions.set(info.id, loaded)
this.contexts.set(info.id, context)
try {
// Initialize extension
await instance.initialize(context, database)
loaded.status = 'ready'
console.log(`[Extensions] Extension ${info.id} v${info.version} loaded successfully`)
} catch (e) {
loaded.status = 'error'
loaded.error = e as Error
console.error(`[Extensions] Extension ${info.id} initialization failed:`, e)
throw e
}
}
/**
* Unload a specific extension
*/
async unloadExtension(extensionId: string): Promise<void> {
const loaded = this.extensions.get(extensionId)
if (!loaded) {
throw new Error(`Extension ${extensionId} not found`)
}
console.log(`[Extensions] Unloading extension: ${extensionId}`)
try {
// Call shutdown if available
if (loaded.instance.shutdown) {
await loaded.instance.shutdown()
}
loaded.status = 'stopped'
} catch (e) {
console.error(`[Extensions] Error during ${extensionId} shutdown:`, e)
}
// Close database
if (loaded.database instanceof ExtensionDatabaseImpl) {
loaded.database.close()
}
// Remove registered methods
for (const [name, method] of this.methodRegistry.entries()) {
if (method.extensionId === extensionId) {
this.methodRegistry.delete(name)
}
}
// Remove from maps
this.extensions.delete(extensionId)
this.contexts.delete(extensionId)
}
/**
* Shutdown all extensions
*/
async shutdown(): Promise<void> {
console.log('[Extensions] Shutting down all extensions...')
for (const extensionId of this.extensions.keys()) {
try {
await this.unloadExtension(extensionId)
} catch (e) {
console.error(`[Extensions] Error unloading ${extensionId}:`, e)
}
}
console.log('[Extensions] All extensions shut down')
}
/**
* Get a loaded extension
*/
getExtension(extensionId: string): LoadedExtension | undefined {
return this.extensions.get(extensionId)
}
/**
* Get all loaded extensions
*/
getAllExtensions(): LoadedExtension[] {
return Array.from(this.extensions.values())
}
/**
* Check if an extension is loaded and ready
*/
isReady(extensionId: string): boolean {
const ext = this.extensions.get(extensionId)
return ext?.status === 'ready'
}
/**
* Get all registered RPC methods
*/
getRegisteredMethods(): Map<string, RegisteredMethod> {
return this.methodRegistry
}
/**
* Call an extension RPC method
*/
async callMethod(
methodName: string,
request: any,
applicationId: string,
userPubkey?: string
): Promise<any> {
const method = this.methodRegistry.get(methodName)
if (!method) {
throw new Error(`Unknown method: ${methodName}`)
}
const ext = this.extensions.get(method.extensionId)
if (!ext || ext.status !== 'ready') {
throw new Error(`Extension ${method.extensionId} not ready`)
}
return method.handler(request, applicationId, userPubkey)
}
/**
* Check if a method exists
*/
hasMethod(methodName: string): boolean {
return this.methodRegistry.has(methodName)
}
/**
* Dispatch payment received event to all extensions
*/
async dispatchPaymentReceived(payment: PaymentReceivedData): Promise<void> {
for (const context of this.contexts.values()) {
try {
await context.dispatchPaymentReceived(payment)
} catch (e) {
console.error('[Extensions] Error dispatching payment:', e)
}
}
}
/**
* Dispatch Nostr event to all extensions
*/
async dispatchNostrEvent(event: NostrEvent, applicationId: string): Promise<void> {
for (const context of this.contexts.values()) {
try {
await context.dispatchNostrEvent(event, applicationId)
} catch (e) {
console.error('[Extensions] Error dispatching Nostr event:', e)
}
}
}
/**
* Run health checks on all extensions
*/
async healthCheck(): Promise<Map<string, boolean>> {
const results = new Map<string, boolean>()
for (const [id, ext] of this.extensions.entries()) {
if (ext.status !== 'ready') {
results.set(id, false)
continue
}
try {
if (ext.instance.healthCheck) {
results.set(id, await ext.instance.healthCheck())
} else {
results.set(id, true)
}
} catch (e) {
results.set(id, false)
}
}
return results
}
/**
* Get extension status summary
*/
getStatus(): {
total: number
ready: number
error: number
extensions: Array<{ id: string; name: string; version: string; status: string }>
} {
const extensions = this.getAllExtensions().map(ext => ({
id: ext.info.id,
name: ext.info.name,
version: ext.info.version,
status: ext.status
}))
return {
total: extensions.length,
ready: extensions.filter(e => e.status === 'ready').length,
error: extensions.filter(e => e.status === 'error').length,
extensions
}
}
}
/**
* Create an extension loader instance
*/
export function createExtensionLoader(
config: ExtensionLoaderConfig,
mainHandler: MainHandlerInterface
): ExtensionLoader {
return new ExtensionLoader(config, mainHandler)
}

View file

@ -0,0 +1,300 @@
/**
* NIP-05 Extension for Lightning.Pub
*
* Implements Nostr NIP-05: Mapping Nostr keys to DNS-based internet identifiers
* Allows users to claim human-readable addresses like alice@domain.com
*
* Features:
* - Username claiming and management
* - .well-known/nostr.json endpoint
* - Optional relay hints
* - Admin controls for identity management
*/
import {
Extension,
ExtensionInfo,
ExtensionContext,
ExtensionDatabase,
HttpRoute,
HttpRequest,
HttpResponse
} from '../types.js'
import { runMigrations } from './migrations.js'
import { Nip05Manager } from './managers/nip05Manager.js'
import {
ClaimUsernameRequest,
UpdateRelaysRequest,
Nip05Config
} from './types.js'
/**
* NIP-05 Extension
*/
export default class Nip05Extension implements Extension {
readonly info: ExtensionInfo = {
id: 'nip05',
name: 'NIP-05 Identity',
version: '1.0.0',
description: 'Human-readable Nostr identities (username@domain)',
author: 'Lightning.Pub',
minPubVersion: '1.0.0'
}
private manager!: Nip05Manager
private ctx!: ExtensionContext
private config: Nip05Config = {}
/**
* Initialize the extension
*/
async initialize(ctx: ExtensionContext, db: ExtensionDatabase): Promise<void> {
this.ctx = ctx
// Run migrations
await runMigrations(db)
// Initialize manager
this.manager = new Nip05Manager(ctx, db, this.config)
// Register RPC methods
this.registerRpcMethods(ctx)
ctx.log('info', 'Extension initialized')
}
/**
* Shutdown the extension
*/
async shutdown(): Promise<void> {
// Cleanup if needed
}
/**
* Configure the extension
*/
configure(config: Nip05Config): void {
this.config = config
}
/**
* Get HTTP routes for this extension
* These need to be mounted by the main HTTP server
*/
getHttpRoutes(): HttpRoute[] {
return [
// NIP-05 well-known endpoint
{
method: 'GET',
path: '/.well-known/nostr.json',
handler: this.handleNostrJson.bind(this)
},
// Alternative path for proxied setups
{
method: 'GET',
path: '/api/v1/nip05/nostr.json',
handler: this.handleNostrJson.bind(this)
},
// Lightning Address endpoint (LUD-16)
// Makes NIP-05 usernames work as Lightning Addresses for zaps
{
method: 'GET',
path: '/.well-known/lnurlp/:username',
handler: this.handleLnurlPay.bind(this)
}
]
}
/**
* Register RPC methods with the extension context
*/
private registerRpcMethods(ctx: ExtensionContext): void {
// Claim a username
ctx.registerMethod('nip05.claim', async (req, appId, userId, pubkey) => {
if (!userId || !pubkey) {
throw new Error('Authentication required')
}
return this.manager.claimUsername(userId, pubkey, appId, req as ClaimUsernameRequest)
})
// Release your username
ctx.registerMethod('nip05.release', async (req, appId, userId) => {
if (!userId) {
throw new Error('Authentication required')
}
await this.manager.releaseUsername(userId, appId)
return { success: true }
})
// Update your relays
ctx.registerMethod('nip05.updateRelays', async (req, appId, userId) => {
if (!userId) {
throw new Error('Authentication required')
}
const identity = await this.manager.updateRelays(userId, appId, req as UpdateRelaysRequest)
return { identity }
})
// Get your identity
ctx.registerMethod('nip05.getMyIdentity', async (req, appId, userId) => {
if (!userId) {
throw new Error('Authentication required')
}
return this.manager.getMyIdentity(userId, appId)
})
// Look up a username (public)
ctx.registerMethod('nip05.lookup', async (req, appId) => {
return this.manager.lookupUsername(appId, req.username)
})
// Look up by pubkey (public)
ctx.registerMethod('nip05.lookupByPubkey', async (req, appId) => {
return this.manager.lookupByPubkey(appId, req.pubkey)
})
// List all identities (admin)
ctx.registerMethod('nip05.listIdentities', async (req, appId) => {
return this.manager.listIdentities(appId, {
limit: req.limit,
offset: req.offset,
activeOnly: req.active_only
})
})
// Deactivate an identity (admin)
ctx.registerMethod('nip05.deactivate', async (req, appId) => {
await this.manager.deactivateIdentity(appId, req.identity_id)
return { success: true }
})
// Reactivate an identity (admin)
ctx.registerMethod('nip05.reactivate', async (req, appId) => {
await this.manager.reactivateIdentity(appId, req.identity_id)
return { success: true }
})
}
// =========================================================================
// HTTP Route Handlers
// =========================================================================
/**
* Handle /.well-known/nostr.json request
* GET /.well-known/nostr.json?name=<username>
*
* Per NIP-05 spec, returns:
* {
* "names": { "<username>": "<pubkey hex>" },
* "relays": { "<pubkey hex>": ["wss://..."] }
* }
*/
private async handleNostrJson(req: HttpRequest): Promise<HttpResponse> {
try {
// Get application ID from request context
// In a multi-tenant setup, this would come from the host or path
const appId = req.headers['x-application-id'] || 'default'
// Set domain from request host for NIP-05 address formatting
if (req.headers['host']) {
this.manager.setDomain(req.headers['host'].split(':')[0])
}
// Get the name parameter
const name = req.query.name
// Get the JSON response
const response = await this.manager.handleNostrJson(appId, name)
return {
status: 200,
body: response,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Cache-Control': 'max-age=300' // Cache for 5 minutes
}
}
} catch (error) {
this.ctx.log('error', `Error handling nostr.json: ${error}`)
return {
status: 500,
body: { error: 'Internal server error' },
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
}
}
}
/**
* Handle /.well-known/lnurlp/:username request (Lightning Address / LUD-16)
*
* This enables NIP-05 usernames to work as Lightning Addresses for receiving
* payments and zaps. When someone sends to alice@domain.com:
* 1. Wallet resolves /.well-known/lnurlp/alice
* 2. We look up alice -> pubkey in our NIP-05 database
* 3. We return LNURL-pay info from Lightning.Pub for that user
*/
private async handleLnurlPay(req: HttpRequest): Promise<HttpResponse> {
try {
const { username } = req.params
const appId = req.headers['x-application-id'] || 'default'
if (!username) {
return {
status: 400,
body: { status: 'ERROR', reason: 'Username required' },
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
}
}
// Look up the username in our NIP-05 database
const lookup = await this.manager.lookupUsername(appId, username)
if (!lookup.found || !lookup.identity) {
return {
status: 404,
body: { status: 'ERROR', reason: 'User not found' },
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
}
}
// Get LNURL-pay info from Lightning.Pub for this user's pubkey
const lnurlPayInfo = await this.ctx.getLnurlPayInfo(lookup.identity.pubkey_hex, {
description: `Pay to ${username}`
})
return {
status: 200,
body: lnurlPayInfo,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Cache-Control': 'max-age=60' // Cache for 1 minute
}
}
} catch (error) {
this.ctx.log('error', `Error handling lnurlp: ${error}`)
return {
status: 500,
body: { status: 'ERROR', reason: 'Internal server error' },
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
}
}
}
}
// Export types for external use
export * from './types.js'
export { Nip05Manager } from './managers/nip05Manager.js'

View file

@ -0,0 +1,452 @@
/**
* NIP-05 Identity Manager
*
* Handles username claiming, lookup, and .well-known/nostr.json responses
*/
import { ExtensionContext, ExtensionDatabase } from '../../types.js'
import {
Nip05Identity,
Nip05IdentityRow,
Nip05JsonResponse,
Nip05Config,
UsernameValidation,
ClaimUsernameRequest,
ClaimUsernameResponse,
UpdateRelaysRequest,
LookupUsernameResponse,
GetMyIdentityResponse
} from '../types.js'
import crypto from 'crypto'
/**
* Default configuration
*/
const DEFAULT_CONFIG: Required<Nip05Config> = {
max_username_length: 30,
min_username_length: 1,
reserved_usernames: ['admin', 'root', 'system', 'support', 'help', 'info', 'contact', 'abuse', 'postmaster', 'webmaster', 'hostmaster', 'noreply', 'no-reply', 'null', 'undefined', 'api', 'www', 'mail', 'ftp', 'ssh', 'test', 'demo'],
include_relays: true,
default_relays: []
}
/**
* Convert database row to Nip05Identity
*/
function rowToIdentity(row: Nip05IdentityRow): Nip05Identity {
return {
id: row.id,
application_id: row.application_id,
user_id: row.user_id,
username: row.username,
pubkey_hex: row.pubkey_hex,
relays: JSON.parse(row.relays_json),
is_active: row.is_active === 1,
created_at: row.created_at,
updated_at: row.updated_at
}
}
/**
* Generate a unique ID
*/
function generateId(): string {
return crypto.randomBytes(16).toString('hex')
}
/**
* Validate username format
* - Lowercase alphanumeric and underscore only
* - Must start with a letter
* - Length within bounds
*/
function validateUsername(username: string, config: Required<Nip05Config>): UsernameValidation {
if (!username) {
return { valid: false, error: 'Username is required' }
}
const normalized = username.toLowerCase().trim()
if (normalized.length < config.min_username_length) {
return { valid: false, error: `Username must be at least ${config.min_username_length} character(s)` }
}
if (normalized.length > config.max_username_length) {
return { valid: false, error: `Username must be at most ${config.max_username_length} characters` }
}
// Only lowercase letters, numbers, and underscores
if (!/^[a-z][a-z0-9_]*$/.test(normalized)) {
return { valid: false, error: 'Username must start with a letter and contain only lowercase letters, numbers, and underscores' }
}
// Check reserved usernames
if (config.reserved_usernames.includes(normalized)) {
return { valid: false, error: 'This username is reserved' }
}
return { valid: true }
}
/**
* Validate relay URLs
*/
function validateRelays(relays: string[]): UsernameValidation {
if (!Array.isArray(relays)) {
return { valid: false, error: 'Relays must be an array' }
}
for (const relay of relays) {
if (typeof relay !== 'string') {
return { valid: false, error: 'Each relay must be a string' }
}
if (!relay.startsWith('wss://') && !relay.startsWith('ws://')) {
return { valid: false, error: `Invalid relay URL: ${relay}` }
}
}
return { valid: true }
}
export class Nip05Manager {
private ctx: ExtensionContext
private db: ExtensionDatabase
private config: Required<Nip05Config>
private domain: string
constructor(ctx: ExtensionContext, db: ExtensionDatabase, config?: Nip05Config) {
this.ctx = ctx
this.db = db
this.config = { ...DEFAULT_CONFIG, ...config }
// Extract domain from the service URL
this.domain = this.extractDomain()
}
/**
* Extract domain from service URL for NIP-05 addresses
*/
private extractDomain(): string {
// This would come from Lightning.Pub's configuration
// For now, we'll derive it when needed from the request host
return 'localhost'
}
/**
* Set the domain (called from HTTP request context)
*/
setDomain(domain: string): void {
this.domain = domain
}
/**
* Claim a username for the current user
*/
async claimUsername(
userId: string,
pubkeyHex: string,
applicationId: string,
request: ClaimUsernameRequest
): Promise<ClaimUsernameResponse> {
const normalizedUsername = request.username.toLowerCase().trim()
// Validate username format
const validation = validateUsername(normalizedUsername, this.config)
if (!validation.valid) {
throw new Error(validation.error)
}
// Validate relays if provided
const relays = request.relays || this.config.default_relays
if (relays.length > 0) {
const relayValidation = validateRelays(relays)
if (!relayValidation.valid) {
throw new Error(relayValidation.error)
}
}
// Check if user already has an identity in this application
const existingByUser = await this.db.query<Nip05IdentityRow>(
`SELECT * FROM identities WHERE application_id = ? AND user_id = ?`,
[applicationId, userId]
)
if (existingByUser.length > 0) {
throw new Error('You already have a username. Release it first to claim a new one.')
}
// Check if username is already taken
const existingByUsername = await this.db.query<Nip05IdentityRow>(
`SELECT * FROM identities WHERE application_id = ? AND username = ?`,
[applicationId, normalizedUsername]
)
if (existingByUsername.length > 0) {
throw new Error('This username is already taken')
}
// Create the identity
const now = Math.floor(Date.now() / 1000)
const id = generateId()
await this.db.execute(
`INSERT INTO identities (id, application_id, user_id, username, pubkey_hex, relays_json, is_active, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?)`,
[id, applicationId, userId, normalizedUsername, pubkeyHex, JSON.stringify(relays), now, now]
)
const identity: Nip05Identity = {
id,
application_id: applicationId,
user_id: userId,
username: normalizedUsername,
pubkey_hex: pubkeyHex,
relays,
is_active: true,
created_at: now,
updated_at: now
}
return {
identity,
nip05_address: `${normalizedUsername}@${this.domain}`
}
}
/**
* Release (delete) the current user's username
*/
async releaseUsername(userId: string, applicationId: string): Promise<void> {
const result = await this.db.execute(
`DELETE FROM identities WHERE application_id = ? AND user_id = ?`,
[applicationId, userId]
)
if (result.changes === 0) {
throw new Error('You do not have a username to release')
}
}
/**
* Update relays for the current user's identity
*/
async updateRelays(
userId: string,
applicationId: string,
request: UpdateRelaysRequest
): Promise<Nip05Identity> {
// Validate relays
const validation = validateRelays(request.relays)
if (!validation.valid) {
throw new Error(validation.error)
}
const now = Math.floor(Date.now() / 1000)
const result = await this.db.execute(
`UPDATE identities SET relays_json = ?, updated_at = ? WHERE application_id = ? AND user_id = ?`,
[JSON.stringify(request.relays), now, applicationId, userId]
)
if (result.changes === 0) {
throw new Error('You do not have a username')
}
// Fetch and return the updated identity
const rows = await this.db.query<Nip05IdentityRow>(
`SELECT * FROM identities WHERE application_id = ? AND user_id = ?`,
[applicationId, userId]
)
return rowToIdentity(rows[0])
}
/**
* Get the current user's identity
*/
async getMyIdentity(userId: string, applicationId: string): Promise<GetMyIdentityResponse> {
const rows = await this.db.query<Nip05IdentityRow>(
`SELECT * FROM identities WHERE application_id = ? AND user_id = ?`,
[applicationId, userId]
)
if (rows.length === 0) {
return { has_identity: false }
}
const identity = rowToIdentity(rows[0])
return {
has_identity: true,
identity,
nip05_address: `${identity.username}@${this.domain}`
}
}
/**
* Look up a username (public, no auth required)
*/
async lookupUsername(applicationId: string, username: string): Promise<LookupUsernameResponse> {
const normalizedUsername = username.toLowerCase().trim()
const rows = await this.db.query<Nip05IdentityRow>(
`SELECT * FROM identities WHERE application_id = ? AND username = ? AND is_active = 1`,
[applicationId, normalizedUsername]
)
if (rows.length === 0) {
return { found: false }
}
const identity = rowToIdentity(rows[0])
return {
found: true,
identity,
nip05_address: `${identity.username}@${this.domain}`
}
}
/**
* Look up by pubkey
*/
async lookupByPubkey(applicationId: string, pubkeyHex: string): Promise<LookupUsernameResponse> {
const rows = await this.db.query<Nip05IdentityRow>(
`SELECT * FROM identities WHERE application_id = ? AND pubkey_hex = ? AND is_active = 1`,
[applicationId, pubkeyHex]
)
if (rows.length === 0) {
return { found: false }
}
const identity = rowToIdentity(rows[0])
return {
found: true,
identity,
nip05_address: `${identity.username}@${this.domain}`
}
}
/**
* Handle /.well-known/nostr.json request
* This is the core NIP-05 endpoint
*/
async handleNostrJson(applicationId: string, name?: string): Promise<Nip05JsonResponse> {
const response: Nip05JsonResponse = {
names: {}
}
if (this.config.include_relays) {
response.relays = {}
}
if (name) {
// Look up specific username
const normalizedName = name.toLowerCase().trim()
const rows = await this.db.query<Nip05IdentityRow>(
`SELECT * FROM identities WHERE application_id = ? AND username = ? AND is_active = 1`,
[applicationId, normalizedName]
)
if (rows.length > 0) {
const identity = rowToIdentity(rows[0])
response.names[identity.username] = identity.pubkey_hex
if (this.config.include_relays && identity.relays.length > 0) {
response.relays![identity.pubkey_hex] = identity.relays
}
}
} else {
// Return all active identities (with reasonable limit)
const rows = await this.db.query<Nip05IdentityRow>(
`SELECT * FROM identities WHERE application_id = ? AND is_active = 1 LIMIT 1000`,
[applicationId]
)
for (const row of rows) {
const identity = rowToIdentity(row)
response.names[identity.username] = identity.pubkey_hex
if (this.config.include_relays && identity.relays.length > 0) {
response.relays![identity.pubkey_hex] = identity.relays
}
}
}
return response
}
/**
* List all identities for an application (admin)
*/
async listIdentities(
applicationId: string,
options?: { limit?: number; offset?: number; activeOnly?: boolean }
): Promise<{ identities: Nip05Identity[]; total: number }> {
const limit = options?.limit || 50
const offset = options?.offset || 0
const activeClause = options?.activeOnly !== false ? 'AND is_active = 1' : ''
// Get total count
const countResult = await this.db.query<{ count: number }>(
`SELECT COUNT(*) as count FROM identities WHERE application_id = ? ${activeClause}`,
[applicationId]
)
const total = countResult[0]?.count || 0
// Get page of results
const rows = await this.db.query<Nip05IdentityRow>(
`SELECT * FROM identities WHERE application_id = ? ${activeClause}
ORDER BY created_at DESC LIMIT ? OFFSET ?`,
[applicationId, limit, offset]
)
return {
identities: rows.map(rowToIdentity),
total
}
}
/**
* Deactivate an identity (admin)
*/
async deactivateIdentity(applicationId: string, identityId: string): Promise<void> {
const now = Math.floor(Date.now() / 1000)
const result = await this.db.execute(
`UPDATE identities SET is_active = 0, updated_at = ? WHERE application_id = ? AND id = ?`,
[now, applicationId, identityId]
)
if (result.changes === 0) {
throw new Error('Identity not found')
}
}
/**
* Reactivate an identity (admin)
*/
async reactivateIdentity(applicationId: string, identityId: string): Promise<void> {
const now = Math.floor(Date.now() / 1000)
// Check if username is taken by an active identity
const identity = await this.db.query<Nip05IdentityRow>(
`SELECT * FROM identities WHERE id = ? AND application_id = ?`,
[identityId, applicationId]
)
if (identity.length === 0) {
throw new Error('Identity not found')
}
const conflicting = await this.db.query<Nip05IdentityRow>(
`SELECT * FROM identities WHERE application_id = ? AND username = ? AND is_active = 1 AND id != ?`,
[applicationId, identity[0].username, identityId]
)
if (conflicting.length > 0) {
throw new Error('Username is already taken by another active identity')
}
await this.db.execute(
`UPDATE identities SET is_active = 1, updated_at = ? WHERE application_id = ? AND id = ?`,
[now, applicationId, identityId]
)
}
}

View file

@ -0,0 +1,93 @@
/**
* NIP-05 Extension Database Migrations
*/
import { ExtensionDatabase } from '../types.js'
export interface Migration {
version: number
name: string
up: (db: ExtensionDatabase) => Promise<void>
down?: (db: ExtensionDatabase) => Promise<void>
}
export const migrations: Migration[] = [
{
version: 1,
name: 'create_identities_table',
up: async (db: ExtensionDatabase) => {
await db.execute(`
CREATE TABLE IF NOT EXISTS identities (
id TEXT PRIMARY KEY,
application_id TEXT NOT NULL,
user_id TEXT NOT NULL,
-- Identity mapping
username TEXT NOT NULL,
pubkey_hex TEXT NOT NULL,
-- Optional relays (JSON array)
relays_json TEXT NOT NULL DEFAULT '[]',
-- Status
is_active INTEGER NOT NULL DEFAULT 1,
-- Timestamps
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)
`)
// Unique username per application (case-insensitive via lowercase storage)
await db.execute(`
CREATE UNIQUE INDEX IF NOT EXISTS idx_identities_username_app
ON identities(application_id, username)
`)
// One identity per user per application
await db.execute(`
CREATE UNIQUE INDEX IF NOT EXISTS idx_identities_user_app
ON identities(application_id, user_id)
`)
// Look up by pubkey
await db.execute(`
CREATE INDEX IF NOT EXISTS idx_identities_pubkey
ON identities(pubkey_hex)
`)
// Look up active identities for .well-known endpoint
await db.execute(`
CREATE INDEX IF NOT EXISTS idx_identities_active
ON identities(application_id, is_active, username)
`)
}
}
]
/**
* Run all pending migrations
*/
export async function runMigrations(db: ExtensionDatabase): Promise<void> {
// Get current version
const versionResult = await db.query<{ value: string }>(
`SELECT value FROM _extension_meta WHERE key = 'migration_version'`
).catch(() => [])
const currentVersion = versionResult.length > 0 ? parseInt(versionResult[0].value, 10) : 0
// Run pending migrations
for (const migration of migrations) {
if (migration.version > currentVersion) {
console.log(`[NIP-05] Running migration ${migration.version}: ${migration.name}`)
await migration.up(db)
// Update version
await db.execute(
`INSERT INTO _extension_meta (key, value) VALUES ('migration_version', ?)
ON CONFLICT(key) DO UPDATE SET value = excluded.value`,
[String(migration.version)]
)
}
}
}

View file

@ -0,0 +1,130 @@
/**
* NIP-05 Extension Types
*
* Implements Nostr NIP-05: Mapping Nostr keys to DNS-based internet identifiers
* Allows users to have human-readable addresses like alice@domain.com
*/
/**
* A NIP-05 identity mapping a username to a Nostr public key
*/
export interface Nip05Identity {
id: string
application_id: string
user_id: string
/** The human-readable username (lowercase, alphanumeric + underscore) */
username: string
/** The Nostr public key in hex format */
pubkey_hex: string
/** Optional list of relay URLs for this user */
relays: string[]
/** Whether this identity is active */
is_active: boolean
created_at: number
updated_at: number
}
/**
* NIP-05 JSON response format per the spec
* GET /.well-known/nostr.json?name=<username>
*/
export interface Nip05JsonResponse {
names: Record<string, string>
relays?: Record<string, string[]>
}
/**
* Request to claim a username
*/
export interface ClaimUsernameRequest {
username: string
relays?: string[]
}
/**
* Response after claiming a username
*/
export interface ClaimUsernameResponse {
identity: Nip05Identity
nip05_address: string
}
/**
* Request to update relays for a username
*/
export interface UpdateRelaysRequest {
relays: string[]
}
/**
* Request to look up a username
*/
export interface LookupUsernameRequest {
username: string
}
/**
* Response for username lookup
*/
export interface LookupUsernameResponse {
found: boolean
identity?: Nip05Identity
nip05_address?: string
}
/**
* Response for getting current user's identity
*/
export interface GetMyIdentityResponse {
has_identity: boolean
identity?: Nip05Identity
nip05_address?: string
}
/**
* Database row for NIP-05 identity
*/
export interface Nip05IdentityRow {
id: string
application_id: string
user_id: string
username: string
pubkey_hex: string
relays_json: string
is_active: number
created_at: number
updated_at: number
}
/**
* Extension configuration
*/
export interface Nip05Config {
/** Maximum username length (default: 30) */
max_username_length?: number
/** Minimum username length (default: 1) */
min_username_length?: number
/** Reserved usernames that cannot be claimed */
reserved_usernames?: string[]
/** Whether to include relays in the JSON response (default: true) */
include_relays?: boolean
/** Default relays to suggest for new users */
default_relays?: string[]
}
/**
* Validation result for username
*/
export interface UsernameValidation {
valid: boolean
error?: string
}

254
src/extensions/types.ts Normal file
View file

@ -0,0 +1,254 @@
/**
* Extension System Core Types
*
* These types define the contract between Lightning.Pub and extensions.
*/
/**
* Extension metadata
*/
export interface ExtensionInfo {
id: string // Unique identifier (lowercase, no spaces)
name: string // Display name
version: string // Semver version
description: string // Short description
author: string // Author name or organization
minPubVersion?: string // Minimum Lightning.Pub version required
dependencies?: string[] // Other extension IDs this depends on
}
/**
* Extension database interface
* Provides isolated database access for each extension
*/
export interface ExtensionDatabase {
/**
* Execute a write query (INSERT, UPDATE, DELETE, CREATE, etc.)
*/
execute(sql: string, params?: any[]): Promise<{ changes?: number; lastId?: number }>
/**
* Execute a read query (SELECT)
*/
query<T = any>(sql: string, params?: any[]): Promise<T[]>
/**
* Execute multiple statements in a transaction
*/
transaction<T>(fn: () => Promise<T>): Promise<T>
}
/**
* Application info provided to extensions
*/
export interface ApplicationInfo {
id: string
name: string
nostr_public: string // Application's Nostr pubkey (hex)
balance_sats: number
}
/**
* Invoice creation options
*/
export interface CreateInvoiceOptions {
memo?: string
expiry?: number // Seconds until expiry
metadata?: Record<string, any> // Custom metadata for callbacks
}
/**
* Created invoice result
*/
export interface CreatedInvoice {
id: string // Internal invoice ID
paymentRequest: string // BOLT11 invoice string
paymentHash: string // Payment hash (hex)
expiry: number // Expiry timestamp
}
/**
* Payment received callback data
*/
export interface PaymentReceivedData {
invoiceId: string
paymentHash: string
amountSats: number
metadata?: Record<string, any>
}
/**
* LNURL-pay info response (LUD-06/LUD-16)
* Used for Lightning Address and zap support
*/
export interface LnurlPayInfo {
tag: 'payRequest'
callback: string // URL to call with amount
minSendable: number // Minimum msats
maxSendable: number // Maximum msats
metadata: string // JSON-encoded metadata array
allowsNostr?: boolean // Whether zaps are supported
nostrPubkey?: string // Pubkey for zap receipts (hex)
}
/**
* Nostr event structure (minimal)
*/
export interface NostrEvent {
id: string
pubkey: string
created_at: number
kind: number
tags: string[][]
content: string
sig?: string
}
/**
* Unsigned Nostr event for publishing
*/
export interface UnsignedNostrEvent {
kind: number
pubkey: string
created_at: number
tags: string[][]
content: string
}
/**
* RPC method handler function
*/
export type RpcMethodHandler = (
request: any,
applicationId: string,
userPubkey?: string
) => Promise<any>
/**
* Extension context - interface provided to extensions for interacting with Lightning.Pub
*/
export interface ExtensionContext {
/**
* Get information about an application
*/
getApplication(applicationId: string): Promise<ApplicationInfo | null>
/**
* Create a Lightning invoice
*/
createInvoice(amountSats: number, options?: CreateInvoiceOptions): Promise<CreatedInvoice>
/**
* Pay a Lightning invoice (requires sufficient balance)
*/
payInvoice(applicationId: string, paymentRequest: string, maxFeeSats?: number): Promise<{
paymentHash: string
feeSats: number
}>
/**
* Send an encrypted DM via Nostr (NIP-44)
*/
sendEncryptedDM(applicationId: string, recipientPubkey: string, content: string): Promise<string>
/**
* Publish a Nostr event (signed by application's key)
*/
publishNostrEvent(event: UnsignedNostrEvent): Promise<string | null>
/**
* Get LNURL-pay info for a user (by pubkey)
* Used to enable Lightning Address support (LUD-16) and zaps (NIP-57)
*/
getLnurlPayInfo(pubkeyHex: string, options?: {
metadata?: string // Custom metadata JSON
description?: string // Human-readable description
}): Promise<LnurlPayInfo>
/**
* Subscribe to payment received callbacks
*/
onPaymentReceived(callback: (payment: PaymentReceivedData) => Promise<void>): void
/**
* Subscribe to incoming Nostr events for the application
*/
onNostrEvent(callback: (event: NostrEvent, applicationId: string) => Promise<void>): void
/**
* Register an RPC method
*/
registerMethod(name: string, handler: RpcMethodHandler): void
/**
* Get the extension's isolated database
*/
getDatabase(): ExtensionDatabase
/**
* Log a message (prefixed with extension ID)
*/
log(level: 'debug' | 'info' | 'warn' | 'error', message: string, ...args: any[]): void
}
/**
* Extension interface - what extensions must implement
*/
export interface Extension {
/**
* Extension metadata
*/
readonly info: ExtensionInfo
/**
* Initialize the extension
* Called once when the extension is loaded
*/
initialize(ctx: ExtensionContext, db: ExtensionDatabase): Promise<void>
/**
* Shutdown the extension
* Called when Lightning.Pub is shutting down
*/
shutdown?(): Promise<void>
/**
* Health check
* Return true if extension is healthy
*/
healthCheck?(): Promise<boolean>
}
/**
* Extension constructor type
*/
export type ExtensionConstructor = new () => Extension
/**
* Extension module default export
*/
export interface ExtensionModule {
default: ExtensionConstructor
}
/**
* Loaded extension state
*/
export interface LoadedExtension {
info: ExtensionInfo
instance: Extension
database: ExtensionDatabase
status: 'loading' | 'ready' | 'error' | 'stopped'
error?: Error
loadedAt: number
}
/**
* Extension loader configuration
*/
export interface ExtensionLoaderConfig {
extensionsDir: string // Directory containing extensions
databaseDir: string // Directory for extension databases
enabledExtensions?: string[] // If set, only load these extensions
disabledExtensions?: string[] // Extensions to skip
}

View file

@ -105,7 +105,7 @@ export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSett
return { return {
Stop: () => { mainHandler.adminManager.setNostrConnected(false); return nostr.Stop }, Stop: () => { mainHandler.adminManager.setNostrConnected(false); return nostr.Stop },
Send: (...args) => nostr.Send(...args), Send: async (...args) => nostr.Send(...args),
Ping: () => nostr.Ping(), Ping: () => nostr.Ping(),
Reset: (settings: NostrSettings) => nostr.Reset(settings) Reset: (settings: NostrSettings) => nostr.Reset(settings)
} }

View file

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

View file

@ -15,7 +15,7 @@ import { AddInvoiceReq } from './addInvoiceReq.js';
import { PayInvoiceReq } from './payInvoiceReq.js'; import { PayInvoiceReq } from './payInvoiceReq.js';
import { SendCoinsReq } from './sendCoinsReq.js'; import { SendCoinsReq } from './sendCoinsReq.js';
import { AddressPaidCb, InvoicePaidCb, NodeInfo, Invoice, DecodedInvoice, PaidInvoice, NewBlockCb, HtlcCb, BalanceInfo, ChannelEventCb } from './settings.js'; import { AddressPaidCb, InvoicePaidCb, NodeInfo, Invoice, DecodedInvoice, PaidInvoice, NewBlockCb, HtlcCb, BalanceInfo, ChannelEventCb } from './settings.js';
import { ERROR, getLogger } from '../helpers/logger.js'; import { ERROR, getLogger, DEBUG, INFO } from '../helpers/logger.js';
import { HtlcEvent_EventType } from '../../../proto/lnd/router.js'; import { HtlcEvent_EventType } from '../../../proto/lnd/router.js';
import { LiquidityProvider } from '../main/liquidityProvider.js'; import { LiquidityProvider } from '../main/liquidityProvider.js';
import { Utils } from '../helpers/utilsWrapper.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 { WalletKitClient } from '../../../proto/lnd/walletkit.client.js';
import SettingsManager from '../main/settingsManager.js'; import SettingsManager from '../main/settingsManager.js';
import { LndNodeSettings, LndSettings } from '../main/settings.js'; import { LndNodeSettings, LndSettings } from '../main/settings.js';
import { ListAddressesResponse } from '../../../proto/lnd/walletkit.js'; import { ListAddressesResponse, PublishResponse } from '../../../proto/lnd/walletkit.js';
const DeadLineMetadata = (deadline = 10 * 1000) => ({ deadline: Date.now() + deadline }) const DeadLineMetadata = (deadline = 10 * 1000) => ({ deadline: Date.now() + deadline })
const deadLndRetrySeconds = 20 const deadLndRetrySeconds = 20
@ -69,7 +69,7 @@ export default class {
// Skip LND client initialization if using only liquidity provider // Skip LND client initialization if using only liquidity provider
if (liquidProvider.getSettings().useOnlyLiquidityProvider) { if (liquidProvider.getSettings().useOnlyLiquidityProvider) {
this.log("USE_ONLY_LIQUIDITY_PROVIDER enabled, skipping LND client initialization") this.log(INFO, "USE_ONLY_LIQUIDITY_PROVIDER enabled, skipping LND client initialization")
// Create minimal dummy clients - they won't be used but prevent null reference errors // Create minimal dummy clients - they won't be used but prevent null reference errors
// Use insecure credentials directly (can't combine them) // Use insecure credentials directly (can't combine them)
const { lndAddr } = this.getSettings().lndNodeSettings const { lndAddr } = this.getSettings().lndNodeSettings
@ -126,13 +126,13 @@ export default class {
} }
async Warmup() { async Warmup() {
this.log(INFO, "Warming up LND")
// Skip LND warmup if using only liquidity provider // Skip LND warmup if using only liquidity provider
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { 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 this.ready = true
return return
} }
// console.log("Warming up LND")
this.SubscribeAddressPaid() this.SubscribeAddressPaid()
this.SubscribeInvoicePaid() this.SubscribeInvoicePaid()
await this.SubscribeNewBlock() await this.SubscribeNewBlock()
@ -147,7 +147,7 @@ export default class {
this.ready = true this.ready = true
res() res()
} catch (err) { } 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) { if (Date.now() - now > 1000 * 60) {
rej(new Error("LND not ready after 1 minute")) rej(new Error("LND not ready after 1 minute"))
} }
@ -156,6 +156,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> { async GetInfo(): Promise<NodeInfo> {
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
// Return dummy info when bypass is enabled // Return dummy info when bypass is enabled
@ -169,27 +176,26 @@ export default class {
uris: [] uris: []
} }
} }
// console.log("Getting info")
const res = await this.lightning.getInfo({}, DeadLineMetadata()) const res = await this.lightning.getInfo({}, DeadLineMetadata())
return res.response return res.response
} }
async ListPendingChannels(): Promise<PendingChannelsResponse> { async ListPendingChannels(): Promise<PendingChannelsResponse> {
this.log(DEBUG, "Listing pending channels")
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
return { pendingOpenChannels: [], pendingClosingChannels: [], pendingForceClosingChannels: [], waitingCloseChannels: [], totalLimboBalance: 0n } return { pendingOpenChannels: [], pendingClosingChannels: [], pendingForceClosingChannels: [], waitingCloseChannels: [], totalLimboBalance: 0n }
} }
// console.log("Listing pending channels")
const res = await this.lightning.pendingChannels({ includeRawTx: false }, DeadLineMetadata()) const res = await this.lightning.pendingChannels({ includeRawTx: false }, DeadLineMetadata())
return res.response return res.response
} }
async ListChannels(peerLookup = false): Promise<ListChannelsResponse> { async ListChannels(peerLookup = false): Promise<ListChannelsResponse> {
// console.log("Listing channels") this.log(DEBUG, "Listing channels")
const res = await this.lightning.listChannels({ const res = await this.lightning.listChannels({
activeOnly: false, inactiveOnly: false, privateOnly: false, publicOnly: false, peer: Buffer.alloc(0), peerAliasLookup: peerLookup activeOnly: false, inactiveOnly: false, privateOnly: false, publicOnly: false, peer: Buffer.alloc(0), peerAliasLookup: peerLookup
}, DeadLineMetadata()) }, DeadLineMetadata())
return res.response return res.response
} }
async ListClosedChannels(): Promise<ClosedChannelsResponse> { async ListClosedChannels(): Promise<ClosedChannelsResponse> {
// console.log("Listing closed channels") this.log(DEBUG, "Listing closed channels")
const res = await this.lightning.closedChannels({ const res = await this.lightning.closedChannels({
abandoned: true, abandoned: true,
breach: true, breach: true,
@ -206,7 +212,6 @@ export default class {
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
return return
} }
// console.log("Checking health")
if (!this.ready) { if (!this.ready) {
throw new Error("not ready") throw new Error("not ready")
} }
@ -217,69 +222,69 @@ export default class {
} }
RestartStreams() { RestartStreams() {
// console.log("Restarting streams") this.log(INFO, "Restarting streams")
if (!this.ready || this.abortController.signal.aborted) { if (!this.ready || this.abortController.signal.aborted) {
return 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 () => { const interval = setInterval(async () => {
try { try {
await this.unlockLnd() await this.unlockLnd()
this.log("LND is back online") this.log(INFO, "LND is back online")
clearInterval(interval) clearInterval(interval)
await this.Warmup() await this.Warmup()
} catch (err) { } 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) }, deadLndRetrySeconds * 1000)
} }
async SubscribeChannelEvents() { async SubscribeChannelEvents() {
// console.log("Subscribing to channel events") this.log(DEBUG, "Subscribing to channel events")
const stream = this.lightning.subscribeChannelEvents({}, { abort: this.abortController.signal }) const stream = this.lightning.subscribeChannelEvents({}, { abort: this.abortController.signal })
stream.responses.onMessage(async channel => { stream.responses.onMessage(async channel => {
const channels = await this.ListChannels() const channels = await this.ListChannels()
this.channelEventCb(channel, channels.channels) this.channelEventCb(channel, channels.channels)
}) })
stream.responses.onError(error => { stream.responses.onError(error => {
this.log("Error with subscribeChannelEvents stream") this.log(ERROR, "Error with subscribeChannelEvents stream")
}) })
stream.responses.onComplete(() => { stream.responses.onComplete(() => {
this.log("subscribeChannelEvents stream closed") this.log(INFO, "subscribeChannelEvents stream closed")
}) })
} }
async SubscribeHtlcEvents() { async SubscribeHtlcEvents() {
// console.log("Subscribing to htlc events") this.log(DEBUG, "Subscribing to htlc events")
const stream = this.router.subscribeHtlcEvents({}, { abort: this.abortController.signal }) const stream = this.router.subscribeHtlcEvents({}, { abort: this.abortController.signal })
stream.responses.onMessage(htlc => { stream.responses.onMessage(htlc => {
this.htlcCb(htlc) this.htlcCb(htlc)
}) })
stream.responses.onError(error => { stream.responses.onError(error => {
this.log("Error with subscribeHtlcEvents stream") this.log(ERROR, "Error with subscribeHtlcEvents stream")
}) })
stream.responses.onComplete(() => { stream.responses.onComplete(() => {
this.log("subscribeHtlcEvents stream closed") this.log(INFO, "subscribeHtlcEvents stream closed")
}) })
} }
async SubscribeNewBlock() { async SubscribeNewBlock() {
// console.log("Subscribing to new block") this.log(DEBUG, "Subscribing to new block")
const { blockHeight } = await this.GetInfo() const { blockHeight } = await this.GetInfo()
const stream = this.chainNotifier.registerBlockEpochNtfn({ height: blockHeight, hash: Buffer.alloc(0) }, { abort: this.abortController.signal }) const stream = this.chainNotifier.registerBlockEpochNtfn({ height: blockHeight, hash: Buffer.alloc(0) }, { abort: this.abortController.signal })
stream.responses.onMessage(block => { stream.responses.onMessage(block => {
this.newBlockCb(block.height) this.newBlockCb(block.height)
}) })
stream.responses.onError(error => { stream.responses.onError(error => {
this.log("Error with new block stream") this.log(ERROR, "Error with new block stream")
}) })
stream.responses.onComplete(() => { stream.responses.onComplete(() => {
this.log("new block stream closed") this.log(INFO, "new block stream closed")
}) })
} }
SubscribeAddressPaid(): void { SubscribeAddressPaid(): void {
// console.log("Subscribing to address paid") this.log(DEBUG, "Subscribing to address paid")
const stream = this.lightning.subscribeTransactions({ const stream = this.lightning.subscribeTransactions({
account: "", account: "",
endHeight: 0, endHeight: 0,
@ -298,15 +303,15 @@ export default class {
} }
}) })
stream.responses.onError(error => { stream.responses.onError(error => {
this.log("Error with onchain tx stream") this.log(ERROR, "Error with onchain tx stream")
}) })
stream.responses.onComplete(() => { stream.responses.onComplete(() => {
this.log("onchain tx stream closed") this.log(INFO, "onchain tx stream closed")
}) })
} }
SubscribeInvoicePaid(): void { SubscribeInvoicePaid(): void {
// console.log("Subscribing to invoice paid") this.log(DEBUG, "Subscribing to invoice paid")
const stream = this.lightning.subscribeInvoices({ const stream = this.lightning.subscribeInvoices({
settleIndex: BigInt(this.latestKnownSettleIndex), settleIndex: BigInt(this.latestKnownSettleIndex),
addIndex: 0n, addIndex: 0n,
@ -319,14 +324,14 @@ export default class {
}) })
let restarted = false let restarted = false
stream.responses.onError(error => { stream.responses.onError(error => {
this.log("Error with invoice stream") this.log(ERROR, "Error with invoice stream")
if (!restarted) { if (!restarted) {
restarted = true restarted = true
this.RestartStreams() this.RestartStreams()
} }
}) })
stream.responses.onComplete(() => { stream.responses.onComplete(() => {
this.log("invoice stream closed") this.log(INFO, "invoice stream closed")
if (!restarted) { if (!restarted) {
restarted = true restarted = true
this.RestartStreams() this.RestartStreams()
@ -348,6 +353,7 @@ export default class {
} }
async ListAddresses(): Promise<LndAddress[]> { async ListAddresses(): Promise<LndAddress[]> {
this.log(DEBUG, "Listing addresses")
const res = await this.walletKit.listAddresses({ accountName: "", showCustomAccounts: false }, DeadLineMetadata()) 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() 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 }) addresses.forEach(a => this.addressesCache[a.address] = { isChange: a.change })
@ -355,11 +361,11 @@ export default class {
} }
async NewAddress(addressType: Types.AddressType, { useProvider, from }: TxActionOptions): Promise<NewAddressResponse> { 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) // Force use of provider when bypass is enabled (addresses not supported by provider, but we should fail gracefully)
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
throw new Error("Address generation not supported when USE_ONLY_LIQUIDITY_PROVIDER is enabled") throw new Error("Address generation not supported when USE_ONLY_LIQUIDITY_PROVIDER is enabled")
} }
// console.log("Creating new address")
let lndAddressType: AddressType let lndAddressType: AddressType
switch (addressType) { switch (addressType) {
case Types.AddressType.NESTED_PUBKEY_HASH: case Types.AddressType.NESTED_PUBKEY_HASH:
@ -389,11 +395,11 @@ export default class {
} }
async NewInvoice(value: number, memo: string, expiry: number, { useProvider, from }: TxActionOptions, blind = false): Promise<Invoice> { 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 // Force use of provider when bypass is enabled
const mustUseProvider = this.liquidProvider.getSettings().useOnlyLiquidityProvider || useProvider const mustUseProvider = this.liquidProvider.getSettings().useOnlyLiquidityProvider || useProvider
if (mustUseProvider) { if (mustUseProvider) {
console.log("using provider") this.log(INFO, "using provider")
const invoice = await this.liquidProvider.AddInvoice(value, memo, from, expiry) const invoice = await this.liquidProvider.AddInvoice(value, memo, from, expiry)
const providerPubkey = this.liquidProvider.GetProviderPubkey() const providerPubkey = this.liquidProvider.GetProviderPubkey()
return { payRequest: invoice, providerPubkey } return { payRequest: invoice, providerPubkey }
@ -409,6 +415,7 @@ export default class {
} }
async DecodeInvoice(paymentRequest: string): Promise<DecodedInvoice> { async DecodeInvoice(paymentRequest: string): Promise<DecodedInvoice> {
this.log(DEBUG, "Decoding invoice")
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
// Use light-bolt11-decoder when LND is bypassed // Use light-bolt11-decoder when LND is bypassed
try { try {
@ -434,24 +441,23 @@ export default class {
throw new Error(`Failed to decode invoice: ${err.message}`) throw new Error(`Failed to decode invoice: ${err.message}`)
} }
} }
// console.log("Decoding invoice")
const res = await this.lightning.decodePayReq({ payReq: paymentRequest }, DeadLineMetadata()) const res = await this.lightning.decodePayReq({ payReq: paymentRequest }, DeadLineMetadata())
return { numSatoshis: Number(res.response.numSatoshis), paymentHash: res.response.paymentHash } return { numSatoshis: Number(res.response.numSatoshis), paymentHash: res.response.paymentHash }
} }
async ChannelBalance(): Promise<{ local: number, remote: number }> { async ChannelBalance(): Promise<{ local: number, remote: number }> {
this.log(DEBUG, "Getting channel balance")
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
return { local: 0, remote: 0 } return { local: 0, remote: 0 }
} }
// console.log("Getting channel balance")
const res = await this.lightning.channelBalance({}) const res = await this.lightning.channelBalance({})
const r = res.response const r = res.response
return { local: r.localBalance ? Number(r.localBalance.sat) : 0, remote: r.remoteBalance ? Number(r.remoteBalance.sat) : 0 } return { local: r.localBalance ? Number(r.localBalance.sat) : 0, remote: r.remoteBalance ? Number(r.remoteBalance.sat) : 0 }
} }
async PayInvoice(invoice: string, amount: number, { routingFeeLimit, serviceFee }: { routingFeeLimit: number, serviceFee: number }, decodedAmount: number, { useProvider, from }: TxActionOptions, paymentIndexCb?: (index: number) => void): Promise<PaidInvoice> { async PayInvoice(invoice: string, amount: number, { routingFeeLimit, serviceFee }: { routingFeeLimit: number, serviceFee: number }, decodedAmount: number, { useProvider, from }: TxActionOptions, paymentIndexCb?: (index: number) => void): Promise<PaidInvoice> {
// console.log("Paying invoice") this.log(DEBUG, "Paying invoice")
if (this.outgoingOpsLocked) { 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") throw new Error("lnd node is currently out of sync")
} }
// Force use of provider when bypass is enabled // Force use of provider when bypass is enabled
@ -468,7 +474,7 @@ export default class {
const stream = this.router.sendPaymentV2(req, { abort: abortController.signal }) const stream = this.router.sendPaymentV2(req, { abort: abortController.signal })
return new Promise((res, rej) => { return new Promise((res, rej) => {
stream.responses.onError(error => { stream.responses.onError(error => {
this.log("invoice payment failed", error) this.log(ERROR, "invoice payment failed", error)
rej(error) rej(error)
}) })
let indexSent = false let indexSent = false
@ -480,7 +486,7 @@ export default class {
} }
switch (payment.status) { switch (payment.status) {
case Payment_PaymentStatus.FAILED: case Payment_PaymentStatus.FAILED:
this.log("invoice payment failed", payment.failureReason) this.log(ERROR, "invoice payment failed", payment.failureReason)
rej(PaymentFailureReason[payment.failureReason]) rej(PaymentFailureReason[payment.failureReason])
return return
case Payment_PaymentStatus.SUCCEEDED: case Payment_PaymentStatus.SUCCEEDED:
@ -498,7 +504,7 @@ export default class {
} }
async EstimateChainFees(address: string, amount: number, targetConf: number): Promise<EstimateFeeResponse> { async EstimateChainFees(address: string, amount: number, targetConf: number): Promise<EstimateFeeResponse> {
// console.log("Estimating chain fees") this.log(DEBUG, "Estimating chain fees")
await this.Health() await this.Health()
const res = await this.lightning.estimateFee({ const res = await this.lightning.estimateFee({
addrToAmount: { [address]: BigInt(amount) }, addrToAmount: { [address]: BigInt(amount) },
@ -511,13 +517,13 @@ export default class {
} }
async PayAddress(address: string, amount: number, satPerVByte: number, label = "", { useProvider, from }: TxActionOptions): Promise<SendCoinsResponse> { 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 // Address payments not supported when bypass is enabled
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
throw new Error("Address payments not supported when USE_ONLY_LIQUIDITY_PROVIDER is enabled") throw new Error("Address payments not supported when USE_ONLY_LIQUIDITY_PROVIDER is enabled")
} }
// console.log("Paying address")
if (this.outgoingOpsLocked) { 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") throw new Error("lnd node is currently out of sync")
} }
if (useProvider) { if (useProvider) {
@ -535,19 +541,19 @@ export default class {
} }
async GetTransactions(startHeight: number): Promise<TransactionDetails> { async GetTransactions(startHeight: number): Promise<TransactionDetails> {
// console.log("Getting transactions") this.log(DEBUG, "Getting transactions")
const res = await this.lightning.getTransactions({ startHeight, endHeight: 0, account: "" }, DeadLineMetadata()) const res = await this.lightning.getTransactions({ startHeight, endHeight: 0, account: "", }, DeadLineMetadata())
return res.response return res.response
} }
async GetChannelInfo(chanId: string) { async GetChannelInfo(chanId: string) {
// console.log("Getting channel info") this.log(DEBUG, "Getting channel info")
const res = await this.lightning.getChanInfo({ chanId, chanPoint: "" }, DeadLineMetadata()) const res = await this.lightning.getChanInfo({ chanId, chanPoint: "" }, DeadLineMetadata())
return res.response return res.response
} }
async UpdateChannelPolicy(chanPoint: string, policy: Types.ChannelPolicy) { async UpdateChannelPolicy(chanPoint: string, policy: Types.ChannelPolicy) {
// console.log("Updating channel policy") this.log(DEBUG, "Updating channel policy")
const split = chanPoint.split(':') const split = chanPoint.split(':')
const res = await this.lightning.updateChannelPolicy({ const res = await this.lightning.updateChannelPolicy({
@ -565,19 +571,19 @@ export default class {
} }
async GetChannelBalance() { async GetChannelBalance() {
// console.log("Getting channel balance") this.log(DEBUG, "Getting channel balance")
const res = await this.lightning.channelBalance({}, DeadLineMetadata()) const res = await this.lightning.channelBalance({}, DeadLineMetadata())
return res.response return res.response
} }
async GetWalletBalance() { async GetWalletBalance() {
// console.log("Getting wallet balance") this.log(DEBUG, "Getting wallet balance")
const res = await this.lightning.walletBalance({ account: "", minConfs: 1 }, DeadLineMetadata()) const res = await this.lightning.walletBalance({ account: "", minConfs: 1 }, DeadLineMetadata())
return res.response return res.response
} }
async GetTotalBalace() { async GetTotalBalace() {
// console.log("Getting total balance") this.log(DEBUG, "Getting total balance")
const walletBalance = await this.GetWalletBalance() const walletBalance = await this.GetWalletBalance()
const confirmedWalletBalance = Number(walletBalance.confirmedBalance) const confirmedWalletBalance = Number(walletBalance.confirmedBalance)
this.utils.stateBundler.AddBalancePoint('walletBalance', confirmedWalletBalance) this.utils.stateBundler.AddBalancePoint('walletBalance', confirmedWalletBalance)
@ -592,10 +598,10 @@ export default class {
} }
async GetBalance(): Promise<BalanceInfo> { // TODO: remove this async GetBalance(): Promise<BalanceInfo> { // TODO: remove this
this.log(DEBUG, "Getting balance")
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
return { confirmedBalance: 0, unconfirmedBalance: 0, totalBalance: 0, channelsBalance: [] } return { confirmedBalance: 0, unconfirmedBalance: 0, totalBalance: 0, channelsBalance: [] }
} }
// console.log("Getting balance")
const wRes = await this.lightning.walletBalance({ account: "", minConfs: 1 }, DeadLineMetadata()) const wRes = await this.lightning.walletBalance({ account: "", minConfs: 1 }, DeadLineMetadata())
const { confirmedBalance, unconfirmedBalance, totalBalance } = wRes.response const { confirmedBalance, unconfirmedBalance, totalBalance } = wRes.response
const { response } = await this.lightning.listChannels({ const { response } = await this.lightning.listChannels({
@ -611,33 +617,47 @@ export default class {
} }
async GetForwardingHistory(indexOffset: number, startTime = 0, endTime = 0): Promise<ForwardingHistoryResponse> { async GetForwardingHistory(indexOffset: number, startTime = 0, endTime = 0): Promise<ForwardingHistoryResponse> {
this.log(DEBUG, "Getting forwarding history")
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
return { forwardingEvents: [], lastOffsetIndex: indexOffset } 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()) const { response } = await this.lightning.forwardingHistory({ indexOffset, numMaxEvents: 0, startTime: BigInt(startTime), endTime: BigInt(endTime), peerAliasLookup: false }, DeadLineMetadata())
return response return response
} }
async GetAllPaidInvoices(max: number) { async GetAllInvoices(max: number) {
this.log(DEBUG, "Getting all paid invoices")
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
return { invoices: [] } 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()) const res = await this.lightning.listInvoices({ indexOffset: 0n, numMaxInvoices: BigInt(max), pendingOnly: false, reversed: true, creationDateEnd: 0n, creationDateStart: 0n }, DeadLineMetadata())
return res.response return res.response
} }
async GetAllPayments(max: number) { async GetAllPayments(max: number) {
this.log(DEBUG, "Getting all payments")
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
return { payments: [] } 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 }) const res = await this.lightning.listPayments({ countTotalPayments: false, includeIncomplete: false, indexOffset: 0n, maxPayments: BigInt(max), reversed: true, creationDateEnd: 0n, creationDateStart: 0n })
return res.response 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) { async GetPayment(paymentIndex: number) {
// console.log("Getting payment") this.log(DEBUG, "Getting payment")
if (paymentIndex === 0) { if (paymentIndex === 0) {
throw new Error("payment index starts from 1") throw new Error("payment index starts from 1")
} }
@ -649,10 +669,10 @@ export default class {
} }
async GetLatestPaymentIndex(from = 0) { async GetLatestPaymentIndex(from = 0) {
this.log(DEBUG, "Getting latest payment index")
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
return from return from
} }
// console.log("Getting latest payment index")
let indexOffset = BigInt(from) let indexOffset = BigInt(from)
while (true) { while (true) {
const res = await this.lightning.listPayments({ countTotalPayments: false, includeIncomplete: false, indexOffset, maxPayments: 0n, reversed: false, creationDateEnd: 0n, creationDateStart: 0n }, DeadLineMetadata()) const res = await this.lightning.listPayments({ countTotalPayments: false, includeIncomplete: false, indexOffset, maxPayments: 0n, reversed: false, creationDateEnd: 0n, creationDateStart: 0n }, DeadLineMetadata())
@ -664,7 +684,7 @@ export default class {
} }
async ConnectPeer(addr: { pubkey: string, host: string }) { async ConnectPeer(addr: { pubkey: string, host: string }) {
// console.log("Connecting to peer") this.log(DEBUG, "Connecting to peer")
const res = await this.lightning.connectPeer({ const res = await this.lightning.connectPeer({
addr, addr,
perm: true, perm: true,
@ -674,7 +694,7 @@ export default class {
} }
async GetPaymentFromHash(paymentHash: string): Promise<Payment | null> { 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 abortController = new AbortController()
const stream = this.router.trackPaymentV2({ const stream = this.router.trackPaymentV2({
paymentHash: Buffer.from(paymentHash, 'hex'), paymentHash: Buffer.from(paymentHash, 'hex'),
@ -696,13 +716,12 @@ export default class {
} }
async GetTx(txid: string) { async GetTx(txid: string) {
// console.log("Getting transaction")
const res = await this.walletKit.getTransaction({ txid }, DeadLineMetadata()) const res = await this.walletKit.getTransaction({ txid }, DeadLineMetadata())
return res.response return res.response
} }
async AddPeer(pub: string, host: string, port: number) { async AddPeer(pub: string, host: string, port: number) {
// console.log("Adding peer") this.log(DEBUG, "Adding peer")
const res = await this.lightning.connectPeer({ const res = await this.lightning.connectPeer({
addr: { addr: {
pubkey: pub, pubkey: pub,
@ -715,19 +734,19 @@ export default class {
} }
async ListPeers() { async ListPeers() {
// console.log("Listing peers") this.log(DEBUG, "Listing peers")
const res = await this.lightning.listPeers({ latestError: true }, DeadLineMetadata()) const res = await this.lightning.listPeers({ latestError: true }, DeadLineMetadata())
return res.response return res.response
} }
async OpenChannel(destination: string, closeAddress: string, fundingAmount: number, pushSats: number, satsPerVByte: number): Promise<OpenStatusUpdate> { 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 abortController = new AbortController()
const req = OpenChannelReq(destination, closeAddress, fundingAmount, pushSats, satsPerVByte) const req = OpenChannelReq(destination, closeAddress, fundingAmount, pushSats, satsPerVByte)
const stream = this.lightning.openChannel(req, { abort: abortController.signal }) const stream = this.lightning.openChannel(req, { abort: abortController.signal })
return new Promise((res, rej) => { return new Promise((res, rej) => {
stream.responses.onMessage(message => { stream.responses.onMessage(message => {
console.log("message", message) this.log(DEBUG, "open channel message", message)
switch (message.update.oneofKind) { switch (message.update.oneofKind) {
case 'chanPending': case 'chanPending':
res(message) res(message)
@ -735,14 +754,14 @@ export default class {
} }
}) })
stream.responses.onError(error => { stream.responses.onError(error => {
console.log("error", error) this.log(ERROR, "open channel error", error)
rej(error) rej(error)
}) })
}) })
} }
async CloseChannel(fundingTx: string, outputIndex: number, force: boolean, satPerVByte: number): Promise<PendingUpdate> { 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({ const stream = this.lightning.closeChannel({
deliveryAddress: "", deliveryAddress: "",
force: force, force: force,
@ -761,7 +780,7 @@ export default class {
}, DeadLineMetadata()) }, DeadLineMetadata())
return new Promise((res, rej) => { return new Promise((res, rej) => {
stream.responses.onMessage(message => { stream.responses.onMessage(message => {
console.log("message", message) this.log(DEBUG, "close channel message", message)
switch (message.update.oneofKind) { switch (message.update.oneofKind) {
case 'closePending': case 'closePending':
res(message.update.closePending) res(message.update.closePending)
@ -769,7 +788,7 @@ export default class {
} }
}) })
stream.responses.onError(error => { stream.responses.onError(error => {
console.log("error", error) this.log(ERROR, "close channel error", error)
rej(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 * as Types from '../../../proto/autogenerated/ts/types.js'
import LND from "../lnd/lnd.js"; import LND from "../lnd/lnd.js";
import SettingsManager from "./settingsManager.js"; import SettingsManager from "./settingsManager.js";
import { Swaps } from "../lnd/swaps.js"; 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 { export class AdminManager {
settings: SettingsManager settings: SettingsManager
liquidityProvider: LiquidityProvider | null = null
storage: Storage storage: Storage
log = getLogger({ component: "adminManager" }) log = getLogger({ component: "adminManager" })
adminNpub = "" adminNpub = ""
@ -40,6 +81,10 @@ export class AdminManager {
this.start() this.start()
} }
attachLiquidityProvider(liquidityProvider: LiquidityProvider) {
this.liquidityProvider = liquidityProvider
}
attachNostrReset(f: () => Promise<void>) { attachNostrReset(f: () => Promise<void>) {
this.nostrReset = f this.nostrReset = f
} }
@ -260,15 +305,64 @@ export class AdminManager {
} }
} }
async ListAdminSwaps(): Promise<Types.SwapsList> { async ListAdminInvoiceSwaps(): Promise<Types.InvoiceSwapsList> {
return this.swaps.ListSwaps("admin", [], p => undefined, amt => 0) 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> { async GetAdminTransactionSwapQuotes(req: Types.TransactionSwapRequest): Promise<Types.TransactionSwapQuoteList> {
const quotes = await this.swaps.GetTxSwapQuotes("admin", req.transaction_amount_sats, () => 0) const quotes = await this.swaps.GetTxSwapQuotes("admin", req.transaction_amount_sats, () => 0)
return { quotes } 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 routingFloor = this.settings.getSettings().lndSettings.routingFeeFloor
const routingLimit = this.settings.getSettings().lndSettings.routingFeeLimitBps / 10000 const routingLimit = this.settings.getSettings().lndSettings.routingFeeLimitBps / 10000
@ -282,6 +376,222 @@ export class AdminManager {
network_fee: swap.network_fee, 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) => { 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) const appUser = await this.storage.applicationStorage.GetAppUserFromUser(app, user.user_id)
if (!appUser) { 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 nostrSettings = this.settings.getSettings().nostrRelaySettings
const { max, serviceFeeFloor, serviceFeeBps } = this.applicationManager.paymentManager.GetMaxPayableInvoice(user.balance_sats) 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] }), ndebit: ndebitEncode({ pubkey: app.nostr_public_key!, pointer: appUser.identifier, relay: nostrSettings.relays[0] }),
nmanage: nmanageEncode({ pubkey: app.nostr_public_key!, pointer: appUser.identifier, relay: nostrSettings.relays[0] }), nmanage: nmanageEncode({ pubkey: app.nostr_public_key!, pointer: appUser.identifier, relay: nostrSettings.relays[0] }),
callback_url: appUser.callback_url, 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") this.log("Found", toDelete.length, "inactive users to delete")
// await this.RemoveUsers(toDelete) await this.LockUsers(toDelete.map(u => u.userId))
} }
async CleanupNeverActiveUsers() { async CleanupNeverActiveUsers() {
this.log("Cleaning up never active users") 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[] }[] = [] const toDelete: { userId: string, appUserIds: string[] }[] = []
for (const u of inactiveUsers) { for (const u of inactiveUsers) {
const user = await this.storage.userStorage.GetUser(u.user_id) 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") 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[] }[]) { async RemoveUsers(toDelete: { userId: string, appUserIds: string[] }[]) {
this.log("Deleting", toDelete.length, "inactive users") this.log("Deleting", toDelete.length, "inactive users")
for (let i = 0; i < toDelete.length; i++) { for (let i = 0; i < toDelete.length; i++) {
const { userId, appUserIds } = toDelete[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) this.log("Deleting user", userId, "progress", i + 1, "/", toDelete.length)
await this.storage.StartTransaction(async tx => { await this.storage.StartTransaction(async tx => {
for (const appUserId of appUserIds) { for (const appUserId of appUserIds) {
@ -174,11 +188,16 @@ export default class {
await this.storage.offerStorage.DeleteUserOffers(appUserId, tx) await this.storage.offerStorage.DeleteUserOffers(appUserId, tx)
await this.storage.debitStorage.RemoveUserDebitAccess(appUserId, tx) await this.storage.debitStorage.RemoveUserDebitAccess(appUserId, tx)
await this.storage.applicationStorage.RemoveAppUserDevices(appUserId, tx) await this.storage.applicationStorage.RemoveAppUserDevices(appUserId, tx)
} }
await this.storage.paymentStorage.RemoveUserInvoices(userId, tx) await this.storage.paymentStorage.RemoveUserInvoices(userId, tx)
await this.storage.productStorage.RemoveUserProducts(userId, tx) await this.storage.productStorage.RemoveUserProducts(userId, tx)
await this.storage.paymentStorage.RemoveUserEphemeralKeys(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") 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] }), ndebit: ndebitEncode({ pubkey: app.nostr_public_key!, pointer: u.identifier, relay: nostrSettings.relays[0] }),
nmanage: nmanageEncode({ pubkey: app.nostr_public_key!, pointer: u.identifier, relay: nostrSettings.relays[0] }), nmanage: nmanageEncode({ pubkey: app.nostr_public_key!, pointer: u.identifier, relay: nostrSettings.relays[0] }),
callback_url: u.callback_url, 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 max_withdrawable: max
@ -227,7 +228,8 @@ export default class {
ndebit: ndebitEncode({ pubkey: app.nostr_public_key!, pointer: user.identifier, relay: nostrSettings.relays[0] }), ndebit: ndebitEncode({ pubkey: app.nostr_public_key!, pointer: user.identifier, relay: nostrSettings.relays[0] }),
nmanage: nmanageEncode({ pubkey: app.nostr_public_key!, pointer: user.identifier, relay: nostrSettings.relays[0] }), nmanage: nmanageEncode({ pubkey: app.nostr_public_key!, pointer: user.identifier, relay: nostrSettings.relays[0] }),
callback_url: user.callback_url, 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

@ -153,13 +153,14 @@ export class DebitManager {
} }
notifyPaymentSuccess = (debitRes: NdebitSuccess, event: { pub: string, id: string, appId: string }) => { notifyPaymentSuccess = (debitRes: NdebitSuccess, event: { pub: string, id: string, appId: string }) => {
this.logger("✅ [DEBIT REQUEST] Payment successful, sending OK response to", event.pub.slice(0, 16) + "...", "for event", event.id.slice(0, 16) + "...")
this.sendDebitResponse(debitRes, event) this.sendDebitResponse(debitRes, event)
} }
sendDebitResponse = (debitRes: NdebitFailure | NdebitSuccess, event: { pub: string, id: string, appId: string }) => { sendDebitResponse = (debitRes: NdebitFailure | NdebitSuccess, event: { pub: string, id: string, appId: string }) => {
this.logger("📤 [DEBIT RESPONSE] Sending Kind 21002 response:", JSON.stringify(debitRes), "to", event.pub.slice(0, 16) + "...")
const e = newNdebitResponse(JSON.stringify(debitRes), event) const e = newNdebitResponse(JSON.stringify(debitRes), event)
this.storage.NostrSender().Send({ type: 'app', appId: event.appId }, { type: 'event', event: e, encrypt: { toPub: event.pub } }) this.storage.NostrSender().Send({ type: 'app', appId: event.appId }, { type: 'event', event: e, encrypt: { toPub: event.pub } })
} }
payNdebitInvoice = async (event: NostrEvent, pointerdata: NdebitData): Promise<HandleNdebitRes> => { payNdebitInvoice = async (event: NostrEvent, pointerdata: NdebitData): Promise<HandleNdebitRes> => {

View file

@ -72,6 +72,7 @@ export default class {
this.unlocker = unlocker this.unlocker = unlocker
const updateProviderBalance = (b: number) => this.storage.liquidityStorage.IncrementTrackedProviderBalance('lnPub', settings.getSettings().liquiditySettings.liquidityProviderPub, b) 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) 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) this.rugPullTracker = new RugPullTracker(this.storage, this.liquidityProvider)
const lndGetSettings = () => ({ const lndGetSettings = () => ({
lndSettings: settings.getSettings().lndSettings, lndSettings: settings.getSettings().lndSettings,
@ -161,19 +162,23 @@ export default class {
NewBlockHandler = async (height: number, skipMetrics?: boolean) => { NewBlockHandler = async (height: number, skipMetrics?: boolean) => {
let confirmed: (PendingTx & { confs: number; })[] let confirmed: (PendingTx & { confs: number; })[]
let log = getLogger({}) let log = getLogger({})
log("NewBlockHandler called", JSON.stringify({ height, skipMetrics }))
this.storage.paymentStorage.DeleteExpiredTransactionSwaps(height) this.storage.paymentStorage.DeleteExpiredTransactionSwaps(height)
.catch(err => log(ERROR, "failed to delete expired transaction swaps", err.message || err)) .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 { try {
const balanceEvents = await this.paymentManager.GetLndBalance() const balanceEvents = await this.paymentManager.GetLndBalance()
if (!skipMetrics) { if (!skipMetrics) {
await this.metricsManager.NewBlockCb(height, balanceEvents) await this.metricsManager.NewBlockCb(height, balanceEvents)
} }
confirmed = await this.paymentManager.CheckNewlyConfirmedTxs(height) confirmed = await this.paymentManager.CheckNewlyConfirmedTxs()
await this.liquidityManager.onNewBlock() await this.liquidityManager.onNewBlock()
} catch (err: any) { } catch (err: any) {
log(ERROR, "failed to check transactions after new block", err.message || err) log(ERROR, "failed to check transactions after new block", err.message || err)
return return
} }
log("NewBlockHandler new confirmed transactions", confirmed.length)
await Promise.all(confirmed.map(async c => { await Promise.all(confirmed.map(async c => {
if (c.type === 'outgoing') { if (c.type === 'outgoing') {
await this.storage.paymentStorage.UpdateUserTransactionPayment(c.tx.serial_id, { confs: c.confs }) 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) => { addressPaidCb: AddressPaidCb = (txOutput, address, amount, used, broadcastHeight) => {
return this.storage.StartTransaction(async tx => { 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 // On-chain payments not supported when bypass is enabled
if (this.liquidityProvider.getSettings().useOnlyLiquidityProvider) { if (this.liquidityProvider.getSettings().useOnlyLiquidityProvider) {
getLogger({})("addressPaidCb called but USE_ONLY_LIQUIDITY_PROVIDER is enabled, ignoring") 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) { if (devices.length === 0 || !app.nostr_public_key || !app.nostr_private_key || !appUser.nostr_public_key) {
return return
} }
const tokens = devices.map(d => d.firebase_messaging_token) 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 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 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 = { const notification: ShockPushNotification = {
message: JSON.stringify(encryptedData), message: JSON.stringify(envelope),
body, body,
title title
} }

View file

@ -11,7 +11,7 @@ import { AdminManager } from "./adminManager.js"
import SettingsManager from "./settingsManager.js" import SettingsManager from "./settingsManager.js"
import { LoadStorageSettingsFromEnv } from "../storage/index.js" import { LoadStorageSettingsFromEnv } from "../storage/index.js"
import { NostrSender } from "../nostr/sender.js" import { NostrSender } from "../nostr/sender.js"
import { Swaps } from "../lnd/swaps.js" import { Swaps } from "../lnd/swaps/swaps.js"
export type AppData = { export type AppData = {
privateKey: string; privateKey: string;
publicKey: string; publicKey: string;
@ -79,6 +79,7 @@ export const initMainHandler = async (log: PubLogger, settingsManager: SettingsM
await mainHandler.paymentManager.CleanupOldUnpaidInvoices() await mainHandler.paymentManager.CleanupOldUnpaidInvoices()
await mainHandler.appUserManager.CleanupInactiveUsers() await mainHandler.appUserManager.CleanupInactiveUsers()
await mainHandler.appUserManager.CleanupNeverActiveUsers() await mainHandler.appUserManager.CleanupNeverActiveUsers()
await swaps.ResumeInvoiceSwaps()
await mainHandler.paymentManager.watchDog.Start() await mainHandler.paymentManager.watchDog.Start()
return { mainHandler, apps, localProviderClient, wizard, adminManager } return { mainHandler, apps, localProviderClient, wizard, adminManager }
} }

View file

@ -277,14 +277,14 @@ export class LiquidityProvider {
return res return res
} }
GetOperations = async () => { GetOperations = async (max = 200) => {
if (!this.IsReady()) { if (!this.IsReady()) {
throw new Error("liquidity provider is not ready yet, disabled or unreachable") throw new Error("liquidity provider is not ready yet, disabled or unreachable")
} }
const res = await this.client.GetUserOperations({ const res = await this.client.GetUserOperations({
latestIncomingInvoice: { ts: 0, id: 0 }, latestOutgoingInvoice: { ts: 0, id: 0 }, latestIncomingInvoice: { ts: 0, id: 0 }, latestOutgoingInvoice: { ts: 0, id: 0 },
latestIncomingTx: { ts: 0, id: 0 }, latestOutgoingTx: { ts: 0, id: 0 }, latestIncomingUserToUserPayment: { 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') { if (res.status === 'ERROR') {
this.log("error getting operations", res.reason) 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 { Utils } from '../helpers/utilsWrapper.js'
import { UserInvoicePayment } from '../storage/entity/UserInvoicePayment.js' import { UserInvoicePayment } from '../storage/entity/UserInvoicePayment.js'
import SettingsManager from './settingsManager.js' import SettingsManager from './settingsManager.js'
import { Swaps, TransactionSwapData } from '../lnd/swaps.js' import { Swaps } from '../lnd/swaps/swaps.js'
import { Transaction, OutputDetail } from '../../../proto/lnd/lightning.js' import { Transaction, OutputDetail } from '../../../proto/lnd/lightning.js'
import { LndAddress } from '../lnd/lnd.js' import { LndAddress } from '../lnd/lnd.js'
import Metrics from '../metrics/index.js' import Metrics from '../metrics/index.js'
@ -201,24 +201,11 @@ export default class {
} else { } else {
log("no missed chain transactions found") log("no missed chain transactions found")
} }
await this.reprocessStuckPendingTx(log, currentHeight)
} catch (err: any) { } catch (err: any) {
log(ERROR, "failed to check for missed chain transactions:", err.message || err) 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 }> { private async getLatestTransactions(log: PubLogger): Promise<{ txs: Transaction[], currentHeight: number, lndPubkey: string, startHeight: number }> {
const lndInfo = await this.lnd.GetInfo() const lndInfo = await this.lnd.GetInfo()
const lndPubkey = lndInfo.identityPubkey const lndPubkey = lndInfo.identityPubkey
@ -273,14 +260,14 @@ export default class {
private async processRootAddressOutput(output: OutputDetail, tx: Transaction, addresses: LndAddress[], log: PubLogger): Promise<boolean> { private async processRootAddressOutput(output: OutputDetail, tx: Transaction, addresses: LndAddress[], log: PubLogger): Promise<boolean> {
const addr = addresses.find(a => a.address === output.address) const addr = addresses.find(a => a.address === output.address)
if (!addr) { 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) { if (addr.change) {
log(`ignoring change address ${output.address}`) log(`ignoring change address ${output.address}`)
return false return false
} }
const outputIndex = Number(output.outputIndex) 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) { if (existingRootOp) {
return false return false
} }
@ -302,8 +289,11 @@ export default class {
const amount = Number(output.amount) const amount = Number(output.amount)
const outputIndex = Number(output.outputIndex) const outputIndex = Number(output.outputIndex)
log(`processing missed chain tx: address=${output.address}, txHash=${tx.txHash}, amount=${amount}, outputIndex=${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) try {
.catch(err => log(ERROR, "failed to process user address output:", err.message || err)) 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 return true
} }
@ -605,9 +595,15 @@ export default class {
async PayInternalAddress(ctx: Types.UserContext, req: Types.PayAddressRequest): Promise<Types.PayAddressResponse> { async PayInternalAddress(ctx: Types.UserContext, req: Types.PayAddressRequest): Promise<Types.PayAddressResponse> {
this.log("paying internal address") this.log("paying internal address")
let amount = req.amountSats
if (req.swap_operation_id) { 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) 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 { blockHeight } = await this.lnd.GetInfo()
const app = await this.storage.applicationStorage.GetApplication(ctx.app_id) const app = await this.storage.applicationStorage.GetApplication(ctx.app_id)
const isManagedUser = ctx.user_id !== app.owner.user_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> { async ListTxSwaps(ctx: Types.UserContext): Promise<Types.TxSwapsList> {
const payments = await this.storage.paymentStorage.ListSwapPayments(ctx.app_user_id) 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 app = await this.storage.applicationStorage.GetApplication(ctx.app_id)
const isManagedUser = ctx.user_id !== app.owner.user_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}` 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 }) 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)) }, 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 } return { amount: payment.paid_amount, fees: payment.service_fees }
} }
async CheckNewlyConfirmedTxs(height: number) { private async getTxConfs(txHash: string): Promise<number> {
const pending = await this.storage.paymentStorage.GetPendingTransactions() try {
let lowestHeight = height const info = await this.lnd.GetTx(txHash)
const map: Record<string, PendingTx> = {} const { numConfirmations: confs, amount: amt } = info
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
}
if (confs > 2 || (amt <= confInTwo && confs > 1) || (amt <= confInOne && confs > 0)) { if (confs > 2 || (amt <= confInTwo && confs > 1) || (amt <= confInOne && confs > 0)) {
return { ...t, confs } return confs
} }
}) } catch (err: any) {
return newlyConfirmedTxs.filter(t => t !== undefined) as (PendingTx & { confs: number })[] 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() { async CleanupOldUnpaidInvoices() {

View file

@ -226,7 +226,7 @@ export default class SanityChecker {
async VerifyEventsLog() { async VerifyEventsLog() {
this.events = await this.storage.eventsLog.GetAllLogs() 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.payments = (await this.lnd.GetAllPayments(1000)).payments
this.incrementSources = {} this.incrementSources = {}

View file

@ -29,6 +29,7 @@ export class Watchdog {
ready = false ready = false
interval: NodeJS.Timer; interval: NodeJS.Timer;
lndPubKey: string; lndPubKey: string;
lastHandlerRootOpsAtUnix = 0
constructor(settings: SettingsManager, liquidityManager: LiquidityManager, lnd: LND, storage: Storage, utils: Utils, rugPullTracker: RugPullTracker) { constructor(settings: SettingsManager, liquidityManager: LiquidityManager, lnd: LND, storage: Storage, utils: Utils, rugPullTracker: RugPullTracker) {
this.lnd = lnd; this.lnd = lnd;
this.settings = settings; this.settings = settings;
@ -67,7 +68,7 @@ export class Watchdog {
await this.getTracker() await this.getTracker()
const totalUsersBalance = await this.storage.paymentStorage.GetTotalUsersBalance() const totalUsersBalance = await this.storage.paymentStorage.GetTotalUsersBalance()
this.utils.stateBundler.AddBalancePoint('usersBalance', totalUsersBalance) this.utils.stateBundler.AddBalancePoint('usersBalance', totalUsersBalance)
const { totalExternal, otherExternal } = await this.getAggregatedExternalBalance() const { totalExternal } = await this.getAggregatedExternalBalance()
this.initialLndBalance = totalExternal this.initialLndBalance = totalExternal
this.initialUsersBalance = totalUsersBalance this.initialUsersBalance = totalUsersBalance
const fwEvents = await this.lnd.GetForwardingHistory(0, this.startedAtUnix) const fwEvents = await this.lnd.GetForwardingHistory(0, this.startedAtUnix)
@ -76,8 +77,6 @@ export class Watchdog {
const paymentFound = await this.storage.paymentStorage.GetMaxPaymentIndex() const paymentFound = await this.storage.paymentStorage.GetMaxPaymentIndex()
const knownMaxIndex = paymentFound.length > 0 ? Math.max(paymentFound[0].paymentIndex, 0) : 0 const knownMaxIndex = paymentFound.length > 0 ? Math.max(paymentFound[0].paymentIndex, 0) : 0
this.latestPaymentIndexOffset = await this.lnd.GetLatestPaymentIndex(knownMaxIndex) 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(() => { this.interval = setInterval(() => {
if (this.latestCheckStart + (1000 * 58) < Date.now()) { if (this.latestCheckStart + (1000 * 58) < Date.now()) {
this.PaymentRequested() this.PaymentRequested()
@ -93,7 +92,49 @@ export class Watchdog {
fwEvents.forwardingEvents.forEach((event) => { fwEvents.forwardingEvents.forEach((event) => {
this.accumulatedHtlcFees += Number(event.fee) 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 () => { getAggregatedExternalBalance = async () => {
@ -101,8 +142,9 @@ export class Watchdog {
const feesPaidForLiquidity = this.liquidityManager.GetPaidFees() const feesPaidForLiquidity = this.liquidityManager.GetPaidFees()
const pb = await this.rugPullTracker.CheckProviderBalance() const pb = await this.rugPullTracker.CheckProviderBalance()
const providerBalance = pb.prevBalance || pb.balance const providerBalance = pb.prevBalance || pb.balance
const otherExternal = { pb: providerBalance, f: feesPaidForLiquidity, lnd: totalLndBalance, olnd: othersFromLnd } const { newReceived, newSpent, pendingChange } = await this.handleRootOperations()
return { totalExternal: totalLndBalance + providerBalance + feesPaidForLiquidity, otherExternal } const opsTotal = newReceived + pendingChange - newSpent
return { totalExternal: totalLndBalance + providerBalance + feesPaidForLiquidity + opsTotal }
} }
checkBalanceUpdate = async (deltaLnd: number, deltaUsers: number) => { checkBalanceUpdate = async (deltaLnd: number, deltaUsers: number) => {
@ -187,7 +229,7 @@ export class Watchdog {
} }
const totalUsersBalance = await this.storage.paymentStorage.GetTotalUsersBalance() const totalUsersBalance = await this.storage.paymentStorage.GetTotalUsersBalance()
this.utils.stateBundler.AddBalancePoint('usersBalance', totalUsersBalance) this.utils.stateBundler.AddBalancePoint('usersBalance', totalUsersBalance)
const { totalExternal, otherExternal } = await this.getAggregatedExternalBalance() const { totalExternal } = await this.getAggregatedExternalBalance()
this.utils.stateBundler.AddBalancePoint('accumulatedHtlcFees', this.accumulatedHtlcFees) this.utils.stateBundler.AddBalancePoint('accumulatedHtlcFees', this.accumulatedHtlcFees)
const deltaLnd = totalExternal - (this.initialLndBalance + this.accumulatedHtlcFees) const deltaLnd = totalExternal - (this.initialLndBalance + this.accumulatedHtlcFees)
const deltaUsers = totalUsersBalance - this.initialUsersBalance const deltaUsers = totalUsersBalance - this.initialUsersBalance
@ -196,8 +238,6 @@ export class Watchdog {
const knownMaxIndex = Math.max(maxFromDb, this.latestPaymentIndexOffset) const knownMaxIndex = Math.max(maxFromDb, this.latestPaymentIndexOffset)
const newLatest = await this.lnd.GetLatestPaymentIndex(knownMaxIndex) const newLatest = await this.lnd.GetLatestPaymentIndex(knownMaxIndex)
const historyMismatch = newLatest > knownMaxIndex const historyMismatch = newLatest > knownMaxIndex
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) const deny = await this.checkBalanceUpdate(deltaLnd, deltaUsers)
if (historyMismatch) { if (historyMismatch) {
getLogger({ component: 'bark' })("History mismatch detected in absolute update, locking outgoing operations") getLogger({ component: 'bark' })("History mismatch detected in absolute update, locking outgoing operations")

View file

@ -414,9 +414,7 @@ export default class Handler {
await this.storage.metricsStorage.AddRootOperation("chain", `${address}:${txOutput.hash}:${txOutput.index}`, amount) await this.storage.metricsStorage.AddRootOperation("chain", `${address}:${txOutput.hash}:${txOutput.index}`, amount)
} }
async GetRootAddressTransaction(address: string, txHash: string, index: number) {
return this.storage.metricsStorage.GetRootOperation("chain", `${address}:${txHash}:${index}`)
}
async AddRootInvoicePaid(paymentRequest: string, amount: number) { async AddRootInvoicePaid(paymentRequest: string, amount: number) {
await this.storage.metricsStorage.AddRootOperation("invoice", paymentRequest, amount) await this.storage.metricsStorage.AddRootOperation("invoice", paymentRequest, amount)
@ -425,8 +423,13 @@ export default class Handler {
const mapRootOpType = (opType: string): Types.OperationType => { const mapRootOpType = (opType: string): Types.OperationType => {
switch (opType) { switch (opType) {
case "chain": return Types.OperationType.CHAIN_OP case "chain_payment":
case "invoice": return Types.OperationType.INVOICE_OP case "chain":
return Types.OperationType.CHAIN_OP
case "invoice_payment":
case "invoice":
return Types.OperationType.INVOICE_OP
default: throw new Error("Unknown operation type") default: throw new Error("Unknown operation type")
} }
} }

View file

@ -132,12 +132,12 @@ const handleNostrSettings = (settings: NostrSettings) => {
send(event) send(event)
}) })
} */ } */
const sendToNostr: NostrSend = (initiator, data, relays) => { const sendToNostr: NostrSend = async (initiator, data, relays) => {
if (!subProcessHandler) { if (!subProcessHandler) {
getLogger({ component: "nostrMiddleware" })(ERROR, "nostr was not initialized") getLogger({ component: "nostrMiddleware" })(ERROR, "nostr was not initialized")
return return
} }
subProcessHandler.Send(initiator, data, relays) await subProcessHandler.Send(initiator, data, relays)
} }
send({ type: 'ready' }) send({ type: 'ready' })

View file

@ -11,28 +11,69 @@ export default class NostrSubprocess {
utils: Utils utils: Utils
awaitingPongs: (() => void)[] = [] awaitingPongs: (() => void)[] = []
log = getLogger({}) 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) { constructor(settings: NostrSettings, utils: Utils, eventCallback: EventCallback, beaconCallback: BeaconCallback) {
this.utils = utils 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 = fork("./build/src/services/nostr/handler")
this.childProcess.on("error", (error) => { this.childProcess.on("error", (error) => {
this.log(ERROR, "nostr subprocess error", error) this.log(ERROR, "nostr subprocess error", error)
}) })
this.childProcess.on("exit", (code) => { this.childProcess.on("exit", (code, signal) => {
this.log(ERROR, `nostr subprocess exited with code ${code}`) if (this.isShuttingDown) {
if (!code) { this.log("nostr subprocess stopped")
return 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) => { this.childProcess.on("message", (message: ChildProcessResponse) => {
switch (message.type) { switch (message.type) {
case 'ready': case 'ready':
this.sendToChildProcess({ type: 'settings', settings: settings }) this.sendToChildProcess({ type: 'settings', settings: this.settings })
break; break;
case 'event': case 'event':
eventCallback(message.event) this.eventCallback(message.event)
break break
case 'processMetrics': case 'processMetrics':
this.utils.tlvStorageFactory.ProcessMetrics(message.metrics, 'nostr') this.utils.tlvStorageFactory.ProcessMetrics(message.metrics, 'nostr')
@ -42,7 +83,7 @@ export default class NostrSubprocess {
this.awaitingPongs = [] this.awaitingPongs = []
break break
case 'beacon': case 'beacon':
beaconCallback({ content: message.content, pub: message.pub }) this.beaconCallback({ content: message.content, pub: message.pub })
break break
default: default:
console.error("unknown nostr event response", message) console.error("unknown nostr event response", message)
@ -50,11 +91,15 @@ export default class NostrSubprocess {
} }
}) })
} }
sendToChildProcess(message: ChildProcessRequest) { sendToChildProcess(message: ChildProcessRequest) {
if (this.childProcess && !this.childProcess.killed) {
this.childProcess.send(message) this.childProcess.send(message)
} }
}
Reset(settings: NostrSettings) { Reset(settings: NostrSettings) {
this.settings = settings
this.sendToChildProcess({ type: 'settings', settings }) this.sendToChildProcess({ type: 'settings', settings })
} }
@ -68,7 +113,9 @@ export default class NostrSubprocess {
Send(initiator: SendInitiator, data: SendData, relays?: string[]) { Send(initiator: SendInitiator, data: SendData, relays?: string[]) {
this.sendToChildProcess({ type: 'send', data, initiator, relays }) this.sendToChildProcess({ type: 'send', data, initiator, relays })
} }
Stop() { Stop() {
this.childProcess.kill() this.isShuttingDown = true
this.cleanupProcess()
} }
} }

View file

@ -16,7 +16,7 @@ export type SendDataContent = { type: "content", content: string, pub: string }
export type SendDataEvent = { type: "event", event: UnsignedEvent, encrypt?: { toPub: string } } export type SendDataEvent = { type: "event", event: UnsignedEvent, encrypt?: { toPub: string } }
export type SendData = SendDataContent | SendDataEvent export type SendData = SendDataContent | SendDataEvent
export type SendInitiator = { type: 'app', appId: string } | { type: 'client', clientId: string } export type SendInitiator = { type: 'app', appId: string } | { type: 'client', clientId: string }
export type NostrSend = (initiator: SendInitiator, data: SendData, relays?: string[] | undefined) => void export type NostrSend = (initiator: SendInitiator, data: SendData, relays?: string[] | undefined) => Promise<void>
export type LinkedProviderInfo = { pubkey: string, clientId: string, relayUrl: string } export type LinkedProviderInfo = { pubkey: string, clientId: string, relayUrl: string }
export type AppInfo = { appId: string, publicKey: string, privateKey: string, name: string, provider?: LinkedProviderInfo } export type AppInfo = { appId: string, publicKey: string, privateKey: string, name: string, provider?: LinkedProviderInfo }
@ -203,21 +203,22 @@ export class NostrPool {
const signed = finalizeEvent(event, Buffer.from(keys.privateKey, 'hex')) const signed = finalizeEvent(event, Buffer.from(keys.privateKey, 'hex'))
let sent = false let sent = false
const log = getLogger({ appName: keys.name }) const log = getLogger({ appName: keys.name })
// const r = relays ? relays : this.getServiceRelays() this.log(`📤 Publishing Kind ${event.kind} event to ${relays.length} relay(s): ${relays.join(', ')}`)
const pool = new SimplePool() const pool = new SimplePool()
await Promise.all(pool.publish(relays, signed).map(async p => { await Promise.all(pool.publish(relays, signed).map(async p => {
try { try {
await p await p
sent = true sent = true
} catch (e: any) { } catch (e: any) {
console.log(e) this.log(ERROR, `Failed to publish Kind ${event.kind} event:`, e.message || e)
log(e) log(e)
} }
})) }))
if (!sent) { if (!sent) {
this.log(ERROR, `Failed to send Kind ${event.kind} event to any relay`)
log("failed to send event") log("failed to send event")
} else { } else {
//log("sent event") this.log(`✅ Kind ${event.kind} event published successfully (id: ${signed.id.slice(0, 16)}...)`)
} }
} }

View file

@ -1,7 +1,7 @@
import { NostrSend, SendData, SendInitiator } from "./nostrPool.js" import { NostrSend, SendData, SendInitiator } from "./nostrPool.js"
import { getLogger } from "../helpers/logger.js" import { ERROR, getLogger } from "../helpers/logger.js"
export class NostrSender { export class NostrSender {
private _nostrSend: NostrSend = () => { throw new Error('nostr send not initialized yet') } private _nostrSend: NostrSend = async () => { throw new Error('nostr send not initialized yet') }
private isReady: boolean = false private isReady: boolean = false
private onReadyCallbacks: (() => void)[] = [] private onReadyCallbacks: (() => void)[] = []
private pendingSends: { initiator: SendInitiator, data: SendData, relays?: string[] | undefined }[] = [] private pendingSends: { initiator: SendInitiator, data: SendData, relays?: string[] | undefined }[] = []
@ -12,7 +12,12 @@ export class NostrSender {
this.isReady = true this.isReady = true
this.onReadyCallbacks.forEach(cb => cb()) this.onReadyCallbacks.forEach(cb => cb())
this.onReadyCallbacks = [] this.onReadyCallbacks = []
this.pendingSends.forEach(send => this._nostrSend(send.initiator, send.data, send.relays)) // Process pending sends with proper error handling
this.pendingSends.forEach(send => {
this._nostrSend(send.initiator, send.data, send.relays).catch(e => {
this.log(ERROR, "failed to send pending event", e.message || e)
})
})
this.pendingSends = [] this.pendingSends = []
} }
OnReady(callback: () => void) { OnReady(callback: () => void) {
@ -22,13 +27,16 @@ export class NostrSender {
this.onReadyCallbacks.push(callback) this.onReadyCallbacks.push(callback)
} }
} }
Send(initiator: SendInitiator, data: SendData, relays?: string[] | undefined) { Send(initiator: SendInitiator, data: SendData, relays?: string[] | undefined): void {
if (!this.isReady) { if (!this.isReady) {
this.log("tried to send before nostr was ready, caching request") this.log("tried to send before nostr was ready, caching request")
this.pendingSends.push({ initiator, data, relays }) this.pendingSends.push({ initiator, data, relays })
return return
} }
this._nostrSend(initiator, data, relays) // Fire and forget but log errors
this._nostrSend(initiator, data, relays).catch(e => {
this.log(ERROR, "failed to send event", e.message || e)
})
} }
IsReady() { IsReady() {
return this.isReady return this.isReady

View file

@ -91,6 +91,14 @@ export default (mainHandler: Main): Types.ServerMethods => {
if (err != null) throw new Error(err.message) if (err != null) throw new Error(err.message)
return mainHandler.adminManager.CloseChannel(req) return mainHandler.adminManager.CloseChannel(req)
}, },
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 }) => { GetAdminTransactionSwapQuotes: async ({ ctx, req }) => {
const err = Types.TransactionSwapRequestValidate(req, { const err = Types.TransactionSwapRequestValidate(req, {
transaction_amount_sats_CustomCheck: amt => amt > 0 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) if (err != null) throw new Error(err.message)
return mainHandler.adminManager.PayAdminTransactionSwap(req) 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 () => { GetProvidersDisruption: async () => {
return mainHandler.metricsManager.GetProvidersDisruption() return mainHandler.metricsManager.GetProvidersDisruption()
}, },
@ -145,9 +183,7 @@ export default (mainHandler: Main): Types.ServerMethods => {
GetUserOperations: async ({ ctx, req }) => { GetUserOperations: async ({ ctx, req }) => {
return mainHandler.paymentManager.GetUserOperations(ctx.user_id, req) return mainHandler.paymentManager.GetUserOperations(ctx.user_id, req)
}, },
ListAdminSwaps: async ({ ctx }) => {
return mainHandler.adminManager.ListAdminSwaps()
},
GetPaymentState: async ({ ctx, req }) => { GetPaymentState: async ({ ctx, req }) => {
const err = Types.GetPaymentStateRequestValidate(req, { const err = Types.GetPaymentStateRequestValidate(req, {
invoice_CustomCheck: invoice => invoice !== "" invoice_CustomCheck: invoice => invoice !== ""
@ -159,14 +195,14 @@ export default (mainHandler: Main): Types.ServerMethods => {
PayAddress: async ({ ctx, req }) => { PayAddress: async ({ ctx, req }) => {
const err = Types.PayAddressRequestValidate(req, { const err = Types.PayAddressRequestValidate(req, {
address_CustomCheck: addr => addr !== '', address_CustomCheck: addr => addr !== '',
amountSats_CustomCheck: amt => amt > 0, // amountSats_CustomCheck: amt => amt > 0,
// satsPerVByte_CustomCheck: spb => spb > 0 // satsPerVByte_CustomCheck: spb => spb > 0
}) })
if (err != null) throw new Error(err.message) if (err != null) throw new Error(err.message)
return mainHandler.paymentManager.PayAddress(ctx, req) return mainHandler.paymentManager.PayAddress(ctx, req)
}, },
ListSwaps: async ({ ctx }) => { ListTxSwaps: async ({ ctx }) => {
return mainHandler.paymentManager.ListSwaps(ctx) return mainHandler.paymentManager.ListTxSwaps(ctx)
}, },
GetTransactionSwapQuotes: async ({ ctx, req }) => { GetTransactionSwapQuotes: async ({ ctx, req }) => {
return mainHandler.paymentManager.GetTransactionSwapQuotes(ctx, req) return mainHandler.paymentManager.GetTransactionSwapQuotes(ctx, req)

View file

@ -1,5 +1,5 @@
import crypto from 'crypto'; 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 { generateSecretKey, getPublicKey } from 'nostr-tools';
import { Application } from "./entity/Application.js" import { Application } from "./entity/Application.js"
import UserStorage from './userStorage.js'; import UserStorage from './userStorage.js';
@ -72,7 +72,8 @@ export default class {
user: user, user: user,
application, application,
identifier: userIdentifier, identifier: userIdentifier,
nostr_public_key: nostrPub nostr_public_key: nostrPub,
topic_id: crypto.randomBytes(32).toString('hex')
}, txId) }, txId)
}) })
} }
@ -161,9 +162,15 @@ export default class {
} }
async RemoveAppUsersAndBaseUsers(appUserIds: string[], baseUser: string, txId?: string) { async RemoveAppUsersAndBaseUsers(appUserIds: string[], baseUser: string, txId?: string) {
await this.dbs.Delete<ApplicationUser>('ApplicationUser', { identifier: In(appUserIds) }, txId) for (const appUserId of appUserIds) {
await this.dbs.Delete<User>('User', { user_id: baseUser }, txId) 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 { UserAccess } from "../entity/UserAccess.js"
import { AdminSettings } from "../entity/AdminSettings.js" import { AdminSettings } from "../entity/AdminSettings.js"
import { TransactionSwap } from "../entity/TransactionSwap.js" import { TransactionSwap } from "../entity/TransactionSwap.js"
import { InvoiceSwap } from "../entity/InvoiceSwap.js"
export type DbSettings = { export type DbSettings = {
@ -76,7 +77,8 @@ export const MainDbEntities = {
'AppUserDevice': AppUserDevice, 'AppUserDevice': AppUserDevice,
'UserAccess': UserAccess, 'UserAccess': UserAccess,
'AdminSettings': AdminSettings, 'AdminSettings': AdminSettings,
'TransactionSwap': TransactionSwap 'TransactionSwap': TransactionSwap,
'InvoiceSwap': InvoiceSwap
} }
export type MainDbNames = keyof typeof MainDbEntities export type MainDbNames = keyof typeof MainDbEntities
export const MainDbEntitiesNames = Object.keys(MainDbEntities) export const MainDbEntitiesNames = Object.keys(MainDbEntities)

View file

@ -8,10 +8,19 @@ type SerializedFindOperator = {
} }
export function serializeFindOperator(operator: FindOperator<any>): 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 { return {
_type: 'FindOperator', _type: 'FindOperator',
type: operator['type'], 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') { if (!r || typeof r !== 'object') {
return r; return r;
} }
@ -61,23 +71,24 @@ export function serializeRequest<T>(r: object): T {
} }
if (Array.isArray(r)) { if (Array.isArray(r)) {
return r.map(item => serializeRequest(item)) as any; return r.map(item => serializeRequest(item, debug)) as any;
} }
const result: any = {}; const result: any = {};
for (const [key, value] of Object.entries(r)) { for (const [key, value] of Object.entries(r)) {
result[key] = serializeRequest(value); result[key] = serializeRequest(value, debug);
} }
return result; 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') { if (!r || typeof r !== 'object') {
return r; return r;
} }
if (Array.isArray(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') { if (r && typeof r === 'object' && (r as any)._type === 'FindOperator') {
@ -86,7 +97,7 @@ export function deserializeRequest<T>(r: object): T {
const result: any = {}; const result: any = {};
for (const [key, value] of Object.entries(r)) { for (const [key, value] of Object.entries(r)) {
result[key] = deserializeRequest(value); result[key] = deserializeRequest(value, debug);
} }
return result; return result;
} }

View file

@ -61,13 +61,13 @@ export class StorageInterface extends EventEmitter {
this.isConnected = false; this.isConnected = false;
}); });
this.process.on('exit', (code: number) => { this.process.on('exit', (code: number, signal: string) => {
this.log(ERROR, `Storage processor exited with code ${code}`); this.log(ERROR, `Storage processor exited with code ${code} and signal ${signal}`);
this.isConnected = false; this.isConnected = false;
if (!code) { if (code === 0) {
return 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; this.isConnected = true;
@ -104,9 +104,10 @@ export class StorageInterface extends EventEmitter {
return this.handleOp<T | null>(findOp) 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 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) return this.handleOp<T[]>(findOp)
} }
@ -166,15 +167,16 @@ export class StorageInterface extends EventEmitter {
} }
private handleOp<T>(op: IStorageOperation): Promise<T> { 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() this.checkConnected()
return new Promise<T>((resolve, reject) => { return new Promise<T>((resolve, reject) => {
const responseHandler = (response: OperationResponse<T>) => { const responseHandler = (response: OperationResponse<T>) => {
if (this.debug) console.log('responseHandler', response) if (this.debug || op.debug) console.log('responseHandler', response)
if (!response.success) { if (!response.success) {
reject(new Error(response.error)); reject(new Error(response.error));
return return
} }
if (this.debug || op.debug) console.log("response", response, op)
if (response.type !== op.type) { if (response.type !== op.type) {
reject(new Error('Invalid storage response type')); reject(new Error('Invalid storage response type'));
return return
@ -186,12 +188,12 @@ export class StorageInterface extends EventEmitter {
}) })
} }
private serializeOperation(operation: IStorageOperation): IStorageOperation { private serializeOperation(operation: IStorageOperation, debug = false): IStorageOperation {
const serialized = { ...operation }; const serialized = { ...operation };
if ('q' in serialized) { 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 serialized.debug = true
} }
return serialized; return serialized;
@ -205,7 +207,7 @@ export class StorageInterface extends EventEmitter {
public disconnect() { public disconnect() {
if (this.process) { if (this.process) {
this.process.kill(); this.process.kill(0);
this.isConnected = false; this.isConnected = false;
this.debug = false; this.debug = false;
} }

View file

@ -26,6 +26,9 @@ export class ApplicationUser {
@Column({ default: "" }) @Column({ default: "" })
callback_url: string callback_url: string
@Column({ unique: true })
topic_id: string;
@CreateDateColumn() @CreateDateColumn()
created_at: Date 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 }) @Column({ default: 0 })
at_unix: number at_unix: number
@Column({ default: false })
pending: boolean
@CreateDateColumn() @CreateDateColumn()
created_at: Date created_at: Date

View file

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

View file

@ -10,6 +10,7 @@ import { StorageInterface } from "./db/storageInterface.js";
import { Utils } from "../helpers/utilsWrapper.js"; import { Utils } from "../helpers/utilsWrapper.js";
import { Channel, ChannelEventUpdate } from "../../../proto/lnd/lightning.js"; import { Channel, ChannelEventUpdate } from "../../../proto/lnd/lightning.js";
import { ChannelEvent } from "./entity/ChannelEvent.js"; import { ChannelEvent } from "./entity/ChannelEvent.js";
export type RootOperationType = 'chain' | 'invoice' | 'chain_payment' | 'invoice_payment'
export default class { export default class {
//DB: DataSource | EntityManager //DB: DataSource | EntityManager
settings: StorageSettings settings: StorageSettings
@ -145,13 +146,27 @@ export default class {
} }
} }
async AddRootOperation(opType: string, id: string, amount: number, txId?: string) { 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) }, txId) 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) 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) { async GetRootOperations({ from, to }: { from?: number, to?: number }, txId?: string) {
const q = getTimeQuery({ from, to }) 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 { 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 { LspOrder1718387847693 } from './1718387847693-lsp_order.js'
import { LiquidityProvider1719335699480 } from './1719335699480-liquidity_provider.js' import { LiquidityProvider1719335699480 } from './1719335699480-liquidity_provider.js'
import { LndNodeInfo1720187506189 } from './1720187506189-lnd_node_info.js' import { LndNodeInfo1720187506189 } from './1720187506189-lnd_node_info.js'
import { TrackedProvider1720814323679 } from './1720814323679-tracked_provider.js' import { TrackedProvider1720814323679 } from './1720814323679-tracked_provider.js'
import { CreateInviteTokenTable1721751414878 } from "./1721751414878-create_invite_token_table.js" import { CreateInviteTokenTable1721751414878 } from "./1721751414878-create_invite_token_table.js"
import { PaymentIndex1721760297610 } from './1721760297610-payment_index.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 { DebitAccess1726496225078 } from './1726496225078-debit_access.js'
import { DebitAccessFixes1726685229264 } from './1726685229264-debit_access_fixes.js' import { DebitAccessFixes1726685229264 } from './1726685229264-debit_access_fixes.js'
import { DebitToPub1727105758354 } from './1727105758354-debit_to_pub.js' import { DebitToPub1727105758354 } from './1727105758354-debit_to_pub.js'
import { UserCbUrl1727112281043 } from './1727112281043-user_cb_url.js' import { UserCbUrl1727112281043 } from './1727112281043-user_cb_url.js'
import { RootOps1732566440447 } from './1732566440447-root_ops.js'
import { UserOffer1733502626042 } from './1733502626042-user_offer.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 { ManagementGrant1751307732346 } from './1751307732346-management_grant.js'
import { ManagementGrantBanned1751989251513 } from './1751989251513-management_grant_banned.js' import { ManagementGrantBanned1751989251513 } from './1751989251513-management_grant_banned.js'
import { InvoiceCallbackUrls1752425992291 } from './1752425992291-invoice_callback_urls.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 { OldSomethingLeftover1753106599604 } from './1753106599604-old_something_leftover.js'
import { UserReceivingInvoiceIdx1753109184611 } from './1753109184611-user_receiving_invoice_idx.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 { UserAccess1759426050669 } from './1759426050669-user_access.js'
import { AddBlindToUserOffer1760000000000 } from './1760000000000-add_blind_to_user_offer.js' import { AddBlindToUserOffer1760000000000 } from './1760000000000-add_blind_to_user_offer.js'
import { ApplicationAvatarUrl1761000001000 } from './1761000001000-application_avatar_url.js' import { ApplicationAvatarUrl1761000001000 } from './1761000001000-application_avatar_url.js'
@ -32,6 +25,23 @@ import { TxSwapAddress1764779178945 } from './1764779178945-tx_swap_address.js'
import { ClinkRequester1765497600000 } from './1765497600000-clink_requester.js' import { ClinkRequester1765497600000 } from './1765497600000-clink_requester.js'
import { TrackedProviderHeight1766504040000 } from './1766504040000-tracked_provider_height.js' import { TrackedProviderHeight1766504040000 } from './1766504040000-tracked_provider_height.js'
import { SwapsServiceUrl1768413055036 } from './1768413055036-swaps_service_url.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, export const allMigrations = [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189,
@ -39,9 +49,13 @@ export const allMigrations = [Initial1703170309875, LspOrder1718387847693, Liqui
DebitToPub1727105758354, UserCbUrl1727112281043, UserOffer1733502626042, ManagementGrant1751307732346, ManagementGrantBanned1751989251513, DebitToPub1727105758354, UserCbUrl1727112281043, UserOffer1733502626042, ManagementGrant1751307732346, ManagementGrantBanned1751989251513,
InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611, AppUserDevice1753285173175, InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611, AppUserDevice1753285173175,
UserAccess1759426050669, AddBlindToUserOffer1760000000000, ApplicationAvatarUrl1761000001000, AdminSettings1761683639419, TxSwap1762890527098, 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> => { /* export const TypeOrmMigrationRunner = async (log: PubLogger, storageManager: Storage, settings: DbSettings, arg: string | undefined): Promise<boolean> => {
await connectAndMigrate(log, storageManager, allMigrations, allMetricsMigrations) await connectAndMigrate(log, storageManager, allMigrations, allMetricsMigrations)
return false return false

View file

@ -15,6 +15,7 @@ import TransactionsQueue from "./db/transactionsQueue.js";
import { LoggedEvent } from './eventsLog.js'; import { LoggedEvent } from './eventsLog.js';
import { StorageInterface } from './db/storageInterface.js'; import { StorageInterface } from './db/storageInterface.js';
import { TransactionSwap } from './entity/TransactionSwap.js'; 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 type InboundOptionals = { product?: Product, callbackUrl?: string, expiry: number, expectedPayer?: User, linkedApplication?: Application, zapInfo?: ZapInfo, offerId?: string, payerData?: Record<string, string>, rejectUnauthorized?: boolean, token?: string, blind?: boolean, clinkRequesterPub?: string, clinkRequesterEventId?: string }
export const defaultInvoiceExpiry = 60 * 60 export const defaultInvoiceExpiry = 60 * 60
export default class { export default class {
@ -137,7 +138,15 @@ export default class {
} }
async RemoveUserInvoices(userId: string, txId?: string) { 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> { 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) 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> { async GetInvoiceOwner(paymentRequest: string, txId?: string): Promise<UserReceivingInvoice | null> {
return this.dbs.FindOne<UserReceivingInvoice>('UserReceivingInvoice', { where: { invoice: paymentRequest } }, txId) return this.dbs.FindOne<UserReceivingInvoice>('UserReceivingInvoice', { where: { invoice: paymentRequest } }, txId)
} }
@ -317,7 +330,51 @@ export default class {
} }
async RemoveUserEphemeralKeys(userId: string, txId?: string) { 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) { 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) { async GetTotalUsersBalance(excludeLocked?: boolean, txId?: string) {
const total = await this.dbs.Sum<User>('User', "balance_sats", {}) const where: { locked?: boolean } = {}
if (excludeLocked) {
where.locked = false
}
const total = await this.dbs.Sum<User>('User', "balance_sats", where, txId)
return total || 0 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) 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 }, { return this.dbs.Update<TransactionSwap>('TransactionSwap', { swap_operation_id: swapOperationId }, {
used: true, paid_at_unix: now,
tx_id: txId, }, txId)
address_paid: address,
})
} }
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 }, { return this.dbs.Update<TransactionSwap>('TransactionSwap', { swap_operation_id: swapOperationId }, {
used: true, used: true,
failure_reason: failureReason, failure_reason: failureReason,
address_paid: address, address_paid: address,
}) completed_at_unix: now,
}, txId)
} }
async DeleteTransactionSwap(swapOperationId: string, txId?: string) { async DeleteTransactionSwap(swapOperationId: string, txId?: string) {
@ -493,18 +565,18 @@ export default class {
} }
async DeleteExpiredTransactionSwaps(currentHeight: number, txId?: string) { 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) { async ListPendingTransactionSwaps(appUserId: string, txId?: string) {
return this.dbs.Find<TransactionSwap>('TransactionSwap', { where: { used: false, app_user_id: appUserId } }, txId) 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) 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 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 payments = await this.dbs.Find<UserInvoicePayment>('UserInvoicePayment', { where: { swap_operation_id: Not(IsNull()), } }, txId)
const paymentsMap = new Map<string, UserInvoicePayment>() const paymentsMap = new Map<string, UserInvoicePayment>()
@ -515,6 +587,85 @@ export default class {
swap: c, payment: paymentsMap.get(c.swap_operation_id) 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>) => { const orFail = async <T>(resultPromise: Promise<T | null>) => {

View file

@ -21,6 +21,14 @@ export default class {
} }
async RemoveUserProducts(userId: string, txId?: string) { 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.isConnected = false;
}); });
this.process.on('exit', (code: number) => { this.process.on('exit', (code: number, signal: string) => {
this.log(ERROR, `Tlv Storage processor exited with code ${code}`); this.log(ERROR, `Tlv Storage processor exited with code ${code} and signal ${signal}`);
this.isConnected = false; this.isConnected = false;
if (!code) { if (code === 0) {
return 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; this.isConnected = true;
@ -173,7 +173,7 @@ export class TlvStorageFactory extends EventEmitter {
public disconnect() { public disconnect() {
if (this.process) { if (this.process) {
this.process.kill(); this.process.kill(0);
this.isConnected = false; this.isConnected = false;
this.debug = false; this.debug = false;
} }

View file

@ -126,7 +126,7 @@ class TlvFilesStorageProcessor {
throw new Error('Unknown metric type: ' + t) throw new Error('Unknown metric type: ' + t)
} }
}) })
this.wrtc.attachNostrSend((initiator: SendInitiator, data: SendData, relays?: string[] | undefined) => { this.wrtc.attachNostrSend(async (initiator: SendInitiator, data: SendData, relays?: string[] | undefined) => {
this.sendResponse({ this.sendResponse({
success: true, success: true,
type: 'nostrSend', type: 'nostrSend',

View file

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

View file

@ -27,11 +27,11 @@ export default class webRTC {
attachNostrSend(f: NostrSend) { attachNostrSend(f: NostrSend) {
this._nostrSend = f this._nostrSend = f
} }
private nostrSend: NostrSend = (initiator: SendInitiator, data: SendData, relays?: string[] | undefined) => { private nostrSend: NostrSend = async (initiator: SendInitiator, data: SendData, relays?: string[] | undefined) => {
if (!this._nostrSend) { if (!this._nostrSend) {
throw new Error("No nostrSend attached") throw new Error("No nostrSend attached")
} }
this._nostrSend(initiator, data, relays) await this._nostrSend(initiator, data, relays)
} }
private sendCandidate = (u: WebRtcUserInfo, candidate: string) => { private sendCandidate = (u: WebRtcUserInfo, candidate: string) => {