Compare commits

..

9 commits

Author SHA1 Message Date
Patrick Mulligan
eb0278a82c fix: use fresh balance in PayAppUserInvoice notification
Some checks failed
Docker Compose Actions Workflow / test (push) Has been cancelled
notifyAppUserPayment was sending the stale cached balance from the
entity loaded before PayInvoice decremented it. Update the entity's
balance_sats from the PayInvoice response so LiveUserOperation events
contain the correct post-payment balance.

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

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 07:53:39 -05:00
Patrick Mulligan
1f4157b00f fix: correct nip44v1 secp256k1 getSharedSecret argument types
The @noble/curves secp256k1.getSharedSecret expects Uint8Array arguments,
not hex strings. Use hex.decode() to convert the private and public keys.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-28 07:53:39 -05:00
Patrick Mulligan
66b1ceedef 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-02-28 07:53:39 -05:00
Patrick Mulligan
e6a4994213 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-02-28 07:53:39 -05:00
Patrick Mulligan
86baf10041 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-02-28 07:53:39 -05:00
Patrick Mulligan
7973fa83cb fix(nostr): close SimplePool after publishing to prevent connection leak
Some checks failed
Docker Compose Actions Workflow / test (push) Has been cancelled
Each sendEvent() call created a new SimplePool() but never closed it,
causing relay WebSocket connections to accumulate indefinitely (~20/min).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 07:53:09 -05:00
Patrick Mulligan
748a2d3ed6 fix(handlers): await NostrSend calls throughout codebase
Some checks failed
Docker Compose Actions Workflow / test (push) Has been cancelled
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-01-25 14:38:24 -05:00
Patrick Mulligan
30d818c4d4 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-01-25 14:37:56 -05:00
51 changed files with 1058 additions and 4459 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

@ -15,7 +15,7 @@ import { AddInvoiceReq } from './addInvoiceReq.js';
import { PayInvoiceReq } from './payInvoiceReq.js';
import { SendCoinsReq } from './sendCoinsReq.js';
import { AddressPaidCb, InvoicePaidCb, NodeInfo, Invoice, DecodedInvoice, PaidInvoice, NewBlockCb, HtlcCb, BalanceInfo, ChannelEventCb } from './settings.js';
import { ERROR, getLogger, DEBUG, INFO } from '../helpers/logger.js';
import { ERROR, getLogger } from '../helpers/logger.js';
import { HtlcEvent_EventType } from '../../../proto/lnd/router.js';
import { LiquidityProvider } from '../main/liquidityProvider.js';
import { Utils } from '../helpers/utilsWrapper.js';
@ -23,7 +23,7 @@ import { TxPointSettings } from '../storage/tlv/stateBundler.js';
import { WalletKitClient } from '../../../proto/lnd/walletkit.client.js';
import SettingsManager from '../main/settingsManager.js';
import { LndNodeSettings, LndSettings } from '../main/settings.js';
import { ListAddressesResponse, PublishResponse } from '../../../proto/lnd/walletkit.js';
import { ListAddressesResponse } from '../../../proto/lnd/walletkit.js';
const DeadLineMetadata = (deadline = 10 * 1000) => ({ deadline: Date.now() + deadline })
const deadLndRetrySeconds = 20
@ -69,7 +69,7 @@ export default class {
// Skip LND client initialization if using only liquidity provider
if (liquidProvider.getSettings().useOnlyLiquidityProvider) {
this.log(INFO, "USE_ONLY_LIQUIDITY_PROVIDER enabled, skipping LND client initialization")
this.log("USE_ONLY_LIQUIDITY_PROVIDER enabled, skipping LND client initialization")
// Create minimal dummy clients - they won't be used but prevent null reference errors
// Use insecure credentials directly (can't combine them)
const { lndAddr } = this.getSettings().lndNodeSettings
@ -126,13 +126,13 @@ export default class {
}
async Warmup() {
this.log(INFO, "Warming up LND")
// Skip LND warmup if using only liquidity provider
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
this.log(INFO, "USE_ONLY_LIQUIDITY_PROVIDER enabled, skipping LND warmup")
this.log("USE_ONLY_LIQUIDITY_PROVIDER enabled, skipping LND warmup")
this.ready = true
return
}
// console.log("Warming up LND")
this.SubscribeAddressPaid()
this.SubscribeInvoicePaid()
await this.SubscribeNewBlock()
@ -147,7 +147,7 @@ export default class {
this.ready = true
res()
} catch (err) {
this.log(INFO, "LND is not ready yet, will try again in 1 second")
this.log("LND is not ready yet, will try again in 1 second")
if (Date.now() - now > 1000 * 60) {
rej(new Error("LND not ready after 1 minute"))
}
@ -156,13 +156,6 @@ export default class {
})
}
async PublishTransaction(txHex: string): Promise<PublishResponse> {
const res = await this.walletKit.publishTransaction({
txHex: Buffer.from(txHex, 'hex'), label: ""
}, DeadLineMetadata())
return res.response
}
async GetInfo(): Promise<NodeInfo> {
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
// Return dummy info when bypass is enabled
@ -176,26 +169,27 @@ export default class {
uris: []
}
}
// console.log("Getting info")
const res = await this.lightning.getInfo({}, DeadLineMetadata())
return res.response
}
async ListPendingChannels(): Promise<PendingChannelsResponse> {
this.log(DEBUG, "Listing pending channels")
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
return { pendingOpenChannels: [], pendingClosingChannels: [], pendingForceClosingChannels: [], waitingCloseChannels: [], totalLimboBalance: 0n }
}
// console.log("Listing pending channels")
const res = await this.lightning.pendingChannels({ includeRawTx: false }, DeadLineMetadata())
return res.response
}
async ListChannels(peerLookup = false): Promise<ListChannelsResponse> {
this.log(DEBUG, "Listing channels")
// console.log("Listing channels")
const res = await this.lightning.listChannels({
activeOnly: false, inactiveOnly: false, privateOnly: false, publicOnly: false, peer: Buffer.alloc(0), peerAliasLookup: peerLookup
}, DeadLineMetadata())
return res.response
}
async ListClosedChannels(): Promise<ClosedChannelsResponse> {
this.log(DEBUG, "Listing closed channels")
// console.log("Listing closed channels")
const res = await this.lightning.closedChannels({
abandoned: true,
breach: true,
@ -212,6 +206,7 @@ export default class {
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
return
}
// console.log("Checking health")
if (!this.ready) {
throw new Error("not ready")
}
@ -222,69 +217,69 @@ export default class {
}
RestartStreams() {
this.log(INFO, "Restarting streams")
// console.log("Restarting streams")
if (!this.ready || this.abortController.signal.aborted) {
return
}
this.log(INFO, "LND is dead, will try to reconnect in", deadLndRetrySeconds, "seconds")
this.log("LND is dead, will try to reconnect in", deadLndRetrySeconds, "seconds")
const interval = setInterval(async () => {
try {
await this.unlockLnd()
this.log(INFO, "LND is back online")
this.log("LND is back online")
clearInterval(interval)
await this.Warmup()
} catch (err) {
this.log(INFO, "LND still dead, will try again in", deadLndRetrySeconds, "seconds")
this.log("LND still dead, will try again in", deadLndRetrySeconds, "seconds")
}
}, deadLndRetrySeconds * 1000)
}
async SubscribeChannelEvents() {
this.log(DEBUG, "Subscribing to channel events")
// console.log("Subscribing to channel events")
const stream = this.lightning.subscribeChannelEvents({}, { abort: this.abortController.signal })
stream.responses.onMessage(async channel => {
const channels = await this.ListChannels()
this.channelEventCb(channel, channels.channels)
})
stream.responses.onError(error => {
this.log(ERROR, "Error with subscribeChannelEvents stream")
this.log("Error with subscribeChannelEvents stream")
})
stream.responses.onComplete(() => {
this.log(INFO, "subscribeChannelEvents stream closed")
this.log("subscribeChannelEvents stream closed")
})
}
async SubscribeHtlcEvents() {
this.log(DEBUG, "Subscribing to htlc events")
// console.log("Subscribing to htlc events")
const stream = this.router.subscribeHtlcEvents({}, { abort: this.abortController.signal })
stream.responses.onMessage(htlc => {
this.htlcCb(htlc)
})
stream.responses.onError(error => {
this.log(ERROR, "Error with subscribeHtlcEvents stream")
this.log("Error with subscribeHtlcEvents stream")
})
stream.responses.onComplete(() => {
this.log(INFO, "subscribeHtlcEvents stream closed")
this.log("subscribeHtlcEvents stream closed")
})
}
async SubscribeNewBlock() {
this.log(DEBUG, "Subscribing to new block")
// console.log("Subscribing to new block")
const { blockHeight } = await this.GetInfo()
const stream = this.chainNotifier.registerBlockEpochNtfn({ height: blockHeight, hash: Buffer.alloc(0) }, { abort: this.abortController.signal })
stream.responses.onMessage(block => {
this.newBlockCb(block.height)
})
stream.responses.onError(error => {
this.log(ERROR, "Error with new block stream")
this.log("Error with new block stream")
})
stream.responses.onComplete(() => {
this.log(INFO, "new block stream closed")
this.log("new block stream closed")
})
}
SubscribeAddressPaid(): void {
this.log(DEBUG, "Subscribing to address paid")
// console.log("Subscribing to address paid")
const stream = this.lightning.subscribeTransactions({
account: "",
endHeight: 0,
@ -303,15 +298,15 @@ export default class {
}
})
stream.responses.onError(error => {
this.log(ERROR, "Error with onchain tx stream")
this.log("Error with onchain tx stream")
})
stream.responses.onComplete(() => {
this.log(INFO, "onchain tx stream closed")
this.log("onchain tx stream closed")
})
}
SubscribeInvoicePaid(): void {
this.log(DEBUG, "Subscribing to invoice paid")
// console.log("Subscribing to invoice paid")
const stream = this.lightning.subscribeInvoices({
settleIndex: BigInt(this.latestKnownSettleIndex),
addIndex: 0n,
@ -324,14 +319,14 @@ export default class {
})
let restarted = false
stream.responses.onError(error => {
this.log(ERROR, "Error with invoice stream")
this.log("Error with invoice stream")
if (!restarted) {
restarted = true
this.RestartStreams()
}
})
stream.responses.onComplete(() => {
this.log(INFO, "invoice stream closed")
this.log("invoice stream closed")
if (!restarted) {
restarted = true
this.RestartStreams()
@ -353,7 +348,6 @@ export default class {
}
async ListAddresses(): Promise<LndAddress[]> {
this.log(DEBUG, "Listing addresses")
const res = await this.walletKit.listAddresses({ accountName: "", showCustomAccounts: false }, DeadLineMetadata())
const addresses = res.response.accountWithAddresses.map(a => a.addresses.map(a => ({ address: a.address, change: a.isInternal }))).flat()
addresses.forEach(a => this.addressesCache[a.address] = { isChange: a.change })
@ -361,11 +355,11 @@ export default class {
}
async NewAddress(addressType: Types.AddressType, { useProvider, from }: TxActionOptions): Promise<NewAddressResponse> {
this.log(DEBUG, "Creating new address")
// Force use of provider when bypass is enabled (addresses not supported by provider, but we should fail gracefully)
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
throw new Error("Address generation not supported when USE_ONLY_LIQUIDITY_PROVIDER is enabled")
}
// console.log("Creating new address")
let lndAddressType: AddressType
switch (addressType) {
case Types.AddressType.NESTED_PUBKEY_HASH:
@ -395,11 +389,11 @@ export default class {
}
async NewInvoice(value: number, memo: string, expiry: number, { useProvider, from }: TxActionOptions, blind = false): Promise<Invoice> {
this.log(DEBUG, "Creating new invoice")
// console.log("Creating new invoice")
// Force use of provider when bypass is enabled
const mustUseProvider = this.liquidProvider.getSettings().useOnlyLiquidityProvider || useProvider
if (mustUseProvider) {
this.log(INFO, "using provider")
console.log("using provider")
const invoice = await this.liquidProvider.AddInvoice(value, memo, from, expiry)
const providerPubkey = this.liquidProvider.GetProviderPubkey()
return { payRequest: invoice, providerPubkey }
@ -415,7 +409,6 @@ export default class {
}
async DecodeInvoice(paymentRequest: string): Promise<DecodedInvoice> {
this.log(DEBUG, "Decoding invoice")
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
// Use light-bolt11-decoder when LND is bypassed
try {
@ -441,23 +434,24 @@ export default class {
throw new Error(`Failed to decode invoice: ${err.message}`)
}
}
// console.log("Decoding invoice")
const res = await this.lightning.decodePayReq({ payReq: paymentRequest }, DeadLineMetadata())
return { numSatoshis: Number(res.response.numSatoshis), paymentHash: res.response.paymentHash }
}
async ChannelBalance(): Promise<{ local: number, remote: number }> {
this.log(DEBUG, "Getting channel balance")
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
return { local: 0, remote: 0 }
}
// console.log("Getting channel balance")
const res = await this.lightning.channelBalance({})
const r = res.response
return { local: r.localBalance ? Number(r.localBalance.sat) : 0, remote: r.remoteBalance ? Number(r.remoteBalance.sat) : 0 }
}
async PayInvoice(invoice: string, amount: number, { routingFeeLimit, serviceFee }: { routingFeeLimit: number, serviceFee: number }, decodedAmount: number, { useProvider, from }: TxActionOptions, paymentIndexCb?: (index: number) => void): Promise<PaidInvoice> {
this.log(DEBUG, "Paying invoice")
// console.log("Paying invoice")
if (this.outgoingOpsLocked) {
this.log(ERROR, "outgoing ops locked, rejecting payment request")
this.log("outgoing ops locked, rejecting payment request")
throw new Error("lnd node is currently out of sync")
}
// Force use of provider when bypass is enabled
@ -474,7 +468,7 @@ export default class {
const stream = this.router.sendPaymentV2(req, { abort: abortController.signal })
return new Promise((res, rej) => {
stream.responses.onError(error => {
this.log(ERROR, "invoice payment failed", error)
this.log("invoice payment failed", error)
rej(error)
})
let indexSent = false
@ -486,7 +480,7 @@ export default class {
}
switch (payment.status) {
case Payment_PaymentStatus.FAILED:
this.log(ERROR, "invoice payment failed", payment.failureReason)
this.log("invoice payment failed", payment.failureReason)
rej(PaymentFailureReason[payment.failureReason])
return
case Payment_PaymentStatus.SUCCEEDED:
@ -504,7 +498,7 @@ export default class {
}
async EstimateChainFees(address: string, amount: number, targetConf: number): Promise<EstimateFeeResponse> {
this.log(DEBUG, "Estimating chain fees")
// console.log("Estimating chain fees")
await this.Health()
const res = await this.lightning.estimateFee({
addrToAmount: { [address]: BigInt(amount) },
@ -517,13 +511,13 @@ export default class {
}
async PayAddress(address: string, amount: number, satPerVByte: number, label = "", { useProvider, from }: TxActionOptions): Promise<SendCoinsResponse> {
this.log(DEBUG, "Paying address")
// Address payments not supported when bypass is enabled
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
throw new Error("Address payments not supported when USE_ONLY_LIQUIDITY_PROVIDER is enabled")
}
// console.log("Paying address")
if (this.outgoingOpsLocked) {
this.log(ERROR, "outgoing ops locked, rejecting payment request")
this.log("outgoing ops locked, rejecting payment request")
throw new Error("lnd node is currently out of sync")
}
if (useProvider) {
@ -541,19 +535,19 @@ export default class {
}
async GetTransactions(startHeight: number): Promise<TransactionDetails> {
this.log(DEBUG, "Getting transactions")
const res = await this.lightning.getTransactions({ startHeight, endHeight: 0, account: "", }, DeadLineMetadata())
// console.log("Getting transactions")
const res = await this.lightning.getTransactions({ startHeight, endHeight: 0, account: "" }, DeadLineMetadata())
return res.response
}
async GetChannelInfo(chanId: string) {
this.log(DEBUG, "Getting channel info")
// console.log("Getting channel info")
const res = await this.lightning.getChanInfo({ chanId, chanPoint: "" }, DeadLineMetadata())
return res.response
}
async UpdateChannelPolicy(chanPoint: string, policy: Types.ChannelPolicy) {
this.log(DEBUG, "Updating channel policy")
// console.log("Updating channel policy")
const split = chanPoint.split(':')
const res = await this.lightning.updateChannelPolicy({
@ -571,19 +565,19 @@ export default class {
}
async GetChannelBalance() {
this.log(DEBUG, "Getting channel balance")
// console.log("Getting channel balance")
const res = await this.lightning.channelBalance({}, DeadLineMetadata())
return res.response
}
async GetWalletBalance() {
this.log(DEBUG, "Getting wallet balance")
// console.log("Getting wallet balance")
const res = await this.lightning.walletBalance({ account: "", minConfs: 1 }, DeadLineMetadata())
return res.response
}
async GetTotalBalace() {
this.log(DEBUG, "Getting total balance")
// console.log("Getting total balance")
const walletBalance = await this.GetWalletBalance()
const confirmedWalletBalance = Number(walletBalance.confirmedBalance)
this.utils.stateBundler.AddBalancePoint('walletBalance', confirmedWalletBalance)
@ -598,10 +592,10 @@ export default class {
}
async GetBalance(): Promise<BalanceInfo> { // TODO: remove this
this.log(DEBUG, "Getting balance")
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
return { confirmedBalance: 0, unconfirmedBalance: 0, totalBalance: 0, channelsBalance: [] }
}
// console.log("Getting balance")
const wRes = await this.lightning.walletBalance({ account: "", minConfs: 1 }, DeadLineMetadata())
const { confirmedBalance, unconfirmedBalance, totalBalance } = wRes.response
const { response } = await this.lightning.listChannels({
@ -617,47 +611,33 @@ export default class {
}
async GetForwardingHistory(indexOffset: number, startTime = 0, endTime = 0): Promise<ForwardingHistoryResponse> {
this.log(DEBUG, "Getting forwarding history")
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
return { forwardingEvents: [], lastOffsetIndex: indexOffset }
}
// console.log("Getting forwarding history")
const { response } = await this.lightning.forwardingHistory({ indexOffset, numMaxEvents: 0, startTime: BigInt(startTime), endTime: BigInt(endTime), peerAliasLookup: false }, DeadLineMetadata())
return response
}
async GetAllInvoices(max: number) {
this.log(DEBUG, "Getting all paid invoices")
async GetAllPaidInvoices(max: number) {
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
return { invoices: [] }
}
// console.log("Getting all paid invoices")
const res = await this.lightning.listInvoices({ indexOffset: 0n, numMaxInvoices: BigInt(max), pendingOnly: false, reversed: true, creationDateEnd: 0n, creationDateStart: 0n }, DeadLineMetadata())
return res.response
}
async GetAllPayments(max: number) {
this.log(DEBUG, "Getting all payments")
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
return { payments: [] }
}
// console.log("Getting all payments")
const res = await this.lightning.listPayments({ countTotalPayments: false, includeIncomplete: false, indexOffset: 0n, maxPayments: BigInt(max), reversed: true, creationDateEnd: 0n, creationDateStart: 0n })
return res.response
}
async BumpFee(txId: string, outputIndex: number, satPerVbyte: number) {
this.log(DEBUG, "Bumping fee")
const res = await this.walletKit.bumpFee({
budget: 0n, immediate: false, targetConf: 0, satPerVbyte: BigInt(satPerVbyte), outpoint: {
txidStr: txId,
outputIndex: outputIndex,
txidBytes: Buffer.alloc(0)
},
force: false,
satPerByte: 0
}, DeadLineMetadata())
return res.response
}
async GetPayment(paymentIndex: number) {
this.log(DEBUG, "Getting payment")
// console.log("Getting payment")
if (paymentIndex === 0) {
throw new Error("payment index starts from 1")
}
@ -669,10 +649,10 @@ export default class {
}
async GetLatestPaymentIndex(from = 0) {
this.log(DEBUG, "Getting latest payment index")
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
return from
}
// console.log("Getting latest payment index")
let indexOffset = BigInt(from)
while (true) {
const res = await this.lightning.listPayments({ countTotalPayments: false, includeIncomplete: false, indexOffset, maxPayments: 0n, reversed: false, creationDateEnd: 0n, creationDateStart: 0n }, DeadLineMetadata())
@ -684,7 +664,7 @@ export default class {
}
async ConnectPeer(addr: { pubkey: string, host: string }) {
this.log(DEBUG, "Connecting to peer")
// console.log("Connecting to peer")
const res = await this.lightning.connectPeer({
addr,
perm: true,
@ -694,7 +674,7 @@ export default class {
}
async GetPaymentFromHash(paymentHash: string): Promise<Payment | null> {
this.log(DEBUG, "Getting payment from hash")
// console.log("Getting payment from hash")
const abortController = new AbortController()
const stream = this.router.trackPaymentV2({
paymentHash: Buffer.from(paymentHash, 'hex'),
@ -716,12 +696,13 @@ export default class {
}
async GetTx(txid: string) {
// console.log("Getting transaction")
const res = await this.walletKit.getTransaction({ txid }, DeadLineMetadata())
return res.response
}
async AddPeer(pub: string, host: string, port: number) {
this.log(DEBUG, "Adding peer")
// console.log("Adding peer")
const res = await this.lightning.connectPeer({
addr: {
pubkey: pub,
@ -734,19 +715,19 @@ export default class {
}
async ListPeers() {
this.log(DEBUG, "Listing peers")
// console.log("Listing peers")
const res = await this.lightning.listPeers({ latestError: true }, DeadLineMetadata())
return res.response
}
async OpenChannel(destination: string, closeAddress: string, fundingAmount: number, pushSats: number, satsPerVByte: number): Promise<OpenStatusUpdate> {
this.log(DEBUG, "Opening channel")
// console.log("Opening channel")
const abortController = new AbortController()
const req = OpenChannelReq(destination, closeAddress, fundingAmount, pushSats, satsPerVByte)
const stream = this.lightning.openChannel(req, { abort: abortController.signal })
return new Promise((res, rej) => {
stream.responses.onMessage(message => {
this.log(DEBUG, "open channel message", message)
console.log("message", message)
switch (message.update.oneofKind) {
case 'chanPending':
res(message)
@ -754,14 +735,14 @@ export default class {
}
})
stream.responses.onError(error => {
this.log(ERROR, "open channel error", error)
console.log("error", error)
rej(error)
})
})
}
async CloseChannel(fundingTx: string, outputIndex: number, force: boolean, satPerVByte: number): Promise<PendingUpdate> {
this.log(DEBUG, "Closing channel")
// console.log("Closing channel")
const stream = this.lightning.closeChannel({
deliveryAddress: "",
force: force,
@ -780,7 +761,7 @@ export default class {
}, DeadLineMetadata())
return new Promise((res, rej) => {
stream.responses.onMessage(message => {
this.log(DEBUG, "close channel message", message)
console.log("message", message)
switch (message.update.oneofKind) {
case 'closePending':
res(message.update.closePending)
@ -788,7 +769,7 @@ export default class {
}
})
stream.responses.onError(error => {
this.log(ERROR, "close channel error", error)
console.log("error", error)
rej(error)
})
})

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

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

View file

@ -1,299 +0,0 @@
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

@ -1,539 +0,0 @@
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

@ -1,50 +0,0 @@
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

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

View file

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

View file

@ -169,8 +169,7 @@ export default class {
ndebit: ndebitEncode({ pubkey: app.nostr_public_key!, pointer: u.identifier, relay: nostrSettings.relays[0] }),
nmanage: nmanageEncode({ pubkey: app.nostr_public_key!, pointer: u.identifier, relay: nostrSettings.relays[0] }),
callback_url: u.callback_url,
bridge_url: this.settings.getSettings().serviceSettings.bridgeUrl,
topic_id: u.topic_id
bridge_url: this.settings.getSettings().serviceSettings.bridgeUrl
},
max_withdrawable: max
@ -228,8 +227,7 @@ export default class {
ndebit: ndebitEncode({ pubkey: app.nostr_public_key!, pointer: user.identifier, relay: nostrSettings.relays[0] }),
nmanage: nmanageEncode({ pubkey: app.nostr_public_key!, pointer: user.identifier, relay: nostrSettings.relays[0] }),
callback_url: user.callback_url,
bridge_url: this.settings.getSettings().serviceSettings.bridgeUrl,
topic_id: user.topic_id
bridge_url: this.settings.getSettings().serviceSettings.bridgeUrl
},
}
}

View file

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

View file

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

View file

@ -277,14 +277,14 @@ export class LiquidityProvider {
return res
}
GetOperations = async (max = 200) => {
GetOperations = async () => {
if (!this.IsReady()) {
throw new Error("liquidity provider is not ready yet, disabled or unreachable")
}
const res = await this.client.GetUserOperations({
latestIncomingInvoice: { ts: 0, id: 0 }, latestOutgoingInvoice: { ts: 0, id: 0 },
latestIncomingTx: { ts: 0, id: 0 }, latestOutgoingTx: { ts: 0, id: 0 }, latestIncomingUserToUserPayment: { ts: 0, id: 0 },
latestOutgoingUserToUserPayment: { ts: 0, id: 0 }, max_size: max
latestOutgoingUserToUserPayment: { ts: 0, id: 0 }, max_size: 200
})
if (res.status === 'ERROR') {
this.log("error getting operations", res.reason)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,94 +0,0 @@
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,9 +17,6 @@ export class RootOperation {
@Column({ default: 0 })
at_unix: number
@Column({ default: false })
pending: boolean
@CreateDateColumn()
created_at: Date

View file

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

View file

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

View file

@ -1,14 +0,0 @@
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

@ -1,21 +0,0 @@
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

@ -1,24 +0,0 @@
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

@ -1,20 +0,0 @@
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

@ -1,20 +0,0 @@
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

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

View file

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

View file

@ -21,14 +21,6 @@ export default class {
}
async RemoveUserProducts(userId: string, txId?: string) {
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
return this.dbs.Delete<Product>('Product', { owner: { user_id: userId } }, txId)
}
}

View file

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

View file

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