diff --git a/datasource.js b/datasource.js index dfae53bf..4badca25 100644 --- a/datasource.js +++ b/datasource.js @@ -1,3 +1,14 @@ +/** + * TypeORM DataSource used only by the TypeORM CLI (e.g. migration:generate). + * + * Migrations at runtime are run from src/services/storage/migrations/runner.ts (allMigrations), + * not from this file. The app never uses this DataSource to run migrations. + * + * Workflow: update the migrations array in this file *before* running + * migration:generate, so TypeORM knows the current schema (entities + existing migrations). + * We do not update this file immediately after adding a new migration; update it when you + * are about to generate the next migration. + */ import { DataSource } from "typeorm" import { User } from "./build/src/services/storage/entity/User.js" import { UserReceivingInvoice } from "./build/src/services/storage/entity/UserReceivingInvoice.js" @@ -22,11 +33,13 @@ import { AppUserDevice } from "./build/src/services/storage/entity/AppUserDevice import { UserAccess } from "./build/src/services/storage/entity/UserAccess.js" import { AdminSettings } from "./build/src/services/storage/entity/AdminSettings.js" import { TransactionSwap } from "./build/src/services/storage/entity/TransactionSwap.js" +import { InvoiceSwap } from "./build/src/services/storage/entity/InvoiceSwap.js" import { Initial1703170309875 } from './build/src/services/storage/migrations/1703170309875-initial.js' import { LspOrder1718387847693 } from './build/src/services/storage/migrations/1718387847693-lsp_order.js' -import { LndNodeInfo1720187506189 } from './build/src/services/storage/migrations/1720187506189-lnd_node_info.js' import { LiquidityProvider1719335699480 } from './build/src/services/storage/migrations/1719335699480-liquidity_provider.js' +import { LndNodeInfo1720187506189 } from './build/src/services/storage/migrations/1720187506189-lnd_node_info.js' +import { TrackedProvider1720814323679 } from './build/src/services/storage/migrations/1720814323679-tracked_provider.js' import { CreateInviteTokenTable1721751414878 } from './build/src/services/storage/migrations/1721751414878-create_invite_token_table.js' import { PaymentIndex1721760297610 } from './build/src/services/storage/migrations/1721760297610-payment_index.js' import { DebitAccess1726496225078 } from './build/src/services/storage/migrations/1726496225078-debit_access.js' @@ -35,6 +48,7 @@ import { DebitToPub1727105758354 } from './build/src/services/storage/migrations import { UserCbUrl1727112281043 } from './build/src/services/storage/migrations/1727112281043-user_cb_url.js' import { UserOffer1733502626042 } from './build/src/services/storage/migrations/1733502626042-user_offer.js' import { ManagementGrant1751307732346 } from './build/src/services/storage/migrations/1751307732346-management_grant.js' +import { ManagementGrantBanned1751989251513 } from './build/src/services/storage/migrations/1751989251513-management_grant_banned.js' import { InvoiceCallbackUrls1752425992291 } from './build/src/services/storage/migrations/1752425992291-invoice_callback_urls.js' import { OldSomethingLeftover1753106599604 } from './build/src/services/storage/migrations/1753106599604-old_something_leftover.js' import { UserReceivingInvoiceIdx1753109184611 } from './build/src/services/storage/migrations/1753109184611-user_receiving_invoice_idx.js' @@ -47,20 +61,32 @@ import { TxSwap1762890527098 } from './build/src/services/storage/migrations/176 import { TxSwapAddress1764779178945 } from './build/src/services/storage/migrations/1764779178945-tx_swap_address.js' import { ClinkRequester1765497600000 } from './build/src/services/storage/migrations/1765497600000-clink_requester.js' import { TrackedProviderHeight1766504040000 } from './build/src/services/storage/migrations/1766504040000-tracked_provider_height.js' +import { SwapsServiceUrl1768413055036 } from './build/src/services/storage/migrations/1768413055036-swaps_service_url.js' +import { InvoiceSwaps1769529793283 } from './build/src/services/storage/migrations/1769529793283-invoice_swaps.js' +import { InvoiceSwapsFixes1769805357459 } from './build/src/services/storage/migrations/1769805357459-invoice_swaps_fixes.js' +import { ApplicationUserTopicId1770038768784 } from './build/src/services/storage/migrations/1770038768784-application_user_topic_id.js' +import { SwapTimestamps1771347307798 } from './build/src/services/storage/migrations/1771347307798-swap_timestamps.js' + + + export default new DataSource({ type: "better-sqlite3", database: "db.sqlite", // logging: true, - migrations: [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189, CreateInviteTokenTable1721751414878, - PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264, DebitToPub1727105758354, UserCbUrl1727112281043, - UserOffer1733502626042, ManagementGrant1751307732346, InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611, - AppUserDevice1753285173175, UserAccess1759426050669, AddBlindToUserOffer1760000000000, ApplicationAvatarUrl1761000001000, AdminSettings1761683639419, TxSwap1762890527098, - TxSwapAddress1764779178945, ClinkRequester1765497600000, TrackedProviderHeight1766504040000], + migrations: [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189, + TrackedProvider1720814323679, CreateInviteTokenTable1721751414878, PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264, + DebitToPub1727105758354, UserCbUrl1727112281043, UserOffer1733502626042, ManagementGrant1751307732346, ManagementGrantBanned1751989251513, + InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611, AppUserDevice1753285173175, + UserAccess1759426050669, AddBlindToUserOffer1760000000000, ApplicationAvatarUrl1761000001000, AdminSettings1761683639419, TxSwap1762890527098, + TxSwapAddress1764779178945, ClinkRequester1765497600000, TrackedProviderHeight1766504040000, SwapsServiceUrl1768413055036, + InvoiceSwaps1769529793283, InvoiceSwapsFixes1769805357459, ApplicationUserTopicId1770038768784, SwapTimestamps1771347307798 + ], + entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment, UserBasicAuth, UserEphemeralKey, Product, UserToUserPayment, Application, ApplicationUser, UserToUserPayment, LspOrder, LndNodeInfo, - TrackedProvider, InviteToken, DebitAccess, UserOffer, ManagementGrant, AppUserDevice, UserAccess, AdminSettings, TransactionSwap], + TrackedProvider, InviteToken, DebitAccess, UserOffer, ManagementGrant, AppUserDevice, UserAccess, AdminSettings, TransactionSwap, InvoiceSwap], // synchronize: true, }) -//npx typeorm migration:generate ./src/services/storage/migrations/swaps_service_url -d ./datasource.js \ No newline at end of file +//npx typeorm migration:generate ./src/services/storage/migrations/tx_swap_timestamps -d ./datasource.js \ No newline at end of file diff --git a/metricsDatasource.js b/metricsDatasource.js index 226b9dac..aaf5ddbc 100644 --- a/metricsDatasource.js +++ b/metricsDatasource.js @@ -1,3 +1,14 @@ +/** + * TypeORM DataSource used only by the TypeORM CLI (e.g. migration:generate). + * + * Migrations at runtime are run from src/services/storage/migrations/runner.ts (allMigrations), + * not from this file. The app never uses this DataSource to run migrations. + * + * Workflow: update the migrations array in this file *before* running + * migration:generate, so TypeORM knows the current schema (entities + existing migrations). + * We do not update this file immediately after adding a new migration; update it when you + * are about to generate the next migration. + */ import { DataSource } from "typeorm" import { BalanceEvent } from "./build/src/services/storage/entity/BalanceEvent.js" import { ChannelBalanceEvent } from "./build/src/services/storage/entity/ChannelsBalanceEvent.js" @@ -8,12 +19,16 @@ import { LndMetrics1703170330183 } from './build/src/services/storage/migrations import { ChannelRouting1709316653538 } from './build/src/services/storage/migrations/1709316653538-channel_routing.js' import { HtlcCount1724266887195 } from './build/src/services/storage/migrations/1724266887195-htlc_count.js' import { BalanceEvents1724860966825 } from './build/src/services/storage/migrations/1724860966825-balance_events.js' - +import { RootOps1732566440447 } from './build/src/services/storage/migrations/1732566440447-root_ops.js' +import { RootOpsTime1745428134124 } from './build/src/services/storage/migrations/1745428134124-root_ops_time.js' +import { ChannelEvents1750777346411 } from './build/src/services/storage/migrations/1750777346411-channel_events.js' +import { RootOpPending1771524665409 } from './build/src/services/storage/migrations/1771524665409-root_op_pending.js' export default new DataSource({ type: "better-sqlite3", database: "metrics.sqlite", entities: [BalanceEvent, ChannelBalanceEvent, ChannelRouting, RootOperation, ChannelEvent], - migrations: [LndMetrics1703170330183, ChannelRouting1709316653538, HtlcCount1724266887195, BalanceEvents1724860966825] + migrations: [LndMetrics1703170330183, ChannelRouting1709316653538, HtlcCount1724266887195, BalanceEvents1724860966825, + RootOps1732566440447, RootOpsTime1745428134124, ChannelEvents1750777346411, RootOpPending1771524665409] }); -//npx typeorm migration:generate ./src/services/storage/migrations/channel_events -d ./metricsDatasource.js \ No newline at end of file +//npx typeorm migration:generate ./src/services/storage/migrations/root_op_pending -d ./metricsDatasource.js \ No newline at end of file diff --git a/proto/autogenerated/client.md b/proto/autogenerated/client.md index 99023c69..d42fc9ac 100644 --- a/proto/autogenerated/client.md +++ b/proto/autogenerated/client.md @@ -58,6 +58,11 @@ The nostr server will send back a message response, and inside the body there wi - This methods has an __empty__ __request__ body - This methods has an __empty__ __response__ body +- BumpTx + - auth type: __Admin__ + - input: [BumpTx](#BumpTx) + - This methods has an __empty__ __response__ body + - CloseChannel - auth type: __Admin__ - input: [CloseChannelRequest](#CloseChannelRequest) @@ -93,6 +98,11 @@ The nostr server will send back a message response, and inside the body there wi - input: [MessagingToken](#MessagingToken) - This methods has an __empty__ __response__ body +- GetAdminInvoiceSwapQuotes + - auth type: __Admin__ + - input: [InvoiceSwapRequest](#InvoiceSwapRequest) + - output: [InvoiceSwapQuoteList](#InvoiceSwapQuoteList) + - GetAdminTransactionSwapQuotes - auth type: __Admin__ - input: [TransactionSwapRequest](#TransactionSwapRequest) @@ -103,6 +113,11 @@ The nostr server will send back a message response, and inside the body there wi - input: [AppsMetricsRequest](#AppsMetricsRequest) - output: [AppsMetrics](#AppsMetrics) +- GetAssetsAndLiabilities + - auth type: __Admin__ + - input: [AssetsAndLiabilitiesReq](#AssetsAndLiabilitiesReq) + - output: [AssetsAndLiabilities](#AssetsAndLiabilities) + - GetBundleMetrics - auth type: __Metrics__ - input: [LatestBundleMetricReq](#LatestBundleMetricReq) @@ -243,20 +258,25 @@ The nostr server will send back a message response, and inside the body there wi - input: [LinkNPubThroughTokenRequest](#LinkNPubThroughTokenRequest) - This methods has an __empty__ __response__ body -- ListAdminSwaps +- ListAdminInvoiceSwaps - auth type: __Admin__ - This methods has an __empty__ __request__ body - - output: [SwapsList](#SwapsList) + - output: [InvoiceSwapsList](#InvoiceSwapsList) + +- ListAdminTxSwaps + - auth type: __Admin__ + - This methods has an __empty__ __request__ body + - output: [TxSwapsList](#TxSwapsList) - ListChannels - auth type: __Admin__ - This methods has an __empty__ __request__ body - output: [LndChannels](#LndChannels) -- ListSwaps +- ListTxSwaps - auth type: __User__ - This methods has an __empty__ __request__ body - - output: [SwapsList](#SwapsList) + - output: [TxSwapsList](#TxSwapsList) - LndGetInfo - auth type: __Admin__ @@ -290,10 +310,15 @@ The nostr server will send back a message response, and inside the body there wi - input: [PayAddressRequest](#PayAddressRequest) - output: [PayAddressResponse](#PayAddressResponse) +- PayAdminInvoiceSwap + - auth type: __Admin__ + - input: [PayAdminInvoiceSwapRequest](#PayAdminInvoiceSwapRequest) + - output: [AdminInvoiceSwapResponse](#AdminInvoiceSwapResponse) + - PayAdminTransactionSwap - auth type: __Admin__ - input: [PayAdminTransactionSwapRequest](#PayAdminTransactionSwapRequest) - - output: [AdminSwapResponse](#AdminSwapResponse) + - output: [AdminTxSwapResponse](#AdminTxSwapResponse) - PayInvoice - auth type: __User__ @@ -305,6 +330,11 @@ The nostr server will send back a message response, and inside the body there wi - This methods has an __empty__ __request__ body - This methods has an __empty__ __response__ body +- RefundAdminInvoiceSwap + - auth type: __Admin__ + - input: [RefundAdminInvoiceSwapRequest](#RefundAdminInvoiceSwapRequest) + - output: [AdminInvoiceSwapResponse](#AdminInvoiceSwapResponse) + - ResetDebit - auth type: __User__ - input: [DebitOperation](#DebitOperation) @@ -484,6 +514,13 @@ The nostr server will send back a message response, and inside the body there wi - This methods has an __empty__ __request__ body - This methods has an __empty__ __response__ body +- BumpTx + - auth type: __Admin__ + - http method: __post__ + - http route: __/api/admin/tx/bump__ + - input: [BumpTx](#BumpTx) + - This methods has an __empty__ __response__ body + - CloseChannel - auth type: __Admin__ - http method: __post__ @@ -540,6 +577,13 @@ The nostr server will send back a message response, and inside the body there wi - input: [MessagingToken](#MessagingToken) - This methods has an __empty__ __response__ body +- GetAdminInvoiceSwapQuotes + - auth type: __Admin__ + - http method: __post__ + - http route: __/api/admin/swap/invoice/quote__ + - input: [InvoiceSwapRequest](#InvoiceSwapRequest) + - output: [InvoiceSwapQuoteList](#InvoiceSwapQuoteList) + - GetAdminTransactionSwapQuotes - auth type: __Admin__ - http method: __post__ @@ -575,6 +619,13 @@ The nostr server will send back a message response, and inside the body there wi - input: [AppsMetricsRequest](#AppsMetricsRequest) - output: [AppsMetrics](#AppsMetrics) +- GetAssetsAndLiabilities + - auth type: __Admin__ + - http method: __post__ + - http route: __/api/admin/assets/liabilities__ + - input: [AssetsAndLiabilitiesReq](#AssetsAndLiabilitiesReq) + - output: [AssetsAndLiabilities](#AssetsAndLiabilities) + - GetBundleMetrics - auth type: __Metrics__ - http method: __post__ @@ -743,7 +794,7 @@ The nostr server will send back a message response, and inside the body there wi - GetTransactionSwapQuotes - auth type: __User__ - http method: __post__ - - http route: __/api/user/swap/quote__ + - http route: __/api/user/swap/transaction/quote__ - input: [TransactionSwapRequest](#TransactionSwapRequest) - output: [TransactionSwapQuoteList](#TransactionSwapQuoteList) @@ -834,12 +885,19 @@ The nostr server will send back a message response, and inside the body there wi - input: [LinkNPubThroughTokenRequest](#LinkNPubThroughTokenRequest) - This methods has an __empty__ __response__ body -- ListAdminSwaps +- ListAdminInvoiceSwaps - auth type: __Admin__ - http method: __post__ - - http route: __/api/admin/swap/list__ + - http route: __/api/admin/swap/invoice/list__ - This methods has an __empty__ __request__ body - - output: [SwapsList](#SwapsList) + - output: [InvoiceSwapsList](#InvoiceSwapsList) + +- ListAdminTxSwaps + - auth type: __Admin__ + - http method: __post__ + - http route: __/api/admin/swap/transaction/list__ + - This methods has an __empty__ __request__ body + - output: [TxSwapsList](#TxSwapsList) - ListChannels - auth type: __Admin__ @@ -848,12 +906,12 @@ The nostr server will send back a message response, and inside the body there wi - This methods has an __empty__ __request__ body - output: [LndChannels](#LndChannels) -- ListSwaps +- ListTxSwaps - auth type: __User__ - http method: __post__ - - http route: __/api/user/swap/list__ + - http route: __/api/user/swap/transaction/list__ - This methods has an __empty__ __request__ body - - output: [SwapsList](#SwapsList) + - output: [TxSwapsList](#TxSwapsList) - LndGetInfo - auth type: __Admin__ @@ -899,12 +957,19 @@ The nostr server will send back a message response, and inside the body there wi - input: [PayAddressRequest](#PayAddressRequest) - output: [PayAddressResponse](#PayAddressResponse) +- PayAdminInvoiceSwap + - auth type: __Admin__ + - http method: __post__ + - http route: __/api/admin/swap/invoice/pay__ + - input: [PayAdminInvoiceSwapRequest](#PayAdminInvoiceSwapRequest) + - output: [AdminInvoiceSwapResponse](#AdminInvoiceSwapResponse) + - PayAdminTransactionSwap - auth type: __Admin__ - http method: __post__ - http route: __/api/admin/swap/transaction/pay__ - input: [PayAdminTransactionSwapRequest](#PayAdminTransactionSwapRequest) - - output: [AdminSwapResponse](#AdminSwapResponse) + - output: [AdminTxSwapResponse](#AdminTxSwapResponse) - PayAppUserInvoice - auth type: __App__ @@ -927,6 +992,13 @@ The nostr server will send back a message response, and inside the body there wi - This methods has an __empty__ __request__ body - This methods has an __empty__ __response__ body +- RefundAdminInvoiceSwap + - auth type: __Admin__ + - http method: __post__ + - http route: __/api/admin/swap/invoice/refund__ + - input: [RefundAdminInvoiceSwapRequest](#RefundAdminInvoiceSwapRequest) + - output: [AdminInvoiceSwapResponse](#AdminInvoiceSwapResponse) + - RequestNPubLinkingToken - auth type: __App__ - http method: __post__ @@ -1098,7 +1170,10 @@ The nostr server will send back a message response, and inside the body there wi - __name__: _string_ - __price_sats__: _number_ -### AdminSwapResponse +### AdminInvoiceSwapResponse + - __tx_id__: _string_ + +### AdminTxSwapResponse - __network_fee__: _number_ - __tx_id__: _string_ @@ -1135,6 +1210,21 @@ The nostr server will send back a message response, and inside the body there wi - __include_operations__: _boolean_ *this field is optional - __to_unix__: _number_ *this field is optional +### AssetOperation + - __amount__: _number_ + - __tracked__: _[TrackedOperation](#TrackedOperation)_ *this field is optional + - __ts__: _number_ + +### AssetsAndLiabilities + - __liquidity_providers__: ARRAY of: _[LiquidityAssetProvider](#LiquidityAssetProvider)_ + - __lnds__: ARRAY of: _[LndAssetProvider](#LndAssetProvider)_ + - __users_balance__: _number_ + +### AssetsAndLiabilitiesReq + - __limit_invoices__: _number_ *this field is optional + - __limit_payments__: _number_ *this field is optional + - __limit_providers__: _number_ *this field is optional + ### AuthApp - __app__: _[Application](#Application)_ - __auth_token__: _string_ @@ -1163,6 +1253,11 @@ The nostr server will send back a message response, and inside the body there wi - __nextRelay__: _string_ *this field is optional - __type__: _string_ +### BumpTx + - __output_index__: _number_ + - __sat_per_vbyte__: _number_ + - __txid__: _string_ + ### BundleData - __available_chunks__: ARRAY of: _number_ - __base_64_data__: ARRAY of: _string_ @@ -1331,6 +1426,36 @@ The nostr server will send back a message response, and inside the body there wi - __token__: _string_ - __url__: _string_ +### InvoiceSwapOperation + - __completed_at_unix__: _number_ *this field is optional + - __failure_reason__: _string_ *this field is optional + - __operation_payment__: _[UserOperation](#UserOperation)_ *this field is optional + - __quote__: _[InvoiceSwapQuote](#InvoiceSwapQuote)_ + +### InvoiceSwapQuote + - __address__: _string_ + - __chain_fee_sats__: _number_ + - __expires_at_block_height__: _number_ + - __invoice__: _string_ + - __invoice_amount_sats__: _number_ + - __paid_at_unix__: _number_ + - __service_fee_sats__: _number_ + - __service_url__: _string_ + - __swap_fee_sats__: _number_ + - __swap_operation_id__: _string_ + - __transaction_amount_sats__: _number_ + - __tx_id__: _string_ + +### InvoiceSwapQuoteList + - __quotes__: ARRAY of: _[InvoiceSwapQuote](#InvoiceSwapQuote)_ + +### InvoiceSwapRequest + - __amount_sats__: _number_ + +### InvoiceSwapsList + - __current_block_height__: _number_ + - __swaps__: ARRAY of: _[InvoiceSwapOperation](#InvoiceSwapOperation)_ + ### LatestBundleMetricReq - __limit__: _number_ *this field is optional @@ -1340,6 +1465,10 @@ The nostr server will send back a message response, and inside the body there wi ### LinkNPubThroughTokenRequest - __token__: _string_ +### LiquidityAssetProvider + - __pubkey__: _string_ + - __tracked__: _[TrackedLiquidityProvider](#TrackedLiquidityProvider)_ *this field is optional + ### LiveDebitRequest - __debit__: _[LiveDebitRequest_debit](#LiveDebitRequest_debit)_ - __npub__: _string_ @@ -1353,6 +1482,10 @@ The nostr server will send back a message response, and inside the body there wi - __latest_balance__: _number_ - __operation__: _[UserOperation](#UserOperation)_ +### LndAssetProvider + - __pubkey__: _string_ + - __tracked__: _[TrackedLndProvider](#TrackedLndProvider)_ *this field is optional + ### LndChannels - __open_channels__: ARRAY of: _[OpenChannel](#OpenChannel)_ @@ -1534,6 +1667,11 @@ The nostr server will send back a message response, and inside the body there wi - __service_fee__: _number_ - __txId__: _string_ +### PayAdminInvoiceSwapRequest + - __no_claim__: _boolean_ *this field is optional + - __sat_per_v_byte__: _number_ + - __swap_operation_id__: _string_ + ### PayAdminTransactionSwapRequest - __address__: _string_ - __swap_operation_id__: _string_ @@ -1584,6 +1722,18 @@ The nostr server will send back a message response, and inside the body there wi ### ProvidersDisruption - __disruptions__: ARRAY of: _[ProviderDisruption](#ProviderDisruption)_ +### PushNotificationEnvelope + - __app_npub_hex__: _string_ + - __encrypted_payload__: _string_ + - __topic_id__: _string_ + +### PushNotificationPayload + - __data__: _[PushNotificationPayload_data](#PushNotificationPayload_data)_ + +### RefundAdminInvoiceSwapRequest + - __sat_per_v_byte__: _number_ + - __swap_operation_id__: _string_ + ### RelaysMigration - __relays__: ARRAY of: _string_ @@ -1640,19 +1790,31 @@ The nostr server will send back a message response, and inside the body there wi - __page__: _number_ - __request_id__: _number_ *this field is optional -### SwapOperation - - __address_paid__: _string_ - - __failure_reason__: _string_ *this field is optional - - __operation_payment__: _[UserOperation](#UserOperation)_ *this field is optional - - __swap_operation_id__: _string_ +### TrackedLiquidityProvider + - __balance__: _number_ + - __invoices__: ARRAY of: _[AssetOperation](#AssetOperation)_ + - __payments__: ARRAY of: _[AssetOperation](#AssetOperation)_ -### SwapsList - - __quotes__: ARRAY of: _[TransactionSwapQuote](#TransactionSwapQuote)_ - - __swaps__: ARRAY of: _[SwapOperation](#SwapOperation)_ +### TrackedLndProvider + - __channels_balance__: _number_ + - __confirmed_balance__: _number_ + - __incoming_tx__: ARRAY of: _[AssetOperation](#AssetOperation)_ + - __invoices__: ARRAY of: _[AssetOperation](#AssetOperation)_ + - __outgoing_tx__: ARRAY of: _[AssetOperation](#AssetOperation)_ + - __payments__: ARRAY of: _[AssetOperation](#AssetOperation)_ + - __unconfirmed_balance__: _number_ + +### TrackedOperation + - __amount__: _number_ + - __ts__: _number_ + - __type__: _[TrackedOperationType](#TrackedOperationType)_ ### TransactionSwapQuote - __chain_fee_sats__: _number_ + - __completed_at_unix__: _number_ + - __expires_at_block_height__: _number_ - __invoice_amount_sats__: _number_ + - __paid_at_unix__: _number_ - __service_fee_sats__: _number_ - __service_url__: _string_ - __swap_fee_sats__: _number_ @@ -1665,6 +1827,16 @@ The nostr server will send back a message response, and inside the body there wi ### TransactionSwapRequest - __transaction_amount_sats__: _number_ +### TxSwapOperation + - __address_paid__: _string_ *this field is optional + - __failure_reason__: _string_ *this field is optional + - __operation_payment__: _[UserOperation](#UserOperation)_ *this field is optional + - __quote__: _[TransactionSwapQuote](#TransactionSwapQuote)_ + - __tx_id__: _string_ *this field is optional + +### TxSwapsList + - __swaps__: ARRAY of: _[TxSwapOperation](#TxSwapOperation)_ + ### UpdateChannelPolicyRequest - __policy__: _[ChannelPolicy](#ChannelPolicy)_ - __update__: _[UpdateChannelPolicyRequest_update](#UpdateChannelPolicyRequest_update)_ @@ -1707,6 +1879,7 @@ The nostr server will send back a message response, and inside the body there wi - __nmanage__: _string_ - __noffer__: _string_ - __service_fee_bps__: _number_ + - __topic_id__: _string_ - __userId__: _string_ - __user_identifier__: _string_ @@ -1771,6 +1944,10 @@ The nostr server will send back a message response, and inside the body there wi - __BUNDLE_METRIC__ - __USAGE_METRIC__ +### TrackedOperationType + - __ROOT__ + - __USER__ + ### UserOperationType - __INCOMING_INVOICE__ - __INCOMING_TX__ diff --git a/proto/autogenerated/go/http_client.go b/proto/autogenerated/go/http_client.go index a2b0ecb0..43d60ce2 100644 --- a/proto/autogenerated/go/http_client.go +++ b/proto/autogenerated/go/http_client.go @@ -66,6 +66,7 @@ type Client struct { BanDebit func(req DebitOperation) error BanUser func(req BanUserRequest) (*BanUserResponse, error) // batching method: BatchUser not implemented + BumpTx func(req BumpTx) error CloseChannel func(req CloseChannelRequest) (*CloseChannelResponse, error) CreateOneTimeInviteLink func(req CreateOneTimeInviteLinkRequest) (*CreateOneTimeInviteLinkResponse, error) DecodeInvoice func(req DecodeInvoiceRequest) (*DecodeInvoiceResponse, error) @@ -74,11 +75,13 @@ type Client struct { EncryptionExchange func(req EncryptionExchangeRequest) error EnrollAdminToken func(req EnrollAdminTokenRequest) error EnrollMessagingToken func(req MessagingToken) error + GetAdminInvoiceSwapQuotes func(req InvoiceSwapRequest) (*InvoiceSwapQuoteList, error) GetAdminTransactionSwapQuotes func(req TransactionSwapRequest) (*TransactionSwapQuoteList, error) GetApp func() (*Application, error) GetAppUser func(req GetAppUserRequest) (*AppUser, error) GetAppUserLNURLInfo func(req GetAppUserLNURLInfoRequest) (*LnurlPayInfoResponse, error) GetAppsMetrics func(req AppsMetricsRequest) (*AppsMetrics, error) + GetAssetsAndLiabilities func(req AssetsAndLiabilitiesReq) (*AssetsAndLiabilities, error) GetBundleMetrics func(req LatestBundleMetricReq) (*BundleMetrics, error) GetDebitAuthorizations func() (*DebitAuthorizations, error) GetErrorStats func() (*ErrorStats, error) @@ -114,19 +117,22 @@ type Client struct { HandleLnurlWithdraw func(query HandleLnurlWithdraw_Query) error Health func() error LinkNPubThroughToken func(req LinkNPubThroughTokenRequest) error - ListAdminSwaps func() (*SwapsList, error) + ListAdminInvoiceSwaps func() (*InvoiceSwapsList, error) + ListAdminTxSwaps func() (*TxSwapsList, error) ListChannels func() (*LndChannels, error) - ListSwaps func() (*SwapsList, error) + ListTxSwaps func() (*TxSwapsList, error) LndGetInfo func(req LndGetInfoRequest) (*LndGetInfoResponse, error) NewAddress func(req NewAddressRequest) (*NewAddressResponse, error) NewInvoice func(req NewInvoiceRequest) (*NewInvoiceResponse, error) NewProductInvoice func(query NewProductInvoice_Query) (*NewInvoiceResponse, error) OpenChannel func(req OpenChannelRequest) (*OpenChannelResponse, error) PayAddress func(req PayAddressRequest) (*PayAddressResponse, error) - PayAdminTransactionSwap func(req PayAdminTransactionSwapRequest) (*AdminSwapResponse, error) + PayAdminInvoiceSwap func(req PayAdminInvoiceSwapRequest) (*AdminInvoiceSwapResponse, error) + PayAdminTransactionSwap func(req PayAdminTransactionSwapRequest) (*AdminTxSwapResponse, error) PayAppUserInvoice func(req PayAppUserInvoiceRequest) (*PayInvoiceResponse, error) PayInvoice func(req PayInvoiceRequest) (*PayInvoiceResponse, error) PingSubProcesses func() error + RefundAdminInvoiceSwap func(req RefundAdminInvoiceSwapRequest) (*AdminInvoiceSwapResponse, error) RequestNPubLinkingToken func(req RequestNPubLinkingTokenRequest) (*RequestNPubLinkingTokenResponse, error) ResetDebit func(req DebitOperation) error ResetManage func(req ManageOperation) error @@ -460,6 +466,30 @@ func NewClient(params ClientParams) *Client { return &res, nil }, // batching method: BatchUser not implemented + BumpTx: func(req BumpTx) error { + auth, err := params.RetrieveAdminAuth() + if err != nil { + return err + } + finalRoute := "/api/admin/tx/bump" + body, err := json.Marshal(req) + if err != nil { + return err + } + resBody, err := doPostRequest(params.BaseURL+finalRoute, body, auth) + if err != nil { + return err + } + result := ResultError{} + err = json.Unmarshal(resBody, &result) + if err != nil { + return err + } + if result.Status == "ERROR" { + return fmt.Errorf(result.Reason) + } + return nil + }, CloseChannel: func(req CloseChannelRequest) (*CloseChannelResponse, error) { auth, err := params.RetrieveAdminAuth() if err != nil { @@ -667,6 +697,35 @@ func NewClient(params ClientParams) *Client { } return nil }, + GetAdminInvoiceSwapQuotes: func(req InvoiceSwapRequest) (*InvoiceSwapQuoteList, error) { + auth, err := params.RetrieveAdminAuth() + if err != nil { + return nil, err + } + finalRoute := "/api/admin/swap/invoice/quote" + body, err := json.Marshal(req) + if err != nil { + return nil, err + } + resBody, err := doPostRequest(params.BaseURL+finalRoute, body, auth) + if err != nil { + return nil, err + } + result := ResultError{} + err = json.Unmarshal(resBody, &result) + if err != nil { + return nil, err + } + if result.Status == "ERROR" { + return nil, fmt.Errorf(result.Reason) + } + res := InvoiceSwapQuoteList{} + err = json.Unmarshal(resBody, &res) + if err != nil { + return nil, err + } + return &res, nil + }, GetAdminTransactionSwapQuotes: func(req TransactionSwapRequest) (*TransactionSwapQuoteList, error) { auth, err := params.RetrieveAdminAuth() if err != nil { @@ -809,6 +868,35 @@ func NewClient(params ClientParams) *Client { } return &res, nil }, + GetAssetsAndLiabilities: func(req AssetsAndLiabilitiesReq) (*AssetsAndLiabilities, error) { + auth, err := params.RetrieveAdminAuth() + if err != nil { + return nil, err + } + finalRoute := "/api/admin/assets/liabilities" + body, err := json.Marshal(req) + if err != nil { + return nil, err + } + resBody, err := doPostRequest(params.BaseURL+finalRoute, body, auth) + if err != nil { + return nil, err + } + result := ResultError{} + err = json.Unmarshal(resBody, &result) + if err != nil { + return nil, err + } + if result.Status == "ERROR" { + return nil, fmt.Errorf(result.Reason) + } + res := AssetsAndLiabilities{} + err = json.Unmarshal(resBody, &res) + if err != nil { + return nil, err + } + return &res, nil + }, GetBundleMetrics: func(req LatestBundleMetricReq) (*BundleMetrics, error) { auth, err := params.RetrieveMetricsAuth() if err != nil { @@ -1324,7 +1412,7 @@ func NewClient(params ClientParams) *Client { if err != nil { return nil, err } - finalRoute := "/api/user/swap/quote" + finalRoute := "/api/user/swap/transaction/quote" body, err := json.Marshal(req) if err != nil { return nil, err @@ -1643,12 +1731,12 @@ func NewClient(params ClientParams) *Client { } return nil }, - ListAdminSwaps: func() (*SwapsList, error) { + ListAdminInvoiceSwaps: func() (*InvoiceSwapsList, error) { auth, err := params.RetrieveAdminAuth() if err != nil { return nil, err } - finalRoute := "/api/admin/swap/list" + finalRoute := "/api/admin/swap/invoice/list" body := []byte{} resBody, err := doPostRequest(params.BaseURL+finalRoute, body, auth) if err != nil { @@ -1662,7 +1750,33 @@ func NewClient(params ClientParams) *Client { if result.Status == "ERROR" { return nil, fmt.Errorf(result.Reason) } - res := SwapsList{} + res := InvoiceSwapsList{} + err = json.Unmarshal(resBody, &res) + if err != nil { + return nil, err + } + return &res, nil + }, + ListAdminTxSwaps: func() (*TxSwapsList, error) { + auth, err := params.RetrieveAdminAuth() + if err != nil { + return nil, err + } + finalRoute := "/api/admin/swap/transaction/list" + body := []byte{} + resBody, err := doPostRequest(params.BaseURL+finalRoute, body, auth) + if err != nil { + return nil, err + } + result := ResultError{} + err = json.Unmarshal(resBody, &result) + if err != nil { + return nil, err + } + if result.Status == "ERROR" { + return nil, fmt.Errorf(result.Reason) + } + res := TxSwapsList{} err = json.Unmarshal(resBody, &res) if err != nil { return nil, err @@ -1691,12 +1805,12 @@ func NewClient(params ClientParams) *Client { } return &res, nil }, - ListSwaps: func() (*SwapsList, error) { + ListTxSwaps: func() (*TxSwapsList, error) { auth, err := params.RetrieveUserAuth() if err != nil { return nil, err } - finalRoute := "/api/user/swap/list" + finalRoute := "/api/user/swap/transaction/list" body := []byte{} resBody, err := doPostRequest(params.BaseURL+finalRoute, body, auth) if err != nil { @@ -1710,7 +1824,7 @@ func NewClient(params ClientParams) *Client { if result.Status == "ERROR" { return nil, fmt.Errorf(result.Reason) } - res := SwapsList{} + res := TxSwapsList{} err = json.Unmarshal(resBody, &res) if err != nil { return nil, err @@ -1892,7 +2006,36 @@ func NewClient(params ClientParams) *Client { } return &res, nil }, - PayAdminTransactionSwap: func(req PayAdminTransactionSwapRequest) (*AdminSwapResponse, error) { + PayAdminInvoiceSwap: func(req PayAdminInvoiceSwapRequest) (*AdminInvoiceSwapResponse, error) { + auth, err := params.RetrieveAdminAuth() + if err != nil { + return nil, err + } + finalRoute := "/api/admin/swap/invoice/pay" + body, err := json.Marshal(req) + if err != nil { + return nil, err + } + resBody, err := doPostRequest(params.BaseURL+finalRoute, body, auth) + if err != nil { + return nil, err + } + result := ResultError{} + err = json.Unmarshal(resBody, &result) + if err != nil { + return nil, err + } + if result.Status == "ERROR" { + return nil, fmt.Errorf(result.Reason) + } + res := AdminInvoiceSwapResponse{} + err = json.Unmarshal(resBody, &res) + if err != nil { + return nil, err + } + return &res, nil + }, + PayAdminTransactionSwap: func(req PayAdminTransactionSwapRequest) (*AdminTxSwapResponse, error) { auth, err := params.RetrieveAdminAuth() if err != nil { return nil, err @@ -1914,7 +2057,7 @@ func NewClient(params ClientParams) *Client { if result.Status == "ERROR" { return nil, fmt.Errorf(result.Reason) } - res := AdminSwapResponse{} + res := AdminTxSwapResponse{} err = json.Unmarshal(resBody, &res) if err != nil { return nil, err @@ -2000,6 +2143,35 @@ func NewClient(params ClientParams) *Client { } return nil }, + RefundAdminInvoiceSwap: func(req RefundAdminInvoiceSwapRequest) (*AdminInvoiceSwapResponse, error) { + auth, err := params.RetrieveAdminAuth() + if err != nil { + return nil, err + } + finalRoute := "/api/admin/swap/invoice/refund" + body, err := json.Marshal(req) + if err != nil { + return nil, err + } + resBody, err := doPostRequest(params.BaseURL+finalRoute, body, auth) + if err != nil { + return nil, err + } + result := ResultError{} + err = json.Unmarshal(resBody, &result) + if err != nil { + return nil, err + } + if result.Status == "ERROR" { + return nil, fmt.Errorf(result.Reason) + } + res := AdminInvoiceSwapResponse{} + err = json.Unmarshal(resBody, &res) + if err != nil { + return nil, err + } + return &res, nil + }, RequestNPubLinkingToken: func(req RequestNPubLinkingTokenRequest) (*RequestNPubLinkingTokenResponse, error) { auth, err := params.RetrieveAppAuth() if err != nil { diff --git a/proto/autogenerated/go/types.go b/proto/autogenerated/go/types.go index 841ced1d..cff46783 100644 --- a/proto/autogenerated/go/types.go +++ b/proto/autogenerated/go/types.go @@ -79,6 +79,13 @@ const ( USAGE_METRIC SingleMetricType = "USAGE_METRIC" ) +type TrackedOperationType string + +const ( + ROOT TrackedOperationType = "ROOT" + USER TrackedOperationType = "USER" +) + type UserOperationType string const ( @@ -123,7 +130,10 @@ type AddProductRequest struct { Name string `json:"name"` Price_sats int64 `json:"price_sats"` } -type AdminSwapResponse struct { +type AdminInvoiceSwapResponse struct { + Tx_id string `json:"tx_id"` +} +type AdminTxSwapResponse struct { Network_fee int64 `json:"network_fee"` Tx_id string `json:"tx_id"` } @@ -160,6 +170,21 @@ type AppsMetricsRequest struct { Include_operations bool `json:"include_operations"` To_unix int64 `json:"to_unix"` } +type AssetOperation struct { + Amount int64 `json:"amount"` + Tracked *TrackedOperation `json:"tracked"` + Ts int64 `json:"ts"` +} +type AssetsAndLiabilities struct { + Liquidity_providers []LiquidityAssetProvider `json:"liquidity_providers"` + Lnds []LndAssetProvider `json:"lnds"` + Users_balance int64 `json:"users_balance"` +} +type AssetsAndLiabilitiesReq struct { + Limit_invoices int64 `json:"limit_invoices"` + Limit_payments int64 `json:"limit_payments"` + Limit_providers int64 `json:"limit_providers"` +} type AuthApp struct { App *Application `json:"app"` Auth_token string `json:"auth_token"` @@ -188,6 +213,11 @@ type BeaconData struct { Nextrelay string `json:"nextRelay"` Type string `json:"type"` } +type BumpTx struct { + Output_index int64 `json:"output_index"` + Sat_per_vbyte int64 `json:"sat_per_vbyte"` + Txid string `json:"txid"` +} type BundleData struct { Available_chunks []int64 `json:"available_chunks"` Base_64_data []string `json:"base_64_data"` @@ -356,6 +386,36 @@ type HttpCreds struct { Token string `json:"token"` Url string `json:"url"` } +type InvoiceSwapOperation struct { + Completed_at_unix int64 `json:"completed_at_unix"` + Failure_reason string `json:"failure_reason"` + Operation_payment *UserOperation `json:"operation_payment"` + Quote *InvoiceSwapQuote `json:"quote"` +} +type InvoiceSwapQuote struct { + Address string `json:"address"` + Chain_fee_sats int64 `json:"chain_fee_sats"` + Expires_at_block_height int64 `json:"expires_at_block_height"` + Invoice string `json:"invoice"` + Invoice_amount_sats int64 `json:"invoice_amount_sats"` + Paid_at_unix int64 `json:"paid_at_unix"` + Service_fee_sats int64 `json:"service_fee_sats"` + Service_url string `json:"service_url"` + Swap_fee_sats int64 `json:"swap_fee_sats"` + Swap_operation_id string `json:"swap_operation_id"` + Transaction_amount_sats int64 `json:"transaction_amount_sats"` + Tx_id string `json:"tx_id"` +} +type InvoiceSwapQuoteList struct { + Quotes []InvoiceSwapQuote `json:"quotes"` +} +type InvoiceSwapRequest struct { + Amount_sats int64 `json:"amount_sats"` +} +type InvoiceSwapsList struct { + Current_block_height int64 `json:"current_block_height"` + Swaps []InvoiceSwapOperation `json:"swaps"` +} type LatestBundleMetricReq struct { Limit int64 `json:"limit"` } @@ -365,6 +425,10 @@ type LatestUsageMetricReq struct { type LinkNPubThroughTokenRequest struct { Token string `json:"token"` } +type LiquidityAssetProvider struct { + Pubkey string `json:"pubkey"` + Tracked *TrackedLiquidityProvider `json:"tracked"` +} type LiveDebitRequest struct { Debit *LiveDebitRequest_debit `json:"debit"` Npub string `json:"npub"` @@ -378,6 +442,10 @@ type LiveUserOperation struct { Latest_balance int64 `json:"latest_balance"` Operation *UserOperation `json:"operation"` } +type LndAssetProvider struct { + Pubkey string `json:"pubkey"` + Tracked *TrackedLndProvider `json:"tracked"` +} type LndChannels struct { Open_channels []OpenChannel `json:"open_channels"` } @@ -559,6 +627,11 @@ type PayAddressResponse struct { Service_fee int64 `json:"service_fee"` Txid string `json:"txId"` } +type PayAdminInvoiceSwapRequest struct { + No_claim bool `json:"no_claim"` + Sat_per_v_byte int64 `json:"sat_per_v_byte"` + Swap_operation_id string `json:"swap_operation_id"` +} type PayAdminTransactionSwapRequest struct { Address string `json:"address"` Swap_operation_id string `json:"swap_operation_id"` @@ -609,6 +682,18 @@ type ProviderDisruption struct { type ProvidersDisruption struct { Disruptions []ProviderDisruption `json:"disruptions"` } +type PushNotificationEnvelope struct { + App_npub_hex string `json:"app_npub_hex"` + Encrypted_payload string `json:"encrypted_payload"` + Topic_id string `json:"topic_id"` +} +type PushNotificationPayload struct { + Data *PushNotificationPayload_data `json:"data"` +} +type RefundAdminInvoiceSwapRequest struct { + Sat_per_v_byte int64 `json:"sat_per_v_byte"` + Swap_operation_id string `json:"swap_operation_id"` +} type RelaysMigration struct { Relays []string `json:"relays"` } @@ -665,19 +750,31 @@ type SingleMetricReq struct { Page int64 `json:"page"` Request_id int64 `json:"request_id"` } -type SwapOperation struct { - Address_paid string `json:"address_paid"` - Failure_reason string `json:"failure_reason"` - Operation_payment *UserOperation `json:"operation_payment"` - Swap_operation_id string `json:"swap_operation_id"` +type TrackedLiquidityProvider struct { + Balance int64 `json:"balance"` + Invoices []AssetOperation `json:"invoices"` + Payments []AssetOperation `json:"payments"` } -type SwapsList struct { - Quotes []TransactionSwapQuote `json:"quotes"` - Swaps []SwapOperation `json:"swaps"` +type TrackedLndProvider struct { + Channels_balance int64 `json:"channels_balance"` + Confirmed_balance int64 `json:"confirmed_balance"` + Incoming_tx []AssetOperation `json:"incoming_tx"` + Invoices []AssetOperation `json:"invoices"` + Outgoing_tx []AssetOperation `json:"outgoing_tx"` + Payments []AssetOperation `json:"payments"` + Unconfirmed_balance int64 `json:"unconfirmed_balance"` +} +type TrackedOperation struct { + Amount int64 `json:"amount"` + Ts int64 `json:"ts"` + Type TrackedOperationType `json:"type"` } type TransactionSwapQuote struct { Chain_fee_sats int64 `json:"chain_fee_sats"` + Completed_at_unix int64 `json:"completed_at_unix"` + Expires_at_block_height int64 `json:"expires_at_block_height"` Invoice_amount_sats int64 `json:"invoice_amount_sats"` + Paid_at_unix int64 `json:"paid_at_unix"` Service_fee_sats int64 `json:"service_fee_sats"` Service_url string `json:"service_url"` Swap_fee_sats int64 `json:"swap_fee_sats"` @@ -690,6 +787,16 @@ type TransactionSwapQuoteList struct { type TransactionSwapRequest struct { Transaction_amount_sats int64 `json:"transaction_amount_sats"` } +type TxSwapOperation struct { + Address_paid string `json:"address_paid"` + Failure_reason string `json:"failure_reason"` + Operation_payment *UserOperation `json:"operation_payment"` + Quote *TransactionSwapQuote `json:"quote"` + Tx_id string `json:"tx_id"` +} +type TxSwapsList struct { + Swaps []TxSwapOperation `json:"swaps"` +} type UpdateChannelPolicyRequest struct { Policy *ChannelPolicy `json:"policy"` Update *UpdateChannelPolicyRequest_update `json:"update"` @@ -732,6 +839,7 @@ type UserInfo struct { Nmanage string `json:"nmanage"` Noffer string `json:"noffer"` Service_fee_bps int64 `json:"service_fee_bps"` + Topic_id string `json:"topic_id"` Userid string `json:"userId"` User_identifier string `json:"user_identifier"` } @@ -830,6 +938,18 @@ type NPubLinking_state struct { Linking_token *string `json:"linking_token"` Unlinked *Empty `json:"unlinked"` } +type PushNotificationPayload_data_type string + +const ( + RECEIVED_OPERATION PushNotificationPayload_data_type = "received_operation" + SENT_OPERATION PushNotificationPayload_data_type = "sent_operation" +) + +type PushNotificationPayload_data struct { + Type PushNotificationPayload_data_type `json:"type"` + Received_operation *UserOperation `json:"received_operation"` + Sent_operation *UserOperation `json:"sent_operation"` +} type UpdateChannelPolicyRequest_update_type string const ( diff --git a/proto/autogenerated/ts/express_server.ts b/proto/autogenerated/ts/express_server.ts index c1740acd..aa809166 100644 --- a/proto/autogenerated/ts/express_server.ts +++ b/proto/autogenerated/ts/express_server.ts @@ -545,12 +545,12 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => { callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) } break - case 'ListSwaps': - if (!methods.ListSwaps) { - throw new Error('method ListSwaps not found' ) + case 'ListTxSwaps': + if (!methods.ListTxSwaps) { + throw new Error('method ListTxSwaps not found' ) } else { opStats.validate = opStats.guard - const res = await methods.ListSwaps({...operation, ctx}); responses.push({ status: 'OK', ...res }) + const res = await methods.ListTxSwaps({...operation, ctx}); responses.push({ status: 'OK', ...res }) opStats.handle = process.hrtime.bigint() callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) } @@ -693,6 +693,28 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => { opts.metricsCallback([{ ...info, ...stats, ...ctx }, ...callsMetrics]) } catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } }) + if (!opts.allowNotImplementedMethods && !methods.BumpTx) throw new Error('method: BumpTx is not implemented') + app.post('/api/admin/tx/bump', async (req, res) => { + const info: Types.RequestInfo = { rpcName: 'BumpTx', batch: false, nostr: false, batchSize: 0} + const stats: Types.RequestStats = { startMs:req.startTimeMs || 0, start:req.startTime || 0n, parse: process.hrtime.bigint(), guard: 0n, validate: 0n, handle: 0n } + let authCtx: Types.AuthContext = {} + try { + if (!methods.BumpTx) throw new Error('method: BumpTx is not implemented') + const authContext = await opts.AdminAuthGuard(req.headers['authorization']) + authCtx = authContext + stats.guard = process.hrtime.bigint() + const request = req.body + const error = Types.BumpTxValidate(request) + stats.validate = process.hrtime.bigint() + if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authContext }, opts.metricsCallback) + const query = req.query + const params = req.params + await methods.BumpTx({rpcName:'BumpTx', ctx:authContext , req: request}) + stats.handle = process.hrtime.bigint() + res.json({status: 'OK'}) + opts.metricsCallback([{ ...info, ...stats, ...authContext }]) + } catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } + }) if (!opts.allowNotImplementedMethods && !methods.CloseChannel) throw new Error('method: CloseChannel is not implemented') app.post('/api/admin/channel/close', async (req, res) => { const info: Types.RequestInfo = { rpcName: 'CloseChannel', batch: false, nostr: false, batchSize: 0} @@ -869,6 +891,28 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => { opts.metricsCallback([{ ...info, ...stats, ...authContext }]) } catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } }) + if (!opts.allowNotImplementedMethods && !methods.GetAdminInvoiceSwapQuotes) throw new Error('method: GetAdminInvoiceSwapQuotes is not implemented') + app.post('/api/admin/swap/invoice/quote', async (req, res) => { + const info: Types.RequestInfo = { rpcName: 'GetAdminInvoiceSwapQuotes', batch: false, nostr: false, batchSize: 0} + const stats: Types.RequestStats = { startMs:req.startTimeMs || 0, start:req.startTime || 0n, parse: process.hrtime.bigint(), guard: 0n, validate: 0n, handle: 0n } + let authCtx: Types.AuthContext = {} + try { + if (!methods.GetAdminInvoiceSwapQuotes) throw new Error('method: GetAdminInvoiceSwapQuotes is not implemented') + const authContext = await opts.AdminAuthGuard(req.headers['authorization']) + authCtx = authContext + stats.guard = process.hrtime.bigint() + const request = req.body + const error = Types.InvoiceSwapRequestValidate(request) + stats.validate = process.hrtime.bigint() + if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authContext }, opts.metricsCallback) + const query = req.query + const params = req.params + const response = await methods.GetAdminInvoiceSwapQuotes({rpcName:'GetAdminInvoiceSwapQuotes', ctx:authContext , req: request}) + stats.handle = process.hrtime.bigint() + res.json({status: 'OK', ...response}) + opts.metricsCallback([{ ...info, ...stats, ...authContext }]) + } catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } + }) if (!opts.allowNotImplementedMethods && !methods.GetAdminTransactionSwapQuotes) throw new Error('method: GetAdminTransactionSwapQuotes is not implemented') app.post('/api/admin/swap/transaction/quote', async (req, res) => { const info: Types.RequestInfo = { rpcName: 'GetAdminTransactionSwapQuotes', batch: false, nostr: false, batchSize: 0} @@ -976,6 +1020,28 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => { opts.metricsCallback([{ ...info, ...stats, ...authContext }]) } catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } }) + if (!opts.allowNotImplementedMethods && !methods.GetAssetsAndLiabilities) throw new Error('method: GetAssetsAndLiabilities is not implemented') + app.post('/api/admin/assets/liabilities', async (req, res) => { + const info: Types.RequestInfo = { rpcName: 'GetAssetsAndLiabilities', batch: false, nostr: false, batchSize: 0} + const stats: Types.RequestStats = { startMs:req.startTimeMs || 0, start:req.startTime || 0n, parse: process.hrtime.bigint(), guard: 0n, validate: 0n, handle: 0n } + let authCtx: Types.AuthContext = {} + try { + if (!methods.GetAssetsAndLiabilities) throw new Error('method: GetAssetsAndLiabilities is not implemented') + const authContext = await opts.AdminAuthGuard(req.headers['authorization']) + authCtx = authContext + stats.guard = process.hrtime.bigint() + const request = req.body + const error = Types.AssetsAndLiabilitiesReqValidate(request) + stats.validate = process.hrtime.bigint() + if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authContext }, opts.metricsCallback) + const query = req.query + const params = req.params + const response = await methods.GetAssetsAndLiabilities({rpcName:'GetAssetsAndLiabilities', ctx:authContext , req: request}) + stats.handle = process.hrtime.bigint() + res.json({status: 'OK', ...response}) + opts.metricsCallback([{ ...info, ...stats, ...authContext }]) + } catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } + }) if (!opts.allowNotImplementedMethods && !methods.GetBundleMetrics) throw new Error('method: GetBundleMetrics is not implemented') app.post('/api/reports/bundle', async (req, res) => { const info: Types.RequestInfo = { rpcName: 'GetBundleMetrics', batch: false, nostr: false, batchSize: 0} @@ -1362,7 +1428,7 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => { } catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } }) if (!opts.allowNotImplementedMethods && !methods.GetTransactionSwapQuotes) throw new Error('method: GetTransactionSwapQuotes is not implemented') - app.post('/api/user/swap/quote', async (req, res) => { + app.post('/api/user/swap/transaction/quote', async (req, res) => { const info: Types.RequestInfo = { rpcName: 'GetTransactionSwapQuotes', batch: false, nostr: false, batchSize: 0} const stats: Types.RequestStats = { startMs:req.startTimeMs || 0, start:req.startTime || 0n, parse: process.hrtime.bigint(), guard: 0n, validate: 0n, handle: 0n } let authCtx: Types.AuthContext = {} @@ -1607,20 +1673,39 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => { opts.metricsCallback([{ ...info, ...stats, ...authContext }]) } catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } }) - if (!opts.allowNotImplementedMethods && !methods.ListAdminSwaps) throw new Error('method: ListAdminSwaps is not implemented') - app.post('/api/admin/swap/list', async (req, res) => { - const info: Types.RequestInfo = { rpcName: 'ListAdminSwaps', batch: false, nostr: false, batchSize: 0} + if (!opts.allowNotImplementedMethods && !methods.ListAdminInvoiceSwaps) throw new Error('method: ListAdminInvoiceSwaps is not implemented') + app.post('/api/admin/swap/invoice/list', async (req, res) => { + const info: Types.RequestInfo = { rpcName: 'ListAdminInvoiceSwaps', batch: false, nostr: false, batchSize: 0} const stats: Types.RequestStats = { startMs:req.startTimeMs || 0, start:req.startTime || 0n, parse: process.hrtime.bigint(), guard: 0n, validate: 0n, handle: 0n } let authCtx: Types.AuthContext = {} try { - if (!methods.ListAdminSwaps) throw new Error('method: ListAdminSwaps is not implemented') + if (!methods.ListAdminInvoiceSwaps) throw new Error('method: ListAdminInvoiceSwaps is not implemented') const authContext = await opts.AdminAuthGuard(req.headers['authorization']) authCtx = authContext stats.guard = process.hrtime.bigint() stats.validate = stats.guard const query = req.query const params = req.params - const response = await methods.ListAdminSwaps({rpcName:'ListAdminSwaps', ctx:authContext }) + const response = await methods.ListAdminInvoiceSwaps({rpcName:'ListAdminInvoiceSwaps', ctx:authContext }) + stats.handle = process.hrtime.bigint() + res.json({status: 'OK', ...response}) + opts.metricsCallback([{ ...info, ...stats, ...authContext }]) + } catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } + }) + if (!opts.allowNotImplementedMethods && !methods.ListAdminTxSwaps) throw new Error('method: ListAdminTxSwaps is not implemented') + app.post('/api/admin/swap/transaction/list', async (req, res) => { + const info: Types.RequestInfo = { rpcName: 'ListAdminTxSwaps', batch: false, nostr: false, batchSize: 0} + const stats: Types.RequestStats = { startMs:req.startTimeMs || 0, start:req.startTime || 0n, parse: process.hrtime.bigint(), guard: 0n, validate: 0n, handle: 0n } + let authCtx: Types.AuthContext = {} + try { + if (!methods.ListAdminTxSwaps) throw new Error('method: ListAdminTxSwaps is not implemented') + const authContext = await opts.AdminAuthGuard(req.headers['authorization']) + authCtx = authContext + stats.guard = process.hrtime.bigint() + stats.validate = stats.guard + const query = req.query + const params = req.params + const response = await methods.ListAdminTxSwaps({rpcName:'ListAdminTxSwaps', ctx:authContext }) stats.handle = process.hrtime.bigint() res.json({status: 'OK', ...response}) opts.metricsCallback([{ ...info, ...stats, ...authContext }]) @@ -1645,20 +1730,20 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => { opts.metricsCallback([{ ...info, ...stats, ...authContext }]) } catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } }) - if (!opts.allowNotImplementedMethods && !methods.ListSwaps) throw new Error('method: ListSwaps is not implemented') - app.post('/api/user/swap/list', async (req, res) => { - const info: Types.RequestInfo = { rpcName: 'ListSwaps', batch: false, nostr: false, batchSize: 0} + if (!opts.allowNotImplementedMethods && !methods.ListTxSwaps) throw new Error('method: ListTxSwaps is not implemented') + app.post('/api/user/swap/transaction/list', async (req, res) => { + const info: Types.RequestInfo = { rpcName: 'ListTxSwaps', batch: false, nostr: false, batchSize: 0} const stats: Types.RequestStats = { startMs:req.startTimeMs || 0, start:req.startTime || 0n, parse: process.hrtime.bigint(), guard: 0n, validate: 0n, handle: 0n } let authCtx: Types.AuthContext = {} try { - if (!methods.ListSwaps) throw new Error('method: ListSwaps is not implemented') + if (!methods.ListTxSwaps) throw new Error('method: ListTxSwaps is not implemented') const authContext = await opts.UserAuthGuard(req.headers['authorization']) authCtx = authContext stats.guard = process.hrtime.bigint() stats.validate = stats.guard const query = req.query const params = req.params - const response = await methods.ListSwaps({rpcName:'ListSwaps', ctx:authContext }) + const response = await methods.ListTxSwaps({rpcName:'ListTxSwaps', ctx:authContext }) stats.handle = process.hrtime.bigint() res.json({status: 'OK', ...response}) opts.metricsCallback([{ ...info, ...stats, ...authContext }]) @@ -1793,6 +1878,28 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => { opts.metricsCallback([{ ...info, ...stats, ...authContext }]) } catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } }) + if (!opts.allowNotImplementedMethods && !methods.PayAdminInvoiceSwap) throw new Error('method: PayAdminInvoiceSwap is not implemented') + app.post('/api/admin/swap/invoice/pay', async (req, res) => { + const info: Types.RequestInfo = { rpcName: 'PayAdminInvoiceSwap', batch: false, nostr: false, batchSize: 0} + const stats: Types.RequestStats = { startMs:req.startTimeMs || 0, start:req.startTime || 0n, parse: process.hrtime.bigint(), guard: 0n, validate: 0n, handle: 0n } + let authCtx: Types.AuthContext = {} + try { + if (!methods.PayAdminInvoiceSwap) throw new Error('method: PayAdminInvoiceSwap is not implemented') + const authContext = await opts.AdminAuthGuard(req.headers['authorization']) + authCtx = authContext + stats.guard = process.hrtime.bigint() + const request = req.body + const error = Types.PayAdminInvoiceSwapRequestValidate(request) + stats.validate = process.hrtime.bigint() + if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authContext }, opts.metricsCallback) + const query = req.query + const params = req.params + const response = await methods.PayAdminInvoiceSwap({rpcName:'PayAdminInvoiceSwap', ctx:authContext , req: request}) + stats.handle = process.hrtime.bigint() + res.json({status: 'OK', ...response}) + opts.metricsCallback([{ ...info, ...stats, ...authContext }]) + } catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } + }) if (!opts.allowNotImplementedMethods && !methods.PayAdminTransactionSwap) throw new Error('method: PayAdminTransactionSwap is not implemented') app.post('/api/admin/swap/transaction/pay', async (req, res) => { const info: Types.RequestInfo = { rpcName: 'PayAdminTransactionSwap', batch: false, nostr: false, batchSize: 0} @@ -1878,6 +1985,28 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => { opts.metricsCallback([{ ...info, ...stats, ...authContext }]) } catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } }) + if (!opts.allowNotImplementedMethods && !methods.RefundAdminInvoiceSwap) throw new Error('method: RefundAdminInvoiceSwap is not implemented') + app.post('/api/admin/swap/invoice/refund', async (req, res) => { + const info: Types.RequestInfo = { rpcName: 'RefundAdminInvoiceSwap', batch: false, nostr: false, batchSize: 0} + const stats: Types.RequestStats = { startMs:req.startTimeMs || 0, start:req.startTime || 0n, parse: process.hrtime.bigint(), guard: 0n, validate: 0n, handle: 0n } + let authCtx: Types.AuthContext = {} + try { + if (!methods.RefundAdminInvoiceSwap) throw new Error('method: RefundAdminInvoiceSwap is not implemented') + const authContext = await opts.AdminAuthGuard(req.headers['authorization']) + authCtx = authContext + stats.guard = process.hrtime.bigint() + const request = req.body + const error = Types.RefundAdminInvoiceSwapRequestValidate(request) + stats.validate = process.hrtime.bigint() + if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authContext }, opts.metricsCallback) + const query = req.query + const params = req.params + const response = await methods.RefundAdminInvoiceSwap({rpcName:'RefundAdminInvoiceSwap', ctx:authContext , req: request}) + stats.handle = process.hrtime.bigint() + res.json({status: 'OK', ...response}) + opts.metricsCallback([{ ...info, ...stats, ...authContext }]) + } catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } + }) if (!opts.allowNotImplementedMethods && !methods.RequestNPubLinkingToken) throw new Error('method: RequestNPubLinkingToken is not implemented') app.post('/api/app/user/npub/token', async (req, res) => { const info: Types.RequestInfo = { rpcName: 'RequestNPubLinkingToken', batch: false, nostr: false, batchSize: 0} diff --git a/proto/autogenerated/ts/http_client.ts b/proto/autogenerated/ts/http_client.ts index a24ff3e6..21597e2c 100644 --- a/proto/autogenerated/ts/http_client.ts +++ b/proto/autogenerated/ts/http_client.ts @@ -176,6 +176,17 @@ export default (params: ClientParams) => ({ } return { status: 'ERROR', reason: 'invalid response' } }, + BumpTx: async (request: Types.BumpTx): Promise => { + 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 => { const auth = await params.retrieveAdminAuth() if (auth === null) throw new Error('retrieveAdminAuth() returned null') @@ -273,6 +284,20 @@ export default (params: ClientParams) => ({ } return { status: 'ERROR', reason: 'invalid response' } }, + GetAdminInvoiceSwapQuotes: async (request: Types.InvoiceSwapRequest): Promise => { + 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 => { const auth = await params.retrieveAdminAuth() if (auth === null) throw new Error('retrieveAdminAuth() returned null') @@ -343,6 +368,20 @@ export default (params: ClientParams) => ({ } return { status: 'ERROR', reason: 'invalid response' } }, + GetAssetsAndLiabilities: async (request: Types.AssetsAndLiabilitiesReq): Promise => { + 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 => { const auth = await params.retrieveMetricsAuth() if (auth === null) throw new Error('retrieveMetricsAuth() returned null') @@ -620,7 +659,7 @@ export default (params: ClientParams) => ({ GetTransactionSwapQuotes: async (request: Types.TransactionSwapRequest): Promise => { const auth = await params.retrieveUserAuth() if (auth === null) throw new Error('retrieveUserAuth() returned null') - let finalRoute = '/api/user/swap/quote' + let finalRoute = '/api/user/swap/transaction/quote' const { data } = await axios.post(params.baseUrl + finalRoute, request, { headers: { 'authorization': auth } }) if (data.status === 'ERROR' && typeof data.reason === 'string') return data if (data.status === 'OK') { @@ -781,16 +820,30 @@ export default (params: ClientParams) => ({ } return { status: 'ERROR', reason: 'invalid response' } }, - ListAdminSwaps: async (): Promise => { + ListAdminInvoiceSwaps: async (): Promise => { const auth = await params.retrieveAdminAuth() if (auth === null) throw new Error('retrieveAdminAuth() returned null') - let finalRoute = '/api/admin/swap/list' + let finalRoute = '/api/admin/swap/invoice/list' const { data } = await axios.post(params.baseUrl + finalRoute, {}, { headers: { 'authorization': auth } }) if (data.status === 'ERROR' && typeof data.reason === 'string') return data if (data.status === 'OK') { const result = data if(!params.checkResult) return { status: 'OK', ...result } - const error = Types.SwapsListValidate(result) + const error = Types.InvoiceSwapsListValidate(result) + if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } + } + return { status: 'ERROR', reason: 'invalid response' } + }, + ListAdminTxSwaps: async (): Promise => { + const auth = await params.retrieveAdminAuth() + if (auth === null) throw new Error('retrieveAdminAuth() returned null') + let finalRoute = '/api/admin/swap/transaction/list' + const { data } = await axios.post(params.baseUrl + finalRoute, {}, { headers: { 'authorization': auth } }) + if (data.status === 'ERROR' && typeof data.reason === 'string') return data + if (data.status === 'OK') { + const result = data + if(!params.checkResult) return { status: 'OK', ...result } + const error = Types.TxSwapsListValidate(result) if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } } return { status: 'ERROR', reason: 'invalid response' } @@ -809,16 +862,16 @@ export default (params: ClientParams) => ({ } return { status: 'ERROR', reason: 'invalid response' } }, - ListSwaps: async (): Promise => { + ListTxSwaps: async (): Promise => { const auth = await params.retrieveUserAuth() if (auth === null) throw new Error('retrieveUserAuth() returned null') - let finalRoute = '/api/user/swap/list' + let finalRoute = '/api/user/swap/transaction/list' const { data } = await axios.post(params.baseUrl + finalRoute, {}, { headers: { 'authorization': auth } }) if (data.status === 'ERROR' && typeof data.reason === 'string') return data if (data.status === 'OK') { const result = data if(!params.checkResult) return { status: 'OK', ...result } - const error = Types.SwapsListValidate(result) + const error = Types.TxSwapsListValidate(result) if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } } return { status: 'ERROR', reason: 'invalid response' } @@ -909,7 +962,21 @@ export default (params: ClientParams) => ({ } return { status: 'ERROR', reason: 'invalid response' } }, - PayAdminTransactionSwap: async (request: Types.PayAdminTransactionSwapRequest): Promise => { + PayAdminInvoiceSwap: async (request: Types.PayAdminInvoiceSwapRequest): Promise => { + 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 => { const auth = await params.retrieveAdminAuth() if (auth === null) throw new Error('retrieveAdminAuth() returned null') let finalRoute = '/api/admin/swap/transaction/pay' @@ -918,7 +985,7 @@ export default (params: ClientParams) => ({ if (data.status === 'OK') { const result = data if(!params.checkResult) return { status: 'OK', ...result } - const error = Types.AdminSwapResponseValidate(result) + const error = Types.AdminTxSwapResponseValidate(result) if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } } return { status: 'ERROR', reason: 'invalid response' } @@ -962,6 +1029,20 @@ export default (params: ClientParams) => ({ } return { status: 'ERROR', reason: 'invalid response' } }, + RefundAdminInvoiceSwap: async (request: Types.RefundAdminInvoiceSwapRequest): Promise => { + 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 => { const auth = await params.retrieveAppAuth() if (auth === null) throw new Error('retrieveAppAuth() returned null') diff --git a/proto/autogenerated/ts/nostr_client.ts b/proto/autogenerated/ts/nostr_client.ts index df969c7b..64e517e6 100644 --- a/proto/autogenerated/ts/nostr_client.ts +++ b/proto/autogenerated/ts/nostr_client.ts @@ -137,6 +137,18 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ } return { status: 'ERROR', reason: 'invalid response' } }, + BumpTx: async (request: Types.BumpTx): Promise => { + 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 => { const auth = await params.retrieveNostrAdminAuth() if (auth === null) throw new Error('retrieveNostrAdminAuth() returned null') @@ -230,6 +242,21 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ } return { status: 'ERROR', reason: 'invalid response' } }, + GetAdminInvoiceSwapQuotes: async (request: Types.InvoiceSwapRequest): Promise => { + 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 => { const auth = await params.retrieveNostrAdminAuth() if (auth === null) throw new Error('retrieveNostrAdminAuth() returned null') @@ -260,6 +287,21 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ } return { status: 'ERROR', reason: 'invalid response' } }, + GetAssetsAndLiabilities: async (request: Types.AssetsAndLiabilitiesReq): Promise => { + 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 => { const auth = await params.retrieveNostrMetricsAuth() if (auth === null) throw new Error('retrieveNostrMetricsAuth() returned null') @@ -666,16 +708,30 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ } return { status: 'ERROR', reason: 'invalid response' } }, - ListAdminSwaps: async (): Promise => { + ListAdminInvoiceSwaps: async (): Promise => { const auth = await params.retrieveNostrAdminAuth() if (auth === null) throw new Error('retrieveNostrAdminAuth() returned null') const nostrRequest: NostrRequest = {} - const data = await send(params.pubDestination, {rpcName:'ListAdminSwaps',authIdentifier:auth, ...nostrRequest }) + const data = await send(params.pubDestination, {rpcName:'ListAdminInvoiceSwaps',authIdentifier:auth, ...nostrRequest }) if (data.status === 'ERROR' && typeof data.reason === 'string') return data if (data.status === 'OK') { const result = data if(!params.checkResult) return { status: 'OK', ...result } - const error = Types.SwapsListValidate(result) + const error = Types.InvoiceSwapsListValidate(result) + if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } + } + return { status: 'ERROR', reason: 'invalid response' } + }, + ListAdminTxSwaps: async (): Promise => { + const auth = await params.retrieveNostrAdminAuth() + if (auth === null) throw new Error('retrieveNostrAdminAuth() returned null') + const nostrRequest: NostrRequest = {} + const data = await send(params.pubDestination, {rpcName:'ListAdminTxSwaps',authIdentifier:auth, ...nostrRequest }) + if (data.status === 'ERROR' && typeof data.reason === 'string') return data + if (data.status === 'OK') { + const result = data + if(!params.checkResult) return { status: 'OK', ...result } + const error = Types.TxSwapsListValidate(result) if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } } return { status: 'ERROR', reason: 'invalid response' } @@ -694,16 +750,16 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ } return { status: 'ERROR', reason: 'invalid response' } }, - ListSwaps: async (): Promise => { + ListTxSwaps: async (): Promise => { const auth = await params.retrieveNostrUserAuth() if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') const nostrRequest: NostrRequest = {} - const data = await send(params.pubDestination, {rpcName:'ListSwaps',authIdentifier:auth, ...nostrRequest }) + const data = await send(params.pubDestination, {rpcName:'ListTxSwaps',authIdentifier:auth, ...nostrRequest }) if (data.status === 'ERROR' && typeof data.reason === 'string') return data if (data.status === 'OK') { const result = data if(!params.checkResult) return { status: 'OK', ...result } - const error = Types.SwapsListValidate(result) + const error = Types.TxSwapsListValidate(result) if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } } return { status: 'ERROR', reason: 'invalid response' } @@ -798,7 +854,22 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ } return { status: 'ERROR', reason: 'invalid response' } }, - PayAdminTransactionSwap: async (request: Types.PayAdminTransactionSwapRequest): Promise => { + PayAdminInvoiceSwap: async (request: Types.PayAdminInvoiceSwapRequest): Promise => { + const auth = await params.retrieveNostrAdminAuth() + if (auth === null) throw new Error('retrieveNostrAdminAuth() returned null') + const nostrRequest: NostrRequest = {} + nostrRequest.body = request + const data = await send(params.pubDestination, {rpcName:'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 => { const auth = await params.retrieveNostrAdminAuth() if (auth === null) throw new Error('retrieveNostrAdminAuth() returned null') const nostrRequest: NostrRequest = {} @@ -808,7 +879,7 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ if (data.status === 'OK') { const result = data if(!params.checkResult) return { status: 'OK', ...result } - const error = Types.AdminSwapResponseValidate(result) + const error = Types.AdminTxSwapResponseValidate(result) if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } } return { status: 'ERROR', reason: 'invalid response' } @@ -839,6 +910,21 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ } return { status: 'ERROR', reason: 'invalid response' } }, + RefundAdminInvoiceSwap: async (request: Types.RefundAdminInvoiceSwapRequest): Promise => { + 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 => { const auth = await params.retrieveNostrUserAuth() if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') diff --git a/proto/autogenerated/ts/nostr_transport.ts b/proto/autogenerated/ts/nostr_transport.ts index 24a96cd6..80284e10 100644 --- a/proto/autogenerated/ts/nostr_transport.ts +++ b/proto/autogenerated/ts/nostr_transport.ts @@ -427,12 +427,12 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => { callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) } break - case 'ListSwaps': - if (!methods.ListSwaps) { - throw new Error('method not defined: ListSwaps') + case 'ListTxSwaps': + if (!methods.ListTxSwaps) { + throw new Error('method not defined: ListTxSwaps') } else { opStats.validate = opStats.guard - const res = await methods.ListSwaps({...operation, ctx}); responses.push({ status: 'OK', ...res }) + const res = await methods.ListTxSwaps({...operation, ctx}); responses.push({ status: 'OK', ...res }) opStats.handle = process.hrtime.bigint() callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) } @@ -575,6 +575,22 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => { opts.metricsCallback([{ ...info, ...stats, ...ctx }, ...callsMetrics]) }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } break + case 'BumpTx': + try { + if (!methods.BumpTx) throw new Error('method: BumpTx is not implemented') + const authContext = await opts.NostrAdminAuthGuard(req.appId, req.authIdentifier) + stats.guard = process.hrtime.bigint() + authCtx = authContext + const request = req.body + const error = Types.BumpTxValidate(request) + stats.validate = process.hrtime.bigint() + if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback) + await methods.BumpTx({rpcName:'BumpTx', ctx:authContext , req: request}) + stats.handle = process.hrtime.bigint() + res({status: 'OK'}) + opts.metricsCallback([{ ...info, ...stats, ...authContext }]) + }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } + break case 'CloseChannel': try { if (!methods.CloseChannel) throw new Error('method: CloseChannel is not implemented') @@ -687,6 +703,22 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => { opts.metricsCallback([{ ...info, ...stats, ...authContext }]) }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } break + case 'GetAdminInvoiceSwapQuotes': + try { + if (!methods.GetAdminInvoiceSwapQuotes) throw new Error('method: GetAdminInvoiceSwapQuotes is not implemented') + const authContext = await opts.NostrAdminAuthGuard(req.appId, req.authIdentifier) + stats.guard = process.hrtime.bigint() + authCtx = authContext + const request = req.body + const error = Types.InvoiceSwapRequestValidate(request) + stats.validate = process.hrtime.bigint() + if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback) + const response = await methods.GetAdminInvoiceSwapQuotes({rpcName:'GetAdminInvoiceSwapQuotes', ctx:authContext , req: request}) + stats.handle = process.hrtime.bigint() + res({status: 'OK', ...response}) + opts.metricsCallback([{ ...info, ...stats, ...authContext }]) + }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } + break case 'GetAdminTransactionSwapQuotes': try { if (!methods.GetAdminTransactionSwapQuotes) throw new Error('method: GetAdminTransactionSwapQuotes is not implemented') @@ -719,6 +751,22 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => { opts.metricsCallback([{ ...info, ...stats, ...authContext }]) }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } break + case 'GetAssetsAndLiabilities': + try { + if (!methods.GetAssetsAndLiabilities) throw new Error('method: GetAssetsAndLiabilities is not implemented') + const authContext = await opts.NostrAdminAuthGuard(req.appId, req.authIdentifier) + stats.guard = process.hrtime.bigint() + authCtx = authContext + const request = req.body + const error = Types.AssetsAndLiabilitiesReqValidate(request) + stats.validate = process.hrtime.bigint() + if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback) + const response = await methods.GetAssetsAndLiabilities({rpcName:'GetAssetsAndLiabilities', ctx:authContext , req: request}) + stats.handle = process.hrtime.bigint() + res({status: 'OK', ...response}) + opts.metricsCallback([{ ...info, ...stats, ...authContext }]) + }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } + break case 'GetBundleMetrics': try { if (!methods.GetBundleMetrics) throw new Error('method: GetBundleMetrics is not implemented') @@ -1122,14 +1170,27 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => { opts.metricsCallback([{ ...info, ...stats, ...authContext }]) }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } break - case 'ListAdminSwaps': + case 'ListAdminInvoiceSwaps': try { - if (!methods.ListAdminSwaps) throw new Error('method: ListAdminSwaps is not implemented') + if (!methods.ListAdminInvoiceSwaps) throw new Error('method: ListAdminInvoiceSwaps is not implemented') const authContext = await opts.NostrAdminAuthGuard(req.appId, req.authIdentifier) stats.guard = process.hrtime.bigint() authCtx = authContext stats.validate = stats.guard - const response = await methods.ListAdminSwaps({rpcName:'ListAdminSwaps', ctx:authContext }) + const response = await methods.ListAdminInvoiceSwaps({rpcName:'ListAdminInvoiceSwaps', ctx:authContext }) + stats.handle = process.hrtime.bigint() + res({status: 'OK', ...response}) + opts.metricsCallback([{ ...info, ...stats, ...authContext }]) + }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } + break + case 'ListAdminTxSwaps': + try { + if (!methods.ListAdminTxSwaps) throw new Error('method: ListAdminTxSwaps is not implemented') + const authContext = await opts.NostrAdminAuthGuard(req.appId, req.authIdentifier) + stats.guard = process.hrtime.bigint() + authCtx = authContext + stats.validate = stats.guard + const response = await methods.ListAdminTxSwaps({rpcName:'ListAdminTxSwaps', ctx:authContext }) stats.handle = process.hrtime.bigint() res({status: 'OK', ...response}) opts.metricsCallback([{ ...info, ...stats, ...authContext }]) @@ -1148,14 +1209,14 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => { opts.metricsCallback([{ ...info, ...stats, ...authContext }]) }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } break - case 'ListSwaps': + case 'ListTxSwaps': try { - if (!methods.ListSwaps) throw new Error('method: ListSwaps is not implemented') + if (!methods.ListTxSwaps) throw new Error('method: ListTxSwaps is not implemented') const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier) stats.guard = process.hrtime.bigint() authCtx = authContext stats.validate = stats.guard - const response = await methods.ListSwaps({rpcName:'ListSwaps', ctx:authContext }) + const response = await methods.ListTxSwaps({rpcName:'ListTxSwaps', ctx:authContext }) stats.handle = process.hrtime.bigint() res({status: 'OK', ...response}) opts.metricsCallback([{ ...info, ...stats, ...authContext }]) @@ -1254,6 +1315,22 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => { opts.metricsCallback([{ ...info, ...stats, ...authContext }]) }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } break + case 'PayAdminInvoiceSwap': + try { + if (!methods.PayAdminInvoiceSwap) throw new Error('method: PayAdminInvoiceSwap is not implemented') + const authContext = await opts.NostrAdminAuthGuard(req.appId, req.authIdentifier) + stats.guard = process.hrtime.bigint() + authCtx = authContext + const request = req.body + const error = Types.PayAdminInvoiceSwapRequestValidate(request) + stats.validate = process.hrtime.bigint() + if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback) + const response = await methods.PayAdminInvoiceSwap({rpcName:'PayAdminInvoiceSwap', ctx:authContext , req: request}) + stats.handle = process.hrtime.bigint() + res({status: 'OK', ...response}) + opts.metricsCallback([{ ...info, ...stats, ...authContext }]) + }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } + break case 'PayAdminTransactionSwap': try { if (!methods.PayAdminTransactionSwap) throw new Error('method: PayAdminTransactionSwap is not implemented') @@ -1299,6 +1376,22 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => { opts.metricsCallback([{ ...info, ...stats, ...authContext }]) }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } break + case 'RefundAdminInvoiceSwap': + try { + if (!methods.RefundAdminInvoiceSwap) throw new Error('method: RefundAdminInvoiceSwap is not implemented') + const authContext = await opts.NostrAdminAuthGuard(req.appId, req.authIdentifier) + stats.guard = process.hrtime.bigint() + authCtx = authContext + const request = req.body + const error = Types.RefundAdminInvoiceSwapRequestValidate(request) + stats.validate = process.hrtime.bigint() + if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback) + const response = await methods.RefundAdminInvoiceSwap({rpcName:'RefundAdminInvoiceSwap', ctx:authContext , req: request}) + stats.handle = process.hrtime.bigint() + res({status: 'OK', ...response}) + opts.metricsCallback([{ ...info, ...stats, ...authContext }]) + }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } + break case 'ResetDebit': try { if (!methods.ResetDebit) throw new Error('method: ResetDebit is not implemented') diff --git a/proto/autogenerated/ts/types.ts b/proto/autogenerated/ts/types.ts index e397cd94..90cd1724 100644 --- a/proto/autogenerated/ts/types.ts +++ b/proto/autogenerated/ts/types.ts @@ -7,8 +7,8 @@ export type RequestMetric = AuthContext & RequestInfo & RequestStats & { error?: export type AdminContext = { admin_id: string } -export type AdminMethodInputs = AddApp_Input | AddPeer_Input | AuthApp_Input | BanUser_Input | CloseChannel_Input | CreateOneTimeInviteLink_Input | GetAdminTransactionSwapQuotes_Input | GetInviteLinkState_Input | GetSeed_Input | ListAdminSwaps_Input | ListChannels_Input | LndGetInfo_Input | OpenChannel_Input | PayAdminTransactionSwap_Input | UpdateChannelPolicy_Input -export type AdminMethodOutputs = AddApp_Output | AddPeer_Output | AuthApp_Output | BanUser_Output | CloseChannel_Output | CreateOneTimeInviteLink_Output | GetAdminTransactionSwapQuotes_Output | GetInviteLinkState_Output | GetSeed_Output | ListAdminSwaps_Output | ListChannels_Output | LndGetInfo_Output | OpenChannel_Output | PayAdminTransactionSwap_Output | UpdateChannelPolicy_Output +export type AdminMethodInputs = AddApp_Input | AddPeer_Input | AuthApp_Input | BanUser_Input | BumpTx_Input | CloseChannel_Input | CreateOneTimeInviteLink_Input | GetAdminInvoiceSwapQuotes_Input | GetAdminTransactionSwapQuotes_Input | GetAssetsAndLiabilities_Input | GetInviteLinkState_Input | GetSeed_Input | ListAdminInvoiceSwaps_Input | ListAdminTxSwaps_Input | ListChannels_Input | LndGetInfo_Input | OpenChannel_Input | PayAdminInvoiceSwap_Input | PayAdminTransactionSwap_Input | RefundAdminInvoiceSwap_Input | UpdateChannelPolicy_Input +export type AdminMethodOutputs = AddApp_Output | AddPeer_Output | AuthApp_Output | BanUser_Output | BumpTx_Output | CloseChannel_Output | CreateOneTimeInviteLink_Output | GetAdminInvoiceSwapQuotes_Output | GetAdminTransactionSwapQuotes_Output | GetAssetsAndLiabilities_Output | GetInviteLinkState_Output | GetSeed_Output | ListAdminInvoiceSwaps_Output | ListAdminTxSwaps_Output | ListChannels_Output | LndGetInfo_Output | OpenChannel_Output | PayAdminInvoiceSwap_Output | PayAdminTransactionSwap_Output | RefundAdminInvoiceSwap_Output | UpdateChannelPolicy_Output export type AppContext = { app_id: string } @@ -35,8 +35,8 @@ export type UserContext = { app_user_id: string user_id: string } -export type UserMethodInputs = AddProduct_Input | AddUserOffer_Input | AuthorizeManage_Input | BanDebit_Input | DecodeInvoice_Input | DeleteUserOffer_Input | EditDebit_Input | EnrollAdminToken_Input | EnrollMessagingToken_Input | GetDebitAuthorizations_Input | GetHttpCreds_Input | GetLNURLChannelLink_Input | GetLnurlPayLink_Input | GetLnurlWithdrawLink_Input | GetManageAuthorizations_Input | GetPaymentState_Input | GetTransactionSwapQuotes_Input | GetUserInfo_Input | GetUserOffer_Input | GetUserOfferInvoices_Input | GetUserOffers_Input | GetUserOperations_Input | ListSwaps_Input | NewAddress_Input | NewInvoice_Input | NewProductInvoice_Input | PayAddress_Input | PayInvoice_Input | ResetDebit_Input | ResetManage_Input | RespondToDebit_Input | UpdateCallbackUrl_Input | UpdateUserOffer_Input | UserHealth_Input -export type UserMethodOutputs = AddProduct_Output | AddUserOffer_Output | AuthorizeManage_Output | BanDebit_Output | DecodeInvoice_Output | DeleteUserOffer_Output | EditDebit_Output | EnrollAdminToken_Output | EnrollMessagingToken_Output | GetDebitAuthorizations_Output | GetHttpCreds_Output | GetLNURLChannelLink_Output | GetLnurlPayLink_Output | GetLnurlWithdrawLink_Output | GetManageAuthorizations_Output | GetPaymentState_Output | GetTransactionSwapQuotes_Output | GetUserInfo_Output | GetUserOffer_Output | GetUserOfferInvoices_Output | GetUserOffers_Output | GetUserOperations_Output | ListSwaps_Output | NewAddress_Output | NewInvoice_Output | NewProductInvoice_Output | PayAddress_Output | PayInvoice_Output | ResetDebit_Output | ResetManage_Output | RespondToDebit_Output | UpdateCallbackUrl_Output | UpdateUserOffer_Output | UserHealth_Output +export type UserMethodInputs = AddProduct_Input | AddUserOffer_Input | AuthorizeManage_Input | BanDebit_Input | DecodeInvoice_Input | DeleteUserOffer_Input | EditDebit_Input | EnrollAdminToken_Input | EnrollMessagingToken_Input | GetDebitAuthorizations_Input | GetHttpCreds_Input | GetLNURLChannelLink_Input | GetLnurlPayLink_Input | GetLnurlWithdrawLink_Input | GetManageAuthorizations_Input | GetPaymentState_Input | GetTransactionSwapQuotes_Input | GetUserInfo_Input | GetUserOffer_Input | GetUserOfferInvoices_Input | GetUserOffers_Input | GetUserOperations_Input | ListTxSwaps_Input | NewAddress_Input | NewInvoice_Input | NewProductInvoice_Input | PayAddress_Input | PayInvoice_Input | ResetDebit_Input | ResetManage_Input | RespondToDebit_Input | UpdateCallbackUrl_Input | UpdateUserOffer_Input | UserHealth_Input +export type UserMethodOutputs = AddProduct_Output | AddUserOffer_Output | AuthorizeManage_Output | BanDebit_Output | DecodeInvoice_Output | DeleteUserOffer_Output | EditDebit_Output | EnrollAdminToken_Output | EnrollMessagingToken_Output | GetDebitAuthorizations_Output | GetHttpCreds_Output | GetLNURLChannelLink_Output | GetLnurlPayLink_Output | GetLnurlWithdrawLink_Output | GetManageAuthorizations_Output | GetPaymentState_Output | GetTransactionSwapQuotes_Output | GetUserInfo_Output | GetUserOffer_Output | GetUserOfferInvoices_Output | GetUserOffers_Output | GetUserOperations_Output | ListTxSwaps_Output | NewAddress_Output | NewInvoice_Output | NewProductInvoice_Output | PayAddress_Output | PayInvoice_Output | ResetDebit_Output | ResetManage_Output | RespondToDebit_Output | UpdateCallbackUrl_Output | UpdateUserOffer_Output | UserHealth_Output export type AuthContext = AdminContext | AppContext | GuestContext | GuestWithPubContext | MetricsContext | UserContext export type AddApp_Input = {rpcName:'AddApp', req: AddAppRequest} @@ -75,6 +75,9 @@ export type BanUser_Output = ResultError | ({ status: 'OK' } & BanUserResponse) export type BatchUser_Input = UserMethodInputs export type BatchUser_Output = UserMethodOutputs +export type BumpTx_Input = {rpcName:'BumpTx', req: BumpTx} +export type BumpTx_Output = ResultError | { status: 'OK' } + export type CloseChannel_Input = {rpcName:'CloseChannel', req: CloseChannelRequest} export type CloseChannel_Output = ResultError | ({ status: 'OK' } & CloseChannelResponse) @@ -99,6 +102,9 @@ export type EnrollAdminToken_Output = ResultError | { status: 'OK' } export type EnrollMessagingToken_Input = {rpcName:'EnrollMessagingToken', req: MessagingToken} export type EnrollMessagingToken_Output = ResultError | { status: 'OK' } +export type GetAdminInvoiceSwapQuotes_Input = {rpcName:'GetAdminInvoiceSwapQuotes', req: InvoiceSwapRequest} +export type GetAdminInvoiceSwapQuotes_Output = ResultError | ({ status: 'OK' } & InvoiceSwapQuoteList) + export type GetAdminTransactionSwapQuotes_Input = {rpcName:'GetAdminTransactionSwapQuotes', req: TransactionSwapRequest} export type GetAdminTransactionSwapQuotes_Output = ResultError | ({ status: 'OK' } & TransactionSwapQuoteList) @@ -114,6 +120,9 @@ export type GetAppUserLNURLInfo_Output = ResultError | ({ status: 'OK' } & Lnurl export type GetAppsMetrics_Input = {rpcName:'GetAppsMetrics', req: AppsMetricsRequest} export type GetAppsMetrics_Output = ResultError | ({ status: 'OK' } & AppsMetrics) +export type GetAssetsAndLiabilities_Input = {rpcName:'GetAssetsAndLiabilities', req: AssetsAndLiabilitiesReq} +export type GetAssetsAndLiabilities_Output = ResultError | ({ status: 'OK' } & AssetsAndLiabilities) + export type GetBundleMetrics_Input = {rpcName:'GetBundleMetrics', req: LatestBundleMetricReq} export type GetBundleMetrics_Output = ResultError | ({ status: 'OK' } & BundleMetrics) @@ -238,14 +247,17 @@ export type Health_Output = ResultError | { status: 'OK' } export type LinkNPubThroughToken_Input = {rpcName:'LinkNPubThroughToken', req: LinkNPubThroughTokenRequest} export type LinkNPubThroughToken_Output = ResultError | { status: 'OK' } -export type ListAdminSwaps_Input = {rpcName:'ListAdminSwaps'} -export type ListAdminSwaps_Output = ResultError | ({ status: 'OK' } & SwapsList) +export type ListAdminInvoiceSwaps_Input = {rpcName:'ListAdminInvoiceSwaps'} +export type ListAdminInvoiceSwaps_Output = ResultError | ({ status: 'OK' } & InvoiceSwapsList) + +export type ListAdminTxSwaps_Input = {rpcName:'ListAdminTxSwaps'} +export type ListAdminTxSwaps_Output = ResultError | ({ status: 'OK' } & TxSwapsList) export type ListChannels_Input = {rpcName:'ListChannels'} export type ListChannels_Output = ResultError | ({ status: 'OK' } & LndChannels) -export type ListSwaps_Input = {rpcName:'ListSwaps'} -export type ListSwaps_Output = ResultError | ({ status: 'OK' } & SwapsList) +export type ListTxSwaps_Input = {rpcName:'ListTxSwaps'} +export type ListTxSwaps_Output = ResultError | ({ status: 'OK' } & TxSwapsList) export type LndGetInfo_Input = {rpcName:'LndGetInfo', req: LndGetInfoRequest} export type LndGetInfo_Output = ResultError | ({ status: 'OK' } & LndGetInfoResponse) @@ -268,8 +280,11 @@ export type OpenChannel_Output = ResultError | ({ status: 'OK' } & OpenChannelRe export type PayAddress_Input = {rpcName:'PayAddress', req: PayAddressRequest} export type PayAddress_Output = ResultError | ({ status: 'OK' } & PayAddressResponse) +export type PayAdminInvoiceSwap_Input = {rpcName:'PayAdminInvoiceSwap', req: PayAdminInvoiceSwapRequest} +export type PayAdminInvoiceSwap_Output = ResultError | ({ status: 'OK' } & AdminInvoiceSwapResponse) + export type PayAdminTransactionSwap_Input = {rpcName:'PayAdminTransactionSwap', req: PayAdminTransactionSwapRequest} -export type PayAdminTransactionSwap_Output = ResultError | ({ status: 'OK' } & AdminSwapResponse) +export type PayAdminTransactionSwap_Output = ResultError | ({ status: 'OK' } & AdminTxSwapResponse) export type PayAppUserInvoice_Input = {rpcName:'PayAppUserInvoice', req: PayAppUserInvoiceRequest} export type PayAppUserInvoice_Output = ResultError | ({ status: 'OK' } & PayInvoiceResponse) @@ -280,6 +295,9 @@ export type PayInvoice_Output = ResultError | ({ status: 'OK' } & PayInvoiceResp export type PingSubProcesses_Input = {rpcName:'PingSubProcesses'} export type PingSubProcesses_Output = ResultError | { status: 'OK' } +export type RefundAdminInvoiceSwap_Input = {rpcName:'RefundAdminInvoiceSwap', req: RefundAdminInvoiceSwapRequest} +export type RefundAdminInvoiceSwap_Output = ResultError | ({ status: 'OK' } & AdminInvoiceSwapResponse) + export type RequestNPubLinkingToken_Input = {rpcName:'RequestNPubLinkingToken', req: RequestNPubLinkingTokenRequest} export type RequestNPubLinkingToken_Output = ResultError | ({ status: 'OK' } & RequestNPubLinkingTokenResponse) @@ -349,6 +367,7 @@ export type ServerMethods = { AuthorizeManage?: (req: AuthorizeManage_Input & {ctx: UserContext }) => Promise BanDebit?: (req: BanDebit_Input & {ctx: UserContext }) => Promise BanUser?: (req: BanUser_Input & {ctx: AdminContext }) => Promise + BumpTx?: (req: BumpTx_Input & {ctx: AdminContext }) => Promise CloseChannel?: (req: CloseChannel_Input & {ctx: AdminContext }) => Promise CreateOneTimeInviteLink?: (req: CreateOneTimeInviteLink_Input & {ctx: AdminContext }) => Promise DecodeInvoice?: (req: DecodeInvoice_Input & {ctx: UserContext }) => Promise @@ -357,11 +376,13 @@ export type ServerMethods = { EncryptionExchange?: (req: EncryptionExchange_Input & {ctx: GuestContext }) => Promise EnrollAdminToken?: (req: EnrollAdminToken_Input & {ctx: UserContext }) => Promise EnrollMessagingToken?: (req: EnrollMessagingToken_Input & {ctx: UserContext }) => Promise + GetAdminInvoiceSwapQuotes?: (req: GetAdminInvoiceSwapQuotes_Input & {ctx: AdminContext }) => Promise GetAdminTransactionSwapQuotes?: (req: GetAdminTransactionSwapQuotes_Input & {ctx: AdminContext }) => Promise GetApp?: (req: GetApp_Input & {ctx: AppContext }) => Promise GetAppUser?: (req: GetAppUser_Input & {ctx: AppContext }) => Promise GetAppUserLNURLInfo?: (req: GetAppUserLNURLInfo_Input & {ctx: AppContext }) => Promise GetAppsMetrics?: (req: GetAppsMetrics_Input & {ctx: MetricsContext }) => Promise + GetAssetsAndLiabilities?: (req: GetAssetsAndLiabilities_Input & {ctx: AdminContext }) => Promise GetBundleMetrics?: (req: GetBundleMetrics_Input & {ctx: MetricsContext }) => Promise GetDebitAuthorizations?: (req: GetDebitAuthorizations_Input & {ctx: UserContext }) => Promise GetErrorStats?: (req: GetErrorStats_Input & {ctx: MetricsContext }) => Promise @@ -397,19 +418,22 @@ export type ServerMethods = { HandleLnurlWithdraw?: (req: HandleLnurlWithdraw_Input & {ctx: GuestContext }) => Promise Health?: (req: Health_Input & {ctx: GuestContext }) => Promise LinkNPubThroughToken?: (req: LinkNPubThroughToken_Input & {ctx: GuestWithPubContext }) => Promise - ListAdminSwaps?: (req: ListAdminSwaps_Input & {ctx: AdminContext }) => Promise + ListAdminInvoiceSwaps?: (req: ListAdminInvoiceSwaps_Input & {ctx: AdminContext }) => Promise + ListAdminTxSwaps?: (req: ListAdminTxSwaps_Input & {ctx: AdminContext }) => Promise ListChannels?: (req: ListChannels_Input & {ctx: AdminContext }) => Promise - ListSwaps?: (req: ListSwaps_Input & {ctx: UserContext }) => Promise + ListTxSwaps?: (req: ListTxSwaps_Input & {ctx: UserContext }) => Promise LndGetInfo?: (req: LndGetInfo_Input & {ctx: AdminContext }) => Promise NewAddress?: (req: NewAddress_Input & {ctx: UserContext }) => Promise NewInvoice?: (req: NewInvoice_Input & {ctx: UserContext }) => Promise NewProductInvoice?: (req: NewProductInvoice_Input & {ctx: UserContext }) => Promise OpenChannel?: (req: OpenChannel_Input & {ctx: AdminContext }) => Promise PayAddress?: (req: PayAddress_Input & {ctx: UserContext }) => Promise - PayAdminTransactionSwap?: (req: PayAdminTransactionSwap_Input & {ctx: AdminContext }) => Promise + PayAdminInvoiceSwap?: (req: PayAdminInvoiceSwap_Input & {ctx: AdminContext }) => Promise + PayAdminTransactionSwap?: (req: PayAdminTransactionSwap_Input & {ctx: AdminContext }) => Promise PayAppUserInvoice?: (req: PayAppUserInvoice_Input & {ctx: AppContext }) => Promise PayInvoice?: (req: PayInvoice_Input & {ctx: UserContext }) => Promise PingSubProcesses?: (req: PingSubProcesses_Input & {ctx: MetricsContext }) => Promise + RefundAdminInvoiceSwap?: (req: RefundAdminInvoiceSwap_Input & {ctx: AdminContext }) => Promise RequestNPubLinkingToken?: (req: RequestNPubLinkingToken_Input & {ctx: AppContext }) => Promise ResetDebit?: (req: ResetDebit_Input & {ctx: UserContext }) => Promise ResetManage?: (req: ResetManage_Input & {ctx: UserContext }) => Promise @@ -465,6 +489,14 @@ export const enumCheckSingleMetricType = (e?: SingleMetricType): boolean => { for (const v in SingleMetricType) if (e === v) return true return false } +export enum TrackedOperationType { + ROOT = 'ROOT', + USER = 'USER', +} +export const enumCheckTrackedOperationType = (e?: TrackedOperationType): boolean => { + for (const v in TrackedOperationType) if (e === v) return true + return false +} export enum UserOperationType { INCOMING_INVOICE = 'INCOMING_INVOICE', INCOMING_TX = 'INCOMING_TX', @@ -671,17 +703,35 @@ export const AddProductRequestValidate = (o?: AddProductRequest, opts: AddProduc return null } -export type AdminSwapResponse = { +export type AdminInvoiceSwapResponse = { + tx_id: string +} +export const AdminInvoiceSwapResponseOptionalFields: [] = [] +export type AdminInvoiceSwapResponseOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: [] + tx_id_CustomCheck?: (v: string) => boolean +} +export const AdminInvoiceSwapResponseValidate = (o?: AdminInvoiceSwapResponse, opts: AdminInvoiceSwapResponseOptions = {}, path: string = 'AdminInvoiceSwapResponse::root.'): Error | null => { + if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') + if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + + if (typeof o.tx_id !== 'string') return new Error(`${path}.tx_id: is not a string`) + if (opts.tx_id_CustomCheck && !opts.tx_id_CustomCheck(o.tx_id)) return new Error(`${path}.tx_id: custom check failed`) + + return null +} + +export type AdminTxSwapResponse = { network_fee: number tx_id: string } -export const AdminSwapResponseOptionalFields: [] = [] -export type AdminSwapResponseOptions = OptionsBaseMessage & { +export const AdminTxSwapResponseOptionalFields: [] = [] +export type AdminTxSwapResponseOptions = OptionsBaseMessage & { checkOptionalsAreSet?: [] network_fee_CustomCheck?: (v: number) => boolean tx_id_CustomCheck?: (v: string) => boolean } -export const AdminSwapResponseValidate = (o?: AdminSwapResponse, opts: AdminSwapResponseOptions = {}, path: string = 'AdminSwapResponse::root.'): Error | null => { +export const AdminTxSwapResponseValidate = (o?: AdminTxSwapResponse, opts: AdminTxSwapResponseOptions = {}, path: string = 'AdminTxSwapResponse::root.'): Error | null => { if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') @@ -895,6 +945,105 @@ export const AppsMetricsRequestValidate = (o?: AppsMetricsRequest, opts: AppsMet return null } +export type AssetOperation = { + amount: number + tracked?: TrackedOperation + ts: number +} +export type AssetOperationOptionalField = 'tracked' +export const AssetOperationOptionalFields: AssetOperationOptionalField[] = ['tracked'] +export type AssetOperationOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: AssetOperationOptionalField[] + amount_CustomCheck?: (v: number) => boolean + tracked_Options?: TrackedOperationOptions + ts_CustomCheck?: (v: number) => boolean +} +export const AssetOperationValidate = (o?: AssetOperation, opts: AssetOperationOptions = {}, path: string = 'AssetOperation::root.'): Error | null => { + if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') + if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + + if (typeof o.amount !== 'number') return new Error(`${path}.amount: is not a number`) + if (opts.amount_CustomCheck && !opts.amount_CustomCheck(o.amount)) return new Error(`${path}.amount: custom check failed`) + + if (typeof o.tracked === 'object' || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('tracked')) { + const trackedErr = TrackedOperationValidate(o.tracked, opts.tracked_Options, `${path}.tracked`) + if (trackedErr !== null) return trackedErr + } + + + if (typeof o.ts !== 'number') return new Error(`${path}.ts: is not a number`) + if (opts.ts_CustomCheck && !opts.ts_CustomCheck(o.ts)) return new Error(`${path}.ts: custom check failed`) + + return null +} + +export type AssetsAndLiabilities = { + liquidity_providers: LiquidityAssetProvider[] + lnds: LndAssetProvider[] + users_balance: number +} +export const AssetsAndLiabilitiesOptionalFields: [] = [] +export type AssetsAndLiabilitiesOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: [] + liquidity_providers_ItemOptions?: LiquidityAssetProviderOptions + liquidity_providers_CustomCheck?: (v: LiquidityAssetProvider[]) => boolean + lnds_ItemOptions?: LndAssetProviderOptions + lnds_CustomCheck?: (v: LndAssetProvider[]) => boolean + users_balance_CustomCheck?: (v: number) => boolean +} +export const AssetsAndLiabilitiesValidate = (o?: AssetsAndLiabilities, opts: AssetsAndLiabilitiesOptions = {}, path: string = 'AssetsAndLiabilities::root.'): Error | null => { + if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') + if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + + if (!Array.isArray(o.liquidity_providers)) return new Error(`${path}.liquidity_providers: is not an array`) + for (let index = 0; index < o.liquidity_providers.length; index++) { + const liquidity_providersErr = LiquidityAssetProviderValidate(o.liquidity_providers[index], opts.liquidity_providers_ItemOptions, `${path}.liquidity_providers[${index}]`) + if (liquidity_providersErr !== null) return liquidity_providersErr + } + if (opts.liquidity_providers_CustomCheck && !opts.liquidity_providers_CustomCheck(o.liquidity_providers)) return new Error(`${path}.liquidity_providers: custom check failed`) + + if (!Array.isArray(o.lnds)) return new Error(`${path}.lnds: is not an array`) + for (let index = 0; index < o.lnds.length; index++) { + const lndsErr = LndAssetProviderValidate(o.lnds[index], opts.lnds_ItemOptions, `${path}.lnds[${index}]`) + if (lndsErr !== null) return lndsErr + } + if (opts.lnds_CustomCheck && !opts.lnds_CustomCheck(o.lnds)) return new Error(`${path}.lnds: custom check failed`) + + if (typeof o.users_balance !== 'number') return new Error(`${path}.users_balance: is not a number`) + if (opts.users_balance_CustomCheck && !opts.users_balance_CustomCheck(o.users_balance)) return new Error(`${path}.users_balance: custom check failed`) + + return null +} + +export type AssetsAndLiabilitiesReq = { + limit_invoices?: number + limit_payments?: number + limit_providers?: number +} +export type AssetsAndLiabilitiesReqOptionalField = 'limit_invoices' | 'limit_payments' | 'limit_providers' +export const AssetsAndLiabilitiesReqOptionalFields: AssetsAndLiabilitiesReqOptionalField[] = ['limit_invoices', 'limit_payments', 'limit_providers'] +export type AssetsAndLiabilitiesReqOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: AssetsAndLiabilitiesReqOptionalField[] + limit_invoices_CustomCheck?: (v?: number) => boolean + limit_payments_CustomCheck?: (v?: number) => boolean + limit_providers_CustomCheck?: (v?: number) => boolean +} +export const AssetsAndLiabilitiesReqValidate = (o?: AssetsAndLiabilitiesReq, opts: AssetsAndLiabilitiesReqOptions = {}, path: string = 'AssetsAndLiabilitiesReq::root.'): Error | null => { + if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') + if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + + if ((o.limit_invoices || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('limit_invoices')) && typeof o.limit_invoices !== 'number') return new Error(`${path}.limit_invoices: is not a number`) + if (opts.limit_invoices_CustomCheck && !opts.limit_invoices_CustomCheck(o.limit_invoices)) return new Error(`${path}.limit_invoices: custom check failed`) + + if ((o.limit_payments || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('limit_payments')) && typeof o.limit_payments !== 'number') return new Error(`${path}.limit_payments: is not a number`) + if (opts.limit_payments_CustomCheck && !opts.limit_payments_CustomCheck(o.limit_payments)) return new Error(`${path}.limit_payments: custom check failed`) + + if ((o.limit_providers || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('limit_providers')) && typeof o.limit_providers !== 'number') return new Error(`${path}.limit_providers: is not a number`) + if (opts.limit_providers_CustomCheck && !opts.limit_providers_CustomCheck(o.limit_providers)) return new Error(`${path}.limit_providers: custom check failed`) + + return null +} + export type AuthApp = { app: Application auth_token: string @@ -1064,6 +1213,34 @@ export const BeaconDataValidate = (o?: BeaconData, opts: BeaconDataOptions = {}, return null } +export type BumpTx = { + output_index: number + sat_per_vbyte: number + txid: string +} +export const BumpTxOptionalFields: [] = [] +export type BumpTxOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: [] + output_index_CustomCheck?: (v: number) => boolean + sat_per_vbyte_CustomCheck?: (v: number) => boolean + txid_CustomCheck?: (v: string) => boolean +} +export const BumpTxValidate = (o?: BumpTx, opts: BumpTxOptions = {}, path: string = 'BumpTx::root.'): Error | null => { + if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') + if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + + if (typeof o.output_index !== 'number') return new Error(`${path}.output_index: is not a number`) + if (opts.output_index_CustomCheck && !opts.output_index_CustomCheck(o.output_index)) return new Error(`${path}.output_index: custom check failed`) + + if (typeof o.sat_per_vbyte !== 'number') return new Error(`${path}.sat_per_vbyte: is not a number`) + if (opts.sat_per_vbyte_CustomCheck && !opts.sat_per_vbyte_CustomCheck(o.sat_per_vbyte)) return new Error(`${path}.sat_per_vbyte: custom check failed`) + + if (typeof o.txid !== 'string') return new Error(`${path}.txid: is not a string`) + if (opts.txid_CustomCheck && !opts.txid_CustomCheck(o.txid)) return new Error(`${path}.txid: custom check failed`) + + return null +} + export type BundleData = { available_chunks: number[] base_64_data: string[] @@ -2088,6 +2265,186 @@ export const HttpCredsValidate = (o?: HttpCreds, opts: HttpCredsOptions = {}, pa return null } +export type InvoiceSwapOperation = { + completed_at_unix?: number + failure_reason?: string + operation_payment?: UserOperation + quote: InvoiceSwapQuote +} +export type InvoiceSwapOperationOptionalField = 'completed_at_unix' | 'failure_reason' | 'operation_payment' +export const InvoiceSwapOperationOptionalFields: InvoiceSwapOperationOptionalField[] = ['completed_at_unix', 'failure_reason', 'operation_payment'] +export type InvoiceSwapOperationOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: InvoiceSwapOperationOptionalField[] + completed_at_unix_CustomCheck?: (v?: number) => boolean + failure_reason_CustomCheck?: (v?: string) => boolean + operation_payment_Options?: UserOperationOptions + quote_Options?: InvoiceSwapQuoteOptions +} +export const InvoiceSwapOperationValidate = (o?: InvoiceSwapOperation, opts: InvoiceSwapOperationOptions = {}, path: string = 'InvoiceSwapOperation::root.'): Error | null => { + if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') + if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + + if ((o.completed_at_unix || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('completed_at_unix')) && typeof o.completed_at_unix !== 'number') return new Error(`${path}.completed_at_unix: is not a number`) + if (opts.completed_at_unix_CustomCheck && !opts.completed_at_unix_CustomCheck(o.completed_at_unix)) return new Error(`${path}.completed_at_unix: custom check failed`) + + if ((o.failure_reason || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('failure_reason')) && typeof o.failure_reason !== 'string') return new Error(`${path}.failure_reason: is not a string`) + if (opts.failure_reason_CustomCheck && !opts.failure_reason_CustomCheck(o.failure_reason)) return new Error(`${path}.failure_reason: custom check failed`) + + if (typeof o.operation_payment === 'object' || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('operation_payment')) { + const operation_paymentErr = UserOperationValidate(o.operation_payment, opts.operation_payment_Options, `${path}.operation_payment`) + if (operation_paymentErr !== null) return operation_paymentErr + } + + + const quoteErr = InvoiceSwapQuoteValidate(o.quote, opts.quote_Options, `${path}.quote`) + if (quoteErr !== null) return quoteErr + + + return null +} + +export type 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 +} +export const InvoiceSwapQuoteOptionalFields: [] = [] +export type InvoiceSwapQuoteOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: [] + address_CustomCheck?: (v: string) => boolean + chain_fee_sats_CustomCheck?: (v: number) => boolean + expires_at_block_height_CustomCheck?: (v: number) => boolean + invoice_CustomCheck?: (v: string) => boolean + invoice_amount_sats_CustomCheck?: (v: number) => boolean + paid_at_unix_CustomCheck?: (v: number) => boolean + service_fee_sats_CustomCheck?: (v: number) => boolean + service_url_CustomCheck?: (v: string) => boolean + swap_fee_sats_CustomCheck?: (v: number) => boolean + swap_operation_id_CustomCheck?: (v: string) => boolean + transaction_amount_sats_CustomCheck?: (v: number) => boolean + tx_id_CustomCheck?: (v: string) => boolean +} +export const InvoiceSwapQuoteValidate = (o?: InvoiceSwapQuote, opts: InvoiceSwapQuoteOptions = {}, path: string = 'InvoiceSwapQuote::root.'): Error | null => { + if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') + if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + + if (typeof o.address !== 'string') return new Error(`${path}.address: is not a string`) + if (opts.address_CustomCheck && !opts.address_CustomCheck(o.address)) return new Error(`${path}.address: custom check failed`) + + if (typeof o.chain_fee_sats !== 'number') return new Error(`${path}.chain_fee_sats: is not a number`) + if (opts.chain_fee_sats_CustomCheck && !opts.chain_fee_sats_CustomCheck(o.chain_fee_sats)) return new Error(`${path}.chain_fee_sats: custom check failed`) + + if (typeof o.expires_at_block_height !== 'number') return new Error(`${path}.expires_at_block_height: is not a number`) + if (opts.expires_at_block_height_CustomCheck && !opts.expires_at_block_height_CustomCheck(o.expires_at_block_height)) return new Error(`${path}.expires_at_block_height: custom check failed`) + + if (typeof o.invoice !== 'string') return new Error(`${path}.invoice: is not a string`) + if (opts.invoice_CustomCheck && !opts.invoice_CustomCheck(o.invoice)) return new Error(`${path}.invoice: custom check failed`) + + if (typeof o.invoice_amount_sats !== 'number') return new Error(`${path}.invoice_amount_sats: is not a number`) + if (opts.invoice_amount_sats_CustomCheck && !opts.invoice_amount_sats_CustomCheck(o.invoice_amount_sats)) return new Error(`${path}.invoice_amount_sats: custom check failed`) + + if (typeof o.paid_at_unix !== 'number') return new Error(`${path}.paid_at_unix: is not a number`) + if (opts.paid_at_unix_CustomCheck && !opts.paid_at_unix_CustomCheck(o.paid_at_unix)) return new Error(`${path}.paid_at_unix: custom check failed`) + + if (typeof o.service_fee_sats !== 'number') return new Error(`${path}.service_fee_sats: is not a number`) + if (opts.service_fee_sats_CustomCheck && !opts.service_fee_sats_CustomCheck(o.service_fee_sats)) return new Error(`${path}.service_fee_sats: custom check failed`) + + if (typeof o.service_url !== 'string') return new Error(`${path}.service_url: is not a string`) + if (opts.service_url_CustomCheck && !opts.service_url_CustomCheck(o.service_url)) return new Error(`${path}.service_url: custom check failed`) + + if (typeof o.swap_fee_sats !== 'number') return new Error(`${path}.swap_fee_sats: is not a number`) + if (opts.swap_fee_sats_CustomCheck && !opts.swap_fee_sats_CustomCheck(o.swap_fee_sats)) return new Error(`${path}.swap_fee_sats: custom check failed`) + + if (typeof o.swap_operation_id !== 'string') return new Error(`${path}.swap_operation_id: is not a string`) + if (opts.swap_operation_id_CustomCheck && !opts.swap_operation_id_CustomCheck(o.swap_operation_id)) return new Error(`${path}.swap_operation_id: custom check failed`) + + if (typeof o.transaction_amount_sats !== 'number') return new Error(`${path}.transaction_amount_sats: is not a number`) + if (opts.transaction_amount_sats_CustomCheck && !opts.transaction_amount_sats_CustomCheck(o.transaction_amount_sats)) return new Error(`${path}.transaction_amount_sats: custom check failed`) + + if (typeof o.tx_id !== 'string') return new Error(`${path}.tx_id: is not a string`) + if (opts.tx_id_CustomCheck && !opts.tx_id_CustomCheck(o.tx_id)) return new Error(`${path}.tx_id: custom check failed`) + + return null +} + +export type InvoiceSwapQuoteList = { + quotes: InvoiceSwapQuote[] +} +export const InvoiceSwapQuoteListOptionalFields: [] = [] +export type InvoiceSwapQuoteListOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: [] + quotes_ItemOptions?: InvoiceSwapQuoteOptions + quotes_CustomCheck?: (v: InvoiceSwapQuote[]) => boolean +} +export const InvoiceSwapQuoteListValidate = (o?: InvoiceSwapQuoteList, opts: InvoiceSwapQuoteListOptions = {}, path: string = 'InvoiceSwapQuoteList::root.'): Error | null => { + if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') + if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + + if (!Array.isArray(o.quotes)) return new Error(`${path}.quotes: is not an array`) + for (let index = 0; index < o.quotes.length; index++) { + const quotesErr = InvoiceSwapQuoteValidate(o.quotes[index], opts.quotes_ItemOptions, `${path}.quotes[${index}]`) + if (quotesErr !== null) return quotesErr + } + if (opts.quotes_CustomCheck && !opts.quotes_CustomCheck(o.quotes)) return new Error(`${path}.quotes: custom check failed`) + + return null +} + +export type InvoiceSwapRequest = { + amount_sats: number +} +export const InvoiceSwapRequestOptionalFields: [] = [] +export type InvoiceSwapRequestOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: [] + amount_sats_CustomCheck?: (v: number) => boolean +} +export const InvoiceSwapRequestValidate = (o?: InvoiceSwapRequest, opts: InvoiceSwapRequestOptions = {}, path: string = 'InvoiceSwapRequest::root.'): Error | null => { + if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') + if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + + if (typeof o.amount_sats !== 'number') return new Error(`${path}.amount_sats: is not a number`) + if (opts.amount_sats_CustomCheck && !opts.amount_sats_CustomCheck(o.amount_sats)) return new Error(`${path}.amount_sats: custom check failed`) + + return null +} + +export type InvoiceSwapsList = { + current_block_height: number + swaps: InvoiceSwapOperation[] +} +export const InvoiceSwapsListOptionalFields: [] = [] +export type InvoiceSwapsListOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: [] + current_block_height_CustomCheck?: (v: number) => boolean + swaps_ItemOptions?: InvoiceSwapOperationOptions + swaps_CustomCheck?: (v: InvoiceSwapOperation[]) => boolean +} +export const InvoiceSwapsListValidate = (o?: InvoiceSwapsList, opts: InvoiceSwapsListOptions = {}, path: string = 'InvoiceSwapsList::root.'): Error | null => { + if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') + if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + + if (typeof o.current_block_height !== 'number') return new Error(`${path}.current_block_height: is not a number`) + if (opts.current_block_height_CustomCheck && !opts.current_block_height_CustomCheck(o.current_block_height)) return new Error(`${path}.current_block_height: custom check failed`) + + if (!Array.isArray(o.swaps)) return new Error(`${path}.swaps: is not an array`) + for (let index = 0; index < o.swaps.length; index++) { + const swapsErr = InvoiceSwapOperationValidate(o.swaps[index], opts.swaps_ItemOptions, `${path}.swaps[${index}]`) + if (swapsErr !== null) return swapsErr + } + if (opts.swaps_CustomCheck && !opts.swaps_CustomCheck(o.swaps)) return new Error(`${path}.swaps: custom check failed`) + + return null +} + export type LatestBundleMetricReq = { limit?: number } @@ -2144,6 +2501,33 @@ export const LinkNPubThroughTokenRequestValidate = (o?: LinkNPubThroughTokenRequ return null } +export type LiquidityAssetProvider = { + pubkey: string + tracked?: TrackedLiquidityProvider +} +export type LiquidityAssetProviderOptionalField = 'tracked' +export const LiquidityAssetProviderOptionalFields: LiquidityAssetProviderOptionalField[] = ['tracked'] +export type LiquidityAssetProviderOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: LiquidityAssetProviderOptionalField[] + pubkey_CustomCheck?: (v: string) => boolean + tracked_Options?: TrackedLiquidityProviderOptions +} +export const LiquidityAssetProviderValidate = (o?: LiquidityAssetProvider, opts: LiquidityAssetProviderOptions = {}, path: string = 'LiquidityAssetProvider::root.'): Error | null => { + if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') + if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + + if (typeof o.pubkey !== 'string') return new Error(`${path}.pubkey: is not a string`) + if (opts.pubkey_CustomCheck && !opts.pubkey_CustomCheck(o.pubkey)) return new Error(`${path}.pubkey: custom check failed`) + + if (typeof o.tracked === 'object' || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('tracked')) { + const trackedErr = TrackedLiquidityProviderValidate(o.tracked, opts.tracked_Options, `${path}.tracked`) + if (trackedErr !== null) return trackedErr + } + + + return null +} + export type LiveDebitRequest = { debit: LiveDebitRequest_debit npub: string @@ -2220,6 +2604,33 @@ export const LiveUserOperationValidate = (o?: LiveUserOperation, opts: LiveUserO return null } +export type LndAssetProvider = { + pubkey: string + tracked?: TrackedLndProvider +} +export type LndAssetProviderOptionalField = 'tracked' +export const LndAssetProviderOptionalFields: LndAssetProviderOptionalField[] = ['tracked'] +export type LndAssetProviderOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: LndAssetProviderOptionalField[] + pubkey_CustomCheck?: (v: string) => boolean + tracked_Options?: TrackedLndProviderOptions +} +export const LndAssetProviderValidate = (o?: LndAssetProvider, opts: LndAssetProviderOptions = {}, path: string = 'LndAssetProvider::root.'): Error | null => { + if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') + if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + + if (typeof o.pubkey !== 'string') return new Error(`${path}.pubkey: is not a string`) + if (opts.pubkey_CustomCheck && !opts.pubkey_CustomCheck(o.pubkey)) return new Error(`${path}.pubkey: custom check failed`) + + if (typeof o.tracked === 'object' || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('tracked')) { + const trackedErr = TrackedLndProviderValidate(o.tracked, opts.tracked_Options, `${path}.tracked`) + if (trackedErr !== null) return trackedErr + } + + + return null +} + export type LndChannels = { open_channels: OpenChannel[] } @@ -3308,6 +3719,35 @@ export const PayAddressResponseValidate = (o?: PayAddressResponse, opts: PayAddr return null } +export type PayAdminInvoiceSwapRequest = { + no_claim?: boolean + sat_per_v_byte: number + swap_operation_id: string +} +export type PayAdminInvoiceSwapRequestOptionalField = 'no_claim' +export const PayAdminInvoiceSwapRequestOptionalFields: PayAdminInvoiceSwapRequestOptionalField[] = ['no_claim'] +export type PayAdminInvoiceSwapRequestOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: PayAdminInvoiceSwapRequestOptionalField[] + no_claim_CustomCheck?: (v?: boolean) => boolean + sat_per_v_byte_CustomCheck?: (v: number) => boolean + swap_operation_id_CustomCheck?: (v: string) => boolean +} +export const PayAdminInvoiceSwapRequestValidate = (o?: PayAdminInvoiceSwapRequest, opts: PayAdminInvoiceSwapRequestOptions = {}, path: string = 'PayAdminInvoiceSwapRequest::root.'): Error | null => { + if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') + if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + + if ((o.no_claim || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('no_claim')) && typeof o.no_claim !== 'boolean') return new Error(`${path}.no_claim: is not a boolean`) + if (opts.no_claim_CustomCheck && !opts.no_claim_CustomCheck(o.no_claim)) return new Error(`${path}.no_claim: custom check failed`) + + if (typeof o.sat_per_v_byte !== 'number') return new Error(`${path}.sat_per_v_byte: is not a number`) + if (opts.sat_per_v_byte_CustomCheck && !opts.sat_per_v_byte_CustomCheck(o.sat_per_v_byte)) return new Error(`${path}.sat_per_v_byte: custom check failed`) + + if (typeof o.swap_operation_id !== 'string') return new Error(`${path}.swap_operation_id: is not a string`) + if (opts.swap_operation_id_CustomCheck && !opts.swap_operation_id_CustomCheck(o.swap_operation_id)) return new Error(`${path}.swap_operation_id: custom check failed`) + + return null +} + export type PayAdminTransactionSwapRequest = { address: string swap_operation_id: string @@ -3600,6 +4040,76 @@ export const ProvidersDisruptionValidate = (o?: ProvidersDisruption, opts: Provi return null } +export type PushNotificationEnvelope = { + app_npub_hex: string + encrypted_payload: string + topic_id: string +} +export const PushNotificationEnvelopeOptionalFields: [] = [] +export type PushNotificationEnvelopeOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: [] + app_npub_hex_CustomCheck?: (v: string) => boolean + encrypted_payload_CustomCheck?: (v: string) => boolean + topic_id_CustomCheck?: (v: string) => boolean +} +export const PushNotificationEnvelopeValidate = (o?: PushNotificationEnvelope, opts: PushNotificationEnvelopeOptions = {}, path: string = 'PushNotificationEnvelope::root.'): Error | null => { + if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') + if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + + if (typeof o.app_npub_hex !== 'string') return new Error(`${path}.app_npub_hex: is not a string`) + if (opts.app_npub_hex_CustomCheck && !opts.app_npub_hex_CustomCheck(o.app_npub_hex)) return new Error(`${path}.app_npub_hex: custom check failed`) + + if (typeof o.encrypted_payload !== 'string') return new Error(`${path}.encrypted_payload: is not a string`) + if (opts.encrypted_payload_CustomCheck && !opts.encrypted_payload_CustomCheck(o.encrypted_payload)) return new Error(`${path}.encrypted_payload: custom check failed`) + + if (typeof o.topic_id !== 'string') return new Error(`${path}.topic_id: is not a string`) + if (opts.topic_id_CustomCheck && !opts.topic_id_CustomCheck(o.topic_id)) return new Error(`${path}.topic_id: custom check failed`) + + return null +} + +export type PushNotificationPayload = { + data: PushNotificationPayload_data +} +export const PushNotificationPayloadOptionalFields: [] = [] +export type PushNotificationPayloadOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: [] + data_Options?: PushNotificationPayload_dataOptions +} +export const PushNotificationPayloadValidate = (o?: PushNotificationPayload, opts: PushNotificationPayloadOptions = {}, path: string = 'PushNotificationPayload::root.'): Error | null => { + if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') + if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + + const dataErr = PushNotificationPayload_dataValidate(o.data, opts.data_Options, `${path}.data`) + if (dataErr !== null) return dataErr + + + return null +} + +export type RefundAdminInvoiceSwapRequest = { + sat_per_v_byte: number + swap_operation_id: string +} +export const RefundAdminInvoiceSwapRequestOptionalFields: [] = [] +export type RefundAdminInvoiceSwapRequestOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: [] + sat_per_v_byte_CustomCheck?: (v: number) => boolean + swap_operation_id_CustomCheck?: (v: string) => boolean +} +export const RefundAdminInvoiceSwapRequestValidate = (o?: RefundAdminInvoiceSwapRequest, opts: RefundAdminInvoiceSwapRequestOptions = {}, path: string = 'RefundAdminInvoiceSwapRequest::root.'): Error | null => { + if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') + if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + + if (typeof o.sat_per_v_byte !== 'number') return new Error(`${path}.sat_per_v_byte: is not a number`) + if (opts.sat_per_v_byte_CustomCheck && !opts.sat_per_v_byte_CustomCheck(o.sat_per_v_byte)) return new Error(`${path}.sat_per_v_byte: custom check failed`) + + if (typeof o.swap_operation_id !== 'string') return new Error(`${path}.swap_operation_id: is not a string`) + if (opts.swap_operation_id_CustomCheck && !opts.swap_operation_id_CustomCheck(o.swap_operation_id)) return new Error(`${path}.swap_operation_id: custom check failed`) + + return null +} + export type RelaysMigration = { relays: string[] } @@ -3917,79 +4427,146 @@ export const SingleMetricReqValidate = (o?: SingleMetricReq, opts: SingleMetricR return null } -export type SwapOperation = { - address_paid: string - failure_reason?: string - operation_payment?: UserOperation - swap_operation_id: string +export type TrackedLiquidityProvider = { + balance: number + invoices: AssetOperation[] + payments: AssetOperation[] } -export type SwapOperationOptionalField = 'failure_reason' | 'operation_payment' -export const SwapOperationOptionalFields: SwapOperationOptionalField[] = ['failure_reason', 'operation_payment'] -export type SwapOperationOptions = OptionsBaseMessage & { - checkOptionalsAreSet?: SwapOperationOptionalField[] - address_paid_CustomCheck?: (v: string) => boolean - failure_reason_CustomCheck?: (v?: string) => boolean - operation_payment_Options?: UserOperationOptions - swap_operation_id_CustomCheck?: (v: string) => boolean +export const TrackedLiquidityProviderOptionalFields: [] = [] +export type TrackedLiquidityProviderOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: [] + balance_CustomCheck?: (v: number) => boolean + invoices_ItemOptions?: AssetOperationOptions + invoices_CustomCheck?: (v: AssetOperation[]) => boolean + payments_ItemOptions?: AssetOperationOptions + payments_CustomCheck?: (v: AssetOperation[]) => boolean } -export const SwapOperationValidate = (o?: SwapOperation, opts: SwapOperationOptions = {}, path: string = 'SwapOperation::root.'): Error | null => { +export const TrackedLiquidityProviderValidate = (o?: TrackedLiquidityProvider, opts: TrackedLiquidityProviderOptions = {}, path: string = 'TrackedLiquidityProvider::root.'): Error | null => { if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') - if (typeof o.address_paid !== 'string') return new Error(`${path}.address_paid: is not a string`) - if (opts.address_paid_CustomCheck && !opts.address_paid_CustomCheck(o.address_paid)) return new Error(`${path}.address_paid: custom check failed`) + if (typeof o.balance !== 'number') return new Error(`${path}.balance: is not a number`) + if (opts.balance_CustomCheck && !opts.balance_CustomCheck(o.balance)) return new Error(`${path}.balance: custom check failed`) - if ((o.failure_reason || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('failure_reason')) && typeof o.failure_reason !== 'string') return new Error(`${path}.failure_reason: is not a string`) - if (opts.failure_reason_CustomCheck && !opts.failure_reason_CustomCheck(o.failure_reason)) return new Error(`${path}.failure_reason: custom check failed`) - - if (typeof o.operation_payment === 'object' || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('operation_payment')) { - const operation_paymentErr = UserOperationValidate(o.operation_payment, opts.operation_payment_Options, `${path}.operation_payment`) - if (operation_paymentErr !== null) return operation_paymentErr + if (!Array.isArray(o.invoices)) return new Error(`${path}.invoices: is not an array`) + for (let index = 0; index < o.invoices.length; index++) { + const invoicesErr = AssetOperationValidate(o.invoices[index], opts.invoices_ItemOptions, `${path}.invoices[${index}]`) + if (invoicesErr !== null) return invoicesErr } - + if (opts.invoices_CustomCheck && !opts.invoices_CustomCheck(o.invoices)) return new Error(`${path}.invoices: custom check failed`) - if (typeof o.swap_operation_id !== 'string') return new Error(`${path}.swap_operation_id: is not a string`) - if (opts.swap_operation_id_CustomCheck && !opts.swap_operation_id_CustomCheck(o.swap_operation_id)) return new Error(`${path}.swap_operation_id: custom check failed`) + if (!Array.isArray(o.payments)) return new Error(`${path}.payments: is not an array`) + for (let index = 0; index < o.payments.length; index++) { + const paymentsErr = AssetOperationValidate(o.payments[index], opts.payments_ItemOptions, `${path}.payments[${index}]`) + if (paymentsErr !== null) return paymentsErr + } + if (opts.payments_CustomCheck && !opts.payments_CustomCheck(o.payments)) return new Error(`${path}.payments: custom check failed`) return null } -export type SwapsList = { - quotes: TransactionSwapQuote[] - swaps: SwapOperation[] +export type TrackedLndProvider = { + channels_balance: number + confirmed_balance: number + incoming_tx: AssetOperation[] + invoices: AssetOperation[] + outgoing_tx: AssetOperation[] + payments: AssetOperation[] + unconfirmed_balance: number } -export const SwapsListOptionalFields: [] = [] -export type SwapsListOptions = OptionsBaseMessage & { +export const TrackedLndProviderOptionalFields: [] = [] +export type TrackedLndProviderOptions = OptionsBaseMessage & { checkOptionalsAreSet?: [] - quotes_ItemOptions?: TransactionSwapQuoteOptions - quotes_CustomCheck?: (v: TransactionSwapQuote[]) => boolean - swaps_ItemOptions?: SwapOperationOptions - swaps_CustomCheck?: (v: SwapOperation[]) => boolean + channels_balance_CustomCheck?: (v: number) => boolean + confirmed_balance_CustomCheck?: (v: number) => boolean + incoming_tx_ItemOptions?: AssetOperationOptions + incoming_tx_CustomCheck?: (v: AssetOperation[]) => boolean + invoices_ItemOptions?: AssetOperationOptions + invoices_CustomCheck?: (v: AssetOperation[]) => boolean + outgoing_tx_ItemOptions?: AssetOperationOptions + outgoing_tx_CustomCheck?: (v: AssetOperation[]) => boolean + payments_ItemOptions?: AssetOperationOptions + payments_CustomCheck?: (v: AssetOperation[]) => boolean + unconfirmed_balance_CustomCheck?: (v: number) => boolean } -export const SwapsListValidate = (o?: SwapsList, opts: SwapsListOptions = {}, path: string = 'SwapsList::root.'): Error | null => { +export const TrackedLndProviderValidate = (o?: TrackedLndProvider, opts: TrackedLndProviderOptions = {}, path: string = 'TrackedLndProvider::root.'): Error | null => { if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') - if (!Array.isArray(o.quotes)) return new Error(`${path}.quotes: is not an array`) - for (let index = 0; index < o.quotes.length; index++) { - const quotesErr = TransactionSwapQuoteValidate(o.quotes[index], opts.quotes_ItemOptions, `${path}.quotes[${index}]`) - if (quotesErr !== null) return quotesErr - } - if (opts.quotes_CustomCheck && !opts.quotes_CustomCheck(o.quotes)) return new Error(`${path}.quotes: custom check failed`) + if (typeof o.channels_balance !== 'number') return new Error(`${path}.channels_balance: is not a number`) + if (opts.channels_balance_CustomCheck && !opts.channels_balance_CustomCheck(o.channels_balance)) return new Error(`${path}.channels_balance: custom check failed`) - if (!Array.isArray(o.swaps)) return new Error(`${path}.swaps: is not an array`) - for (let index = 0; index < o.swaps.length; index++) { - const swapsErr = SwapOperationValidate(o.swaps[index], opts.swaps_ItemOptions, `${path}.swaps[${index}]`) - if (swapsErr !== null) return swapsErr + if (typeof o.confirmed_balance !== 'number') return new Error(`${path}.confirmed_balance: is not a number`) + if (opts.confirmed_balance_CustomCheck && !opts.confirmed_balance_CustomCheck(o.confirmed_balance)) return new Error(`${path}.confirmed_balance: custom check failed`) + + if (!Array.isArray(o.incoming_tx)) return new Error(`${path}.incoming_tx: is not an array`) + for (let index = 0; index < o.incoming_tx.length; index++) { + const incoming_txErr = AssetOperationValidate(o.incoming_tx[index], opts.incoming_tx_ItemOptions, `${path}.incoming_tx[${index}]`) + if (incoming_txErr !== null) return incoming_txErr } - if (opts.swaps_CustomCheck && !opts.swaps_CustomCheck(o.swaps)) return new Error(`${path}.swaps: custom check failed`) + if (opts.incoming_tx_CustomCheck && !opts.incoming_tx_CustomCheck(o.incoming_tx)) return new Error(`${path}.incoming_tx: custom check failed`) + + if (!Array.isArray(o.invoices)) return new Error(`${path}.invoices: is not an array`) + for (let index = 0; index < o.invoices.length; index++) { + const invoicesErr = AssetOperationValidate(o.invoices[index], opts.invoices_ItemOptions, `${path}.invoices[${index}]`) + if (invoicesErr !== null) return invoicesErr + } + if (opts.invoices_CustomCheck && !opts.invoices_CustomCheck(o.invoices)) return new Error(`${path}.invoices: custom check failed`) + + if (!Array.isArray(o.outgoing_tx)) return new Error(`${path}.outgoing_tx: is not an array`) + for (let index = 0; index < o.outgoing_tx.length; index++) { + const outgoing_txErr = AssetOperationValidate(o.outgoing_tx[index], opts.outgoing_tx_ItemOptions, `${path}.outgoing_tx[${index}]`) + if (outgoing_txErr !== null) return outgoing_txErr + } + if (opts.outgoing_tx_CustomCheck && !opts.outgoing_tx_CustomCheck(o.outgoing_tx)) return new Error(`${path}.outgoing_tx: custom check failed`) + + if (!Array.isArray(o.payments)) return new Error(`${path}.payments: is not an array`) + for (let index = 0; index < o.payments.length; index++) { + const paymentsErr = AssetOperationValidate(o.payments[index], opts.payments_ItemOptions, `${path}.payments[${index}]`) + if (paymentsErr !== null) return paymentsErr + } + if (opts.payments_CustomCheck && !opts.payments_CustomCheck(o.payments)) return new Error(`${path}.payments: custom check failed`) + + if (typeof o.unconfirmed_balance !== 'number') return new Error(`${path}.unconfirmed_balance: is not a number`) + if (opts.unconfirmed_balance_CustomCheck && !opts.unconfirmed_balance_CustomCheck(o.unconfirmed_balance)) return new Error(`${path}.unconfirmed_balance: custom check failed`) + + return null +} + +export type TrackedOperation = { + amount: number + ts: number + type: TrackedOperationType +} +export const TrackedOperationOptionalFields: [] = [] +export type TrackedOperationOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: [] + amount_CustomCheck?: (v: number) => boolean + ts_CustomCheck?: (v: number) => boolean + type_CustomCheck?: (v: TrackedOperationType) => boolean +} +export const TrackedOperationValidate = (o?: TrackedOperation, opts: TrackedOperationOptions = {}, path: string = 'TrackedOperation::root.'): Error | null => { + if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') + if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + + if (typeof o.amount !== 'number') return new Error(`${path}.amount: is not a number`) + if (opts.amount_CustomCheck && !opts.amount_CustomCheck(o.amount)) return new Error(`${path}.amount: custom check failed`) + + if (typeof o.ts !== 'number') return new Error(`${path}.ts: is not a number`) + if (opts.ts_CustomCheck && !opts.ts_CustomCheck(o.ts)) return new Error(`${path}.ts: custom check failed`) + + if (!enumCheckTrackedOperationType(o.type)) return new Error(`${path}.type: is not a valid TrackedOperationType`) + if (opts.type_CustomCheck && !opts.type_CustomCheck(o.type)) return new Error(`${path}.type: custom check failed`) return null } export type 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 @@ -4000,7 +4577,10 @@ export const TransactionSwapQuoteOptionalFields: [] = [] export type TransactionSwapQuoteOptions = OptionsBaseMessage & { checkOptionalsAreSet?: [] chain_fee_sats_CustomCheck?: (v: number) => boolean + completed_at_unix_CustomCheck?: (v: number) => boolean + expires_at_block_height_CustomCheck?: (v: number) => boolean invoice_amount_sats_CustomCheck?: (v: number) => boolean + paid_at_unix_CustomCheck?: (v: number) => boolean service_fee_sats_CustomCheck?: (v: number) => boolean service_url_CustomCheck?: (v: string) => boolean swap_fee_sats_CustomCheck?: (v: number) => boolean @@ -4014,9 +4594,18 @@ export const TransactionSwapQuoteValidate = (o?: TransactionSwapQuote, opts: Tra if (typeof o.chain_fee_sats !== 'number') return new Error(`${path}.chain_fee_sats: is not a number`) if (opts.chain_fee_sats_CustomCheck && !opts.chain_fee_sats_CustomCheck(o.chain_fee_sats)) return new Error(`${path}.chain_fee_sats: custom check failed`) + if (typeof o.completed_at_unix !== 'number') return new Error(`${path}.completed_at_unix: is not a number`) + if (opts.completed_at_unix_CustomCheck && !opts.completed_at_unix_CustomCheck(o.completed_at_unix)) return new Error(`${path}.completed_at_unix: custom check failed`) + + if (typeof o.expires_at_block_height !== 'number') return new Error(`${path}.expires_at_block_height: is not a number`) + if (opts.expires_at_block_height_CustomCheck && !opts.expires_at_block_height_CustomCheck(o.expires_at_block_height)) return new Error(`${path}.expires_at_block_height: custom check failed`) + if (typeof o.invoice_amount_sats !== 'number') return new Error(`${path}.invoice_amount_sats: is not a number`) if (opts.invoice_amount_sats_CustomCheck && !opts.invoice_amount_sats_CustomCheck(o.invoice_amount_sats)) return new Error(`${path}.invoice_amount_sats: custom check failed`) + if (typeof o.paid_at_unix !== 'number') return new Error(`${path}.paid_at_unix: is not a number`) + if (opts.paid_at_unix_CustomCheck && !opts.paid_at_unix_CustomCheck(o.paid_at_unix)) return new Error(`${path}.paid_at_unix: custom check failed`) + if (typeof o.service_fee_sats !== 'number') return new Error(`${path}.service_fee_sats: is not a number`) if (opts.service_fee_sats_CustomCheck && !opts.service_fee_sats_CustomCheck(o.service_fee_sats)) return new Error(`${path}.service_fee_sats: custom check failed`) @@ -4076,6 +4665,72 @@ export const TransactionSwapRequestValidate = (o?: TransactionSwapRequest, opts: return null } +export type TxSwapOperation = { + address_paid?: string + failure_reason?: string + operation_payment?: UserOperation + quote: TransactionSwapQuote + tx_id?: string +} +export type TxSwapOperationOptionalField = 'address_paid' | 'failure_reason' | 'operation_payment' | 'tx_id' +export const TxSwapOperationOptionalFields: TxSwapOperationOptionalField[] = ['address_paid', 'failure_reason', 'operation_payment', 'tx_id'] +export type TxSwapOperationOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: TxSwapOperationOptionalField[] + address_paid_CustomCheck?: (v?: string) => boolean + failure_reason_CustomCheck?: (v?: string) => boolean + operation_payment_Options?: UserOperationOptions + quote_Options?: TransactionSwapQuoteOptions + tx_id_CustomCheck?: (v?: string) => boolean +} +export const TxSwapOperationValidate = (o?: TxSwapOperation, opts: TxSwapOperationOptions = {}, path: string = 'TxSwapOperation::root.'): Error | null => { + if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') + if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + + if ((o.address_paid || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('address_paid')) && typeof o.address_paid !== 'string') return new Error(`${path}.address_paid: is not a string`) + if (opts.address_paid_CustomCheck && !opts.address_paid_CustomCheck(o.address_paid)) return new Error(`${path}.address_paid: custom check failed`) + + if ((o.failure_reason || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('failure_reason')) && typeof o.failure_reason !== 'string') return new Error(`${path}.failure_reason: is not a string`) + if (opts.failure_reason_CustomCheck && !opts.failure_reason_CustomCheck(o.failure_reason)) return new Error(`${path}.failure_reason: custom check failed`) + + if (typeof o.operation_payment === 'object' || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('operation_payment')) { + const operation_paymentErr = UserOperationValidate(o.operation_payment, opts.operation_payment_Options, `${path}.operation_payment`) + if (operation_paymentErr !== null) return operation_paymentErr + } + + + const quoteErr = TransactionSwapQuoteValidate(o.quote, opts.quote_Options, `${path}.quote`) + if (quoteErr !== null) return quoteErr + + + if ((o.tx_id || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('tx_id')) && typeof o.tx_id !== 'string') return new Error(`${path}.tx_id: is not a string`) + if (opts.tx_id_CustomCheck && !opts.tx_id_CustomCheck(o.tx_id)) return new Error(`${path}.tx_id: custom check failed`) + + return null +} + +export type TxSwapsList = { + swaps: TxSwapOperation[] +} +export const TxSwapsListOptionalFields: [] = [] +export type TxSwapsListOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: [] + swaps_ItemOptions?: TxSwapOperationOptions + swaps_CustomCheck?: (v: TxSwapOperation[]) => boolean +} +export const TxSwapsListValidate = (o?: TxSwapsList, opts: TxSwapsListOptions = {}, path: string = 'TxSwapsList::root.'): Error | null => { + if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') + if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + + if (!Array.isArray(o.swaps)) return new Error(`${path}.swaps: is not an array`) + for (let index = 0; index < o.swaps.length; index++) { + const swapsErr = TxSwapOperationValidate(o.swaps[index], opts.swaps_ItemOptions, `${path}.swaps[${index}]`) + if (swapsErr !== null) return swapsErr + } + if (opts.swaps_CustomCheck && !opts.swaps_CustomCheck(o.swaps)) return new Error(`${path}.swaps: custom check failed`) + + return null +} + export type UpdateChannelPolicyRequest = { policy: ChannelPolicy update: UpdateChannelPolicyRequest_update @@ -4273,6 +4928,7 @@ export type UserInfo = { nmanage: string noffer: string service_fee_bps: number + topic_id: string userId: string user_identifier: string } @@ -4289,6 +4945,7 @@ export type UserInfoOptions = OptionsBaseMessage & { nmanage_CustomCheck?: (v: string) => boolean noffer_CustomCheck?: (v: string) => boolean service_fee_bps_CustomCheck?: (v: number) => boolean + topic_id_CustomCheck?: (v: string) => boolean userId_CustomCheck?: (v: string) => boolean user_identifier_CustomCheck?: (v: string) => boolean } @@ -4326,6 +4983,9 @@ export const UserInfoValidate = (o?: UserInfo, opts: UserInfoOptions = {}, path: if (typeof o.service_fee_bps !== 'number') return new Error(`${path}.service_fee_bps: is not a number`) if (opts.service_fee_bps_CustomCheck && !opts.service_fee_bps_CustomCheck(o.service_fee_bps)) return new Error(`${path}.service_fee_bps: custom check failed`) + if (typeof o.topic_id !== 'string') return new Error(`${path}.topic_id: is not a string`) + if (opts.topic_id_CustomCheck && !opts.topic_id_CustomCheck(o.topic_id)) return new Error(`${path}.topic_id: custom check failed`) + if (typeof o.userId !== 'string') return new Error(`${path}.userId: is not a string`) if (opts.userId_CustomCheck && !opts.userId_CustomCheck(o.userId)) return new Error(`${path}.userId: custom check failed`) @@ -4743,6 +5403,43 @@ export const NPubLinking_stateValidate = (o?: NPubLinking_state, opts:NPubLinkin if (unlinkedErr !== null) return unlinkedErr + break + default: + return new Error(path + ': unknown type '+ stringType) + } + return null +} +export enum PushNotificationPayload_data_type { + RECEIVED_OPERATION = 'received_operation', + SENT_OPERATION = 'sent_operation', +} +export const enumCheckPushNotificationPayload_data_type = (e?: PushNotificationPayload_data_type): boolean => { + for (const v in PushNotificationPayload_data_type) if (e === v) return true + return false +} +export type PushNotificationPayload_data = + {type:PushNotificationPayload_data_type.RECEIVED_OPERATION, received_operation:UserOperation}| + {type:PushNotificationPayload_data_type.SENT_OPERATION, sent_operation:UserOperation} + +export type PushNotificationPayload_dataOptions = { + received_operation_Options?: UserOperationOptions + sent_operation_Options?: UserOperationOptions +} +export const PushNotificationPayload_dataValidate = (o?: PushNotificationPayload_data, opts:PushNotificationPayload_dataOptions = {}, path: string = 'PushNotificationPayload_data::root.'): Error | null => { + if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + const stringType: string = o.type + switch (o.type) { + case PushNotificationPayload_data_type.RECEIVED_OPERATION: + const received_operationErr = UserOperationValidate(o.received_operation, opts.received_operation_Options, `${path}.received_operation`) + if (received_operationErr !== null) return received_operationErr + + + break + case PushNotificationPayload_data_type.SENT_OPERATION: + const sent_operationErr = UserOperationValidate(o.sent_operation, opts.sent_operation_Options, `${path}.sent_operation`) + if (sent_operationErr !== null) return sent_operationErr + + break default: return new Error(path + ': unknown type '+ stringType) diff --git a/proto/service/methods.proto b/proto/service/methods.proto index feaf3e97..2a54767b 100644 --- a/proto/service/methods.proto +++ b/proto/service/methods.proto @@ -112,6 +112,20 @@ service LightningPub { option (nostr) = true; }; + rpc GetAssetsAndLiabilities(structs.AssetsAndLiabilitiesReq) returns (structs.AssetsAndLiabilities) { + option (auth_type) = "Admin"; + option (http_method) = "post"; + option (http_route) = "/api/admin/assets/liabilities"; + option (nostr) = true; + }; + + rpc BumpTx(structs.BumpTx) returns (structs.Empty) { + option (auth_type) = "Admin"; + option (http_method) = "post"; + option (http_route) = "/api/admin/tx/bump"; + option (nostr) = true; + }; + rpc AddApp(structs.AddAppRequest) returns (structs.AuthApp) { option (auth_type) = "Admin"; option (http_method) = "post"; @@ -175,6 +189,34 @@ service LightningPub { option (nostr) = true; } + rpc GetAdminInvoiceSwapQuotes(structs.InvoiceSwapRequest) returns (structs.InvoiceSwapQuoteList) { + option (auth_type) = "Admin"; + option (http_method) = "post"; + option (http_route) = "/api/admin/swap/invoice/quote"; + option (nostr) = true; + } + + rpc ListAdminInvoiceSwaps(structs.Empty) returns (structs.InvoiceSwapsList) { + option (auth_type) = "Admin"; + option (http_method) = "post"; + option (http_route) = "/api/admin/swap/invoice/list"; + option (nostr) = true; + } + + rpc PayAdminInvoiceSwap(structs.PayAdminInvoiceSwapRequest) returns (structs.AdminInvoiceSwapResponse) { + option (auth_type) = "Admin"; + option (http_method) = "post"; + option (http_route) = "/api/admin/swap/invoice/pay"; + option (nostr) = true; + } + + rpc RefundAdminInvoiceSwap(structs.RefundAdminInvoiceSwapRequest) returns (structs.AdminInvoiceSwapResponse) { + option (auth_type) = "Admin"; + option (http_method) = "post"; + option (http_route) = "/api/admin/swap/invoice/refund"; + option (nostr) = true; + } + rpc GetAdminTransactionSwapQuotes(structs.TransactionSwapRequest) returns (structs.TransactionSwapQuoteList) { option (auth_type) = "Admin"; option (http_method) = "post"; @@ -182,17 +224,17 @@ service LightningPub { option (nostr) = true; } - rpc PayAdminTransactionSwap(structs.PayAdminTransactionSwapRequest) returns (structs.AdminSwapResponse) { + rpc PayAdminTransactionSwap(structs.PayAdminTransactionSwapRequest) returns (structs.AdminTxSwapResponse) { option (auth_type) = "Admin"; option (http_method) = "post"; option (http_route) = "/api/admin/swap/transaction/pay"; option (nostr) = true; } - rpc ListAdminSwaps(structs.Empty) returns (structs.SwapsList) { + rpc ListAdminTxSwaps(structs.Empty) returns (structs.TxSwapsList) { option (auth_type) = "Admin"; option (http_method) = "post"; - option (http_route) = "/api/admin/swap/list"; + option (http_route) = "/api/admin/swap/transaction/list"; option (nostr) = true; } @@ -520,14 +562,14 @@ service LightningPub { rpc GetTransactionSwapQuotes(structs.TransactionSwapRequest) returns (structs.TransactionSwapQuoteList){ option (auth_type) = "User"; option (http_method) = "post"; - option (http_route) = "/api/user/swap/quote"; + option (http_route) = "/api/user/swap/transaction/quote"; option (nostr) = true; } - rpc ListSwaps(structs.Empty) returns (structs.SwapsList){ + rpc ListTxSwaps(structs.Empty) returns (structs.TxSwapsList){ option (auth_type) = "User"; option (http_method) = "post"; - option (http_route) = "/api/user/swap/list"; + option (http_route) = "/api/user/swap/transaction/list"; option (nostr) = true; } diff --git a/proto/service/structs.proto b/proto/service/structs.proto index 00552fdd..3d64ef67 100644 --- a/proto/service/structs.proto +++ b/proto/service/structs.proto @@ -19,6 +19,68 @@ message EncryptionExchangeRequest { string deviceId = 2; } +message AssetsAndLiabilitiesReq { + optional int64 limit_invoices = 1; + optional int64 limit_payments = 2; + optional int64 limit_providers = 4; +} + +message BumpTx { + string txid = 1; + int64 output_index = 2; + int64 sat_per_vbyte = 3; +} + + +enum TrackedOperationType { + USER = 0; + ROOT = 1; +} + +message TrackedOperation { + int64 ts = 1; + int64 amount = 2; + TrackedOperationType type = 3; +} + +message AssetOperation { + int64 ts = 1; + int64 amount = 2; + optional TrackedOperation tracked = 3; +} + +message TrackedLndProvider { + int64 confirmed_balance = 1; + int64 unconfirmed_balance = 2; + int64 channels_balance = 3; + repeated AssetOperation payments = 4; + repeated AssetOperation invoices = 5; + repeated AssetOperation incoming_tx = 6; + repeated AssetOperation outgoing_tx = 7; +} + +message TrackedLiquidityProvider { + int64 balance = 1; + repeated AssetOperation payments = 2; + repeated AssetOperation invoices = 3; +} + +message LndAssetProvider { + string pubkey = 1; + optional TrackedLndProvider tracked = 2; +} + +message LiquidityAssetProvider { + string pubkey = 1; + optional TrackedLiquidityProvider tracked = 2; +} + +message AssetsAndLiabilities { + int64 users_balance = 1; + repeated LndAssetProvider lnds = 2; + repeated LiquidityAssetProvider liquidity_providers = 3; +} + message UserHealthState { string downtime_reason = 1; } @@ -37,6 +99,8 @@ message ErrorStats { ErrorStat past1m = 5; } + + message MetricsFile { } @@ -541,6 +605,7 @@ message UserInfo{ string callback_url = 10; string bridge_url = 11; string nmanage = 12; + string topic_id = 13; } @@ -833,6 +898,56 @@ message MessagingToken { string firebase_messaging_token = 2; } +message InvoiceSwapRequest { + int64 amount_sats = 1; +} + +message InvoiceSwapQuote { + string swap_operation_id = 1; + string invoice = 2; + int64 invoice_amount_sats = 3; + string address = 4; + int64 transaction_amount_sats = 5; + int64 chain_fee_sats = 6; + int64 service_fee_sats = 7; + string service_url = 8; + int64 swap_fee_sats = 9; + string tx_id = 10; + int64 paid_at_unix = 11; + int64 expires_at_block_height = 12; +} + +message InvoiceSwapQuoteList { + repeated InvoiceSwapQuote quotes = 1; +} + +message InvoiceSwapOperation { + InvoiceSwapQuote quote = 1; + optional UserOperation operation_payment = 2; + optional string failure_reason = 3; + optional int64 completed_at_unix = 6; +} + +message InvoiceSwapsList { + repeated InvoiceSwapOperation swaps = 1; + int64 current_block_height = 3; +} + +message RefundAdminInvoiceSwapRequest { + string swap_operation_id = 1; + int64 sat_per_v_byte = 2; +} + +message PayAdminInvoiceSwapRequest { + string swap_operation_id = 1; + int64 sat_per_v_byte = 2; + optional bool no_claim = 3; +} + +message AdminInvoiceSwapResponse { + string tx_id = 1; +} + message TransactionSwapRequest { int64 transaction_amount_sats = 2; } @@ -851,27 +966,31 @@ message TransactionSwapQuote { int64 chain_fee_sats = 5; int64 service_fee_sats = 7; string service_url = 8; + + int64 expires_at_block_height = 9; + int64 paid_at_unix = 10; + int64 completed_at_unix = 11; } message TransactionSwapQuoteList { repeated TransactionSwapQuote quotes = 1; } -message AdminSwapResponse { +message AdminTxSwapResponse { string tx_id = 1; int64 network_fee = 2; } -message SwapOperation { - string swap_operation_id = 1; +message TxSwapOperation { + TransactionSwapQuote quote = 1; optional UserOperation operation_payment = 2; optional string failure_reason = 3; - string address_paid = 4; + optional string address_paid = 4; + optional string tx_id = 5; } -message SwapsList { - repeated SwapOperation swaps = 1; - repeated TransactionSwapQuote quotes = 2; +message TxSwapsList { + repeated TxSwapOperation swaps = 1; } message CumulativeFees { @@ -885,4 +1004,19 @@ message BeaconData { optional string avatarUrl = 3; optional string nextRelay = 4; optional CumulativeFees fees = 5; +} + + +message PushNotificationEnvelope { + string topic_id = 1; + string app_npub_hex = 2; + string encrypted_payload = 3; // encrypted PushNotificationPayload +} + + +message PushNotificationPayload { + oneof data { + UserOperation received_operation = 1; + UserOperation sent_operation = 2; + } } \ No newline at end of file diff --git a/src/nostrMiddleware.ts b/src/nostrMiddleware.ts index 034dbce8..4dd3b281 100644 --- a/src/nostrMiddleware.ts +++ b/src/nostrMiddleware.ts @@ -105,7 +105,7 @@ export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSett return { Stop: () => { mainHandler.adminManager.setNostrConnected(false); return nostr.Stop }, - Send: (...args) => nostr.Send(...args), + Send: async (...args) => nostr.Send(...args), Ping: () => nostr.Ping(), Reset: (settings: NostrSettings) => nostr.Reset(settings) } diff --git a/src/services/helpers/logger.ts b/src/services/helpers/logger.ts index 4d6601c9..62a87eac 100644 --- a/src/services/helpers/logger.ts +++ b/src/services/helpers/logger.ts @@ -1,17 +1,17 @@ import fs from 'fs' export const DEBUG = Symbol("DEBUG") export const ERROR = Symbol("ERROR") -export const WARN = Symbol("WARN") +export const INFO = Symbol("INFO") type LoggerParams = { appName?: string, userId?: string, component?: string } export type PubLogger = (...message: (string | number | object | symbol)[]) => void type Writer = (message: string) => void const logsDir = process.env.LOGS_DIR || "logs" -const logLevel = process.env.LOG_LEVEL || "DEBUG" +const logLevel = process.env.LOG_LEVEL || "INFO" try { fs.mkdirSync(logsDir) } catch { } -if (logLevel !== "DEBUG" && logLevel !== "WARN" && logLevel !== "ERROR") { - throw new Error("Invalid log level " + logLevel + " must be one of (DEBUG, WARN, ERROR)") +if (logLevel !== "DEBUG" && logLevel !== "INFO" && logLevel !== "ERROR") { + throw new Error("Invalid log level " + logLevel + " must be one of (DEBUG, INFO, ERROR)") } const z = (n: number) => n < 10 ? `0${n}` : `${n}` // Sanitize filename to remove invalid characters for filesystem @@ -67,19 +67,17 @@ export const getLogger = (params: LoggerParams): PubLogger => { } message[0] = "DEBUG" break; - case WARN: + case INFO: if (logLevel === "ERROR") { return } - message[0] = "WARN" + message[0] = "INFO" break; case ERROR: message[0] = "ERROR" break; default: - if (logLevel !== "DEBUG") { - return - } + // treats logs without a level as ERROR level, without prefix so it can be found and fixed if needed } const now = new Date() const timestamp = `${now.getFullYear()}-${z(now.getMonth() + 1)}-${z(now.getDate())} ${z(now.getHours())}:${z(now.getMinutes())}:${z(now.getSeconds())}` diff --git a/src/services/lnd/lnd.ts b/src/services/lnd/lnd.ts index daf7b411..8ccc42ee 100644 --- a/src/services/lnd/lnd.ts +++ b/src/services/lnd/lnd.ts @@ -15,7 +15,7 @@ import { AddInvoiceReq } from './addInvoiceReq.js'; import { PayInvoiceReq } from './payInvoiceReq.js'; import { SendCoinsReq } from './sendCoinsReq.js'; import { AddressPaidCb, InvoicePaidCb, NodeInfo, Invoice, DecodedInvoice, PaidInvoice, NewBlockCb, HtlcCb, BalanceInfo, ChannelEventCb } from './settings.js'; -import { ERROR, getLogger } from '../helpers/logger.js'; +import { ERROR, getLogger, DEBUG, INFO } from '../helpers/logger.js'; import { HtlcEvent_EventType } from '../../../proto/lnd/router.js'; import { LiquidityProvider } from '../main/liquidityProvider.js'; import { Utils } from '../helpers/utilsWrapper.js'; @@ -23,7 +23,7 @@ import { TxPointSettings } from '../storage/tlv/stateBundler.js'; import { WalletKitClient } from '../../../proto/lnd/walletkit.client.js'; import SettingsManager from '../main/settingsManager.js'; import { LndNodeSettings, LndSettings } from '../main/settings.js'; -import { ListAddressesResponse } from '../../../proto/lnd/walletkit.js'; +import { ListAddressesResponse, PublishResponse } from '../../../proto/lnd/walletkit.js'; const DeadLineMetadata = (deadline = 10 * 1000) => ({ deadline: Date.now() + deadline }) const deadLndRetrySeconds = 20 @@ -69,7 +69,7 @@ export default class { // Skip LND client initialization if using only liquidity provider if (liquidProvider.getSettings().useOnlyLiquidityProvider) { - this.log("USE_ONLY_LIQUIDITY_PROVIDER enabled, skipping LND client initialization") + this.log(INFO, "USE_ONLY_LIQUIDITY_PROVIDER enabled, skipping LND client initialization") // Create minimal dummy clients - they won't be used but prevent null reference errors // Use insecure credentials directly (can't combine them) const { lndAddr } = this.getSettings().lndNodeSettings @@ -126,13 +126,13 @@ export default class { } async Warmup() { + this.log(INFO, "Warming up LND") // Skip LND warmup if using only liquidity provider if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { - this.log("USE_ONLY_LIQUIDITY_PROVIDER enabled, skipping LND warmup") + this.log(INFO, "USE_ONLY_LIQUIDITY_PROVIDER enabled, skipping LND warmup") this.ready = true return } - // console.log("Warming up LND") this.SubscribeAddressPaid() this.SubscribeInvoicePaid() await this.SubscribeNewBlock() @@ -142,20 +142,32 @@ export default class { return new Promise((res, rej) => { const interval = setInterval(async () => { try { - await this.GetInfo() + const info = await this.GetInfo() + if (!info.syncedToChain || !info.syncedToGraph) { + this.log("LND responding but not synced yet, waiting...") + return + } clearInterval(interval) this.ready = true res() } catch (err) { - 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")) - } + this.log(INFO, "LND is not ready yet, will try again in 1 second") + } + if (Date.now() - now > 1000 * 60 * 10) { + clearInterval(interval) + rej(new Error("LND not synced after 10 minutes")) } }, 1000) }) } + async PublishTransaction(txHex: string): Promise { + const res = await this.walletKit.publishTransaction({ + txHex: Buffer.from(txHex, 'hex'), label: "" + }, DeadLineMetadata()) + return res.response + } + async GetInfo(): Promise { if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { // Return dummy info when bypass is enabled @@ -169,27 +181,26 @@ export default class { uris: [] } } - // console.log("Getting info") const res = await this.lightning.getInfo({}, DeadLineMetadata()) return res.response } async ListPendingChannels(): Promise { + 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 { - // console.log("Listing channels") + this.log(DEBUG, "Listing channels") const res = await this.lightning.listChannels({ activeOnly: false, inactiveOnly: false, privateOnly: false, publicOnly: false, peer: Buffer.alloc(0), peerAliasLookup: peerLookup }, DeadLineMetadata()) return res.response } async ListClosedChannels(): Promise { - // console.log("Listing closed channels") + this.log(DEBUG, "Listing closed channels") const res = await this.lightning.closedChannels({ abandoned: true, breach: true, @@ -206,7 +217,6 @@ export default class { if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { return } - // console.log("Checking health") if (!this.ready) { throw new Error("not ready") } @@ -217,69 +227,69 @@ export default class { } RestartStreams() { - // console.log("Restarting streams") + this.log(INFO, "Restarting streams") if (!this.ready || this.abortController.signal.aborted) { return } - this.log("LND is dead, will try to reconnect in", deadLndRetrySeconds, "seconds") + this.log(INFO, "LND is dead, will try to reconnect in", deadLndRetrySeconds, "seconds") const interval = setInterval(async () => { try { await this.unlockLnd() - this.log("LND is back online") + this.log(INFO, "LND is back online") clearInterval(interval) await this.Warmup() } catch (err) { - this.log("LND still dead, will try again in", deadLndRetrySeconds, "seconds") + this.log(INFO, "LND still dead, will try again in", deadLndRetrySeconds, "seconds") } }, deadLndRetrySeconds * 1000) } async SubscribeChannelEvents() { - // console.log("Subscribing to channel events") + this.log(DEBUG, "Subscribing to channel events") const stream = this.lightning.subscribeChannelEvents({}, { abort: this.abortController.signal }) stream.responses.onMessage(async channel => { const channels = await this.ListChannels() this.channelEventCb(channel, channels.channels) }) stream.responses.onError(error => { - this.log("Error with subscribeChannelEvents stream") + this.log(ERROR, "Error with subscribeChannelEvents stream") }) stream.responses.onComplete(() => { - this.log("subscribeChannelEvents stream closed") + this.log(INFO, "subscribeChannelEvents stream closed") }) } async SubscribeHtlcEvents() { - // console.log("Subscribing to htlc events") + this.log(DEBUG, "Subscribing to htlc events") const stream = this.router.subscribeHtlcEvents({}, { abort: this.abortController.signal }) stream.responses.onMessage(htlc => { this.htlcCb(htlc) }) stream.responses.onError(error => { - this.log("Error with subscribeHtlcEvents stream") + this.log(ERROR, "Error with subscribeHtlcEvents stream") }) stream.responses.onComplete(() => { - this.log("subscribeHtlcEvents stream closed") + this.log(INFO, "subscribeHtlcEvents stream closed") }) } async SubscribeNewBlock() { - // console.log("Subscribing to new block") + this.log(DEBUG, "Subscribing to new block") const { blockHeight } = await this.GetInfo() const stream = this.chainNotifier.registerBlockEpochNtfn({ height: blockHeight, hash: Buffer.alloc(0) }, { abort: this.abortController.signal }) stream.responses.onMessage(block => { this.newBlockCb(block.height) }) stream.responses.onError(error => { - this.log("Error with new block stream") + this.log(ERROR, "Error with new block stream") }) stream.responses.onComplete(() => { - this.log("new block stream closed") + this.log(INFO, "new block stream closed") }) } SubscribeAddressPaid(): void { - // console.log("Subscribing to address paid") + this.log(DEBUG, "Subscribing to address paid") const stream = this.lightning.subscribeTransactions({ account: "", endHeight: 0, @@ -298,15 +308,15 @@ export default class { } }) stream.responses.onError(error => { - this.log("Error with onchain tx stream") + this.log(ERROR, "Error with onchain tx stream") }) stream.responses.onComplete(() => { - this.log("onchain tx stream closed") + this.log(INFO, "onchain tx stream closed") }) } SubscribeInvoicePaid(): void { - // console.log("Subscribing to invoice paid") + this.log(DEBUG, "Subscribing to invoice paid") const stream = this.lightning.subscribeInvoices({ settleIndex: BigInt(this.latestKnownSettleIndex), addIndex: 0n, @@ -319,14 +329,14 @@ export default class { }) let restarted = false stream.responses.onError(error => { - this.log("Error with invoice stream") + this.log(ERROR, "Error with invoice stream") if (!restarted) { restarted = true this.RestartStreams() } }) stream.responses.onComplete(() => { - this.log("invoice stream closed") + this.log(INFO, "invoice stream closed") if (!restarted) { restarted = true this.RestartStreams() @@ -348,6 +358,7 @@ export default class { } async ListAddresses(): Promise { + 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 }) @@ -355,11 +366,11 @@ export default class { } async NewAddress(addressType: Types.AddressType, { useProvider, from }: TxActionOptions): Promise { + 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: @@ -389,11 +400,11 @@ export default class { } async NewInvoice(value: number, memo: string, expiry: number, { useProvider, from }: TxActionOptions, blind = false): Promise { - // console.log("Creating new invoice") + this.log(DEBUG, "Creating new invoice") // Force use of provider when bypass is enabled const mustUseProvider = this.liquidProvider.getSettings().useOnlyLiquidityProvider || useProvider if (mustUseProvider) { - console.log("using provider") + this.log(INFO, "using provider") const invoice = await this.liquidProvider.AddInvoice(value, memo, from, expiry) const providerPubkey = this.liquidProvider.GetProviderPubkey() return { payRequest: invoice, providerPubkey } @@ -409,6 +420,7 @@ export default class { } async DecodeInvoice(paymentRequest: string): Promise { + this.log(DEBUG, "Decoding invoice") if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { // Use light-bolt11-decoder when LND is bypassed try { @@ -434,24 +446,23 @@ export default class { throw new Error(`Failed to decode invoice: ${err.message}`) } } - // console.log("Decoding invoice") const res = await this.lightning.decodePayReq({ payReq: paymentRequest }, DeadLineMetadata()) return { numSatoshis: Number(res.response.numSatoshis), paymentHash: res.response.paymentHash } } async ChannelBalance(): Promise<{ local: number, remote: number }> { + this.log(DEBUG, "Getting channel balance") if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { return { local: 0, remote: 0 } } - // console.log("Getting channel balance") const res = await this.lightning.channelBalance({}) const r = res.response return { local: r.localBalance ? Number(r.localBalance.sat) : 0, remote: r.remoteBalance ? Number(r.remoteBalance.sat) : 0 } } async PayInvoice(invoice: string, amount: number, { routingFeeLimit, serviceFee }: { routingFeeLimit: number, serviceFee: number }, decodedAmount: number, { useProvider, from }: TxActionOptions, paymentIndexCb?: (index: number) => void): Promise { - // console.log("Paying invoice") + this.log(DEBUG, "Paying invoice") if (this.outgoingOpsLocked) { - this.log("outgoing ops locked, rejecting payment request") + this.log(ERROR, "outgoing ops locked, rejecting payment request") throw new Error("lnd node is currently out of sync") } // Force use of provider when bypass is enabled @@ -468,7 +479,7 @@ export default class { const stream = this.router.sendPaymentV2(req, { abort: abortController.signal }) return new Promise((res, rej) => { stream.responses.onError(error => { - this.log("invoice payment failed", error) + this.log(ERROR, "invoice payment failed", error) rej(error) }) let indexSent = false @@ -480,7 +491,7 @@ export default class { } switch (payment.status) { case Payment_PaymentStatus.FAILED: - this.log("invoice payment failed", payment.failureReason) + this.log(ERROR, "invoice payment failed", payment.failureReason) rej(PaymentFailureReason[payment.failureReason]) return case Payment_PaymentStatus.SUCCEEDED: @@ -498,7 +509,7 @@ export default class { } async EstimateChainFees(address: string, amount: number, targetConf: number): Promise { - // console.log("Estimating chain fees") + this.log(DEBUG, "Estimating chain fees") await this.Health() const res = await this.lightning.estimateFee({ addrToAmount: { [address]: BigInt(amount) }, @@ -511,13 +522,13 @@ export default class { } async PayAddress(address: string, amount: number, satPerVByte: number, label = "", { useProvider, from }: TxActionOptions): Promise { + this.log(DEBUG, "Paying address") // Address payments not supported when bypass is enabled if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { throw new Error("Address payments not supported when USE_ONLY_LIQUIDITY_PROVIDER is enabled") } - // console.log("Paying address") if (this.outgoingOpsLocked) { - this.log("outgoing ops locked, rejecting payment request") + this.log(ERROR, "outgoing ops locked, rejecting payment request") throw new Error("lnd node is currently out of sync") } if (useProvider) { @@ -535,19 +546,19 @@ export default class { } async GetTransactions(startHeight: number): Promise { - // console.log("Getting transactions") - const res = await this.lightning.getTransactions({ startHeight, endHeight: 0, account: "" }, DeadLineMetadata()) + this.log(DEBUG, "Getting transactions") + const res = await this.lightning.getTransactions({ startHeight, endHeight: 0, account: "", }, DeadLineMetadata()) return res.response } async GetChannelInfo(chanId: string) { - // console.log("Getting channel info") + this.log(DEBUG, "Getting channel info") const res = await this.lightning.getChanInfo({ chanId, chanPoint: "" }, DeadLineMetadata()) return res.response } async UpdateChannelPolicy(chanPoint: string, policy: Types.ChannelPolicy) { - // console.log("Updating channel policy") + this.log(DEBUG, "Updating channel policy") const split = chanPoint.split(':') const res = await this.lightning.updateChannelPolicy({ @@ -565,19 +576,19 @@ export default class { } async GetChannelBalance() { - // console.log("Getting channel balance") + this.log(DEBUG, "Getting channel balance") const res = await this.lightning.channelBalance({}, DeadLineMetadata()) return res.response } async GetWalletBalance() { - // console.log("Getting wallet balance") + this.log(DEBUG, "Getting wallet balance") const res = await this.lightning.walletBalance({ account: "", minConfs: 1 }, DeadLineMetadata()) return res.response } async GetTotalBalace() { - // console.log("Getting total balance") + this.log(DEBUG, "Getting total balance") const walletBalance = await this.GetWalletBalance() const confirmedWalletBalance = Number(walletBalance.confirmedBalance) this.utils.stateBundler.AddBalancePoint('walletBalance', confirmedWalletBalance) @@ -592,10 +603,10 @@ export default class { } async GetBalance(): Promise { // 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({ @@ -611,33 +622,47 @@ export default class { } async GetForwardingHistory(indexOffset: number, startTime = 0, endTime = 0): Promise { + this.log(DEBUG, "Getting forwarding history") if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { return { forwardingEvents: [], lastOffsetIndex: indexOffset } } - // console.log("Getting forwarding history") const { response } = await this.lightning.forwardingHistory({ indexOffset, numMaxEvents: 0, startTime: BigInt(startTime), endTime: BigInt(endTime), peerAliasLookup: false }, DeadLineMetadata()) return response } - async GetAllPaidInvoices(max: number) { + async GetAllInvoices(max: number) { + this.log(DEBUG, "Getting all paid invoices") if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { return { invoices: [] } } - // console.log("Getting all paid invoices") const res = await this.lightning.listInvoices({ indexOffset: 0n, numMaxInvoices: BigInt(max), pendingOnly: false, reversed: true, creationDateEnd: 0n, creationDateStart: 0n }, DeadLineMetadata()) return res.response } async GetAllPayments(max: number) { + this.log(DEBUG, "Getting all payments") if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { return { payments: [] } } - // console.log("Getting all payments") const res = await this.lightning.listPayments({ countTotalPayments: false, includeIncomplete: false, indexOffset: 0n, maxPayments: BigInt(max), reversed: true, creationDateEnd: 0n, creationDateStart: 0n }) return res.response } + async BumpFee(txId: string, outputIndex: number, satPerVbyte: number) { + this.log(DEBUG, "Bumping fee") + const res = await this.walletKit.bumpFee({ + budget: 0n, immediate: false, targetConf: 0, satPerVbyte: BigInt(satPerVbyte), outpoint: { + txidStr: txId, + outputIndex: outputIndex, + txidBytes: Buffer.alloc(0) + }, + force: false, + satPerByte: 0 + }, DeadLineMetadata()) + return res.response + } + async GetPayment(paymentIndex: number) { - // console.log("Getting payment") + this.log(DEBUG, "Getting payment") if (paymentIndex === 0) { throw new Error("payment index starts from 1") } @@ -649,10 +674,10 @@ export default class { } async GetLatestPaymentIndex(from = 0) { + this.log(DEBUG, "Getting latest payment index") if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { return from } - // console.log("Getting latest payment index") let indexOffset = BigInt(from) while (true) { const res = await this.lightning.listPayments({ countTotalPayments: false, includeIncomplete: false, indexOffset, maxPayments: 0n, reversed: false, creationDateEnd: 0n, creationDateStart: 0n }, DeadLineMetadata()) @@ -664,7 +689,7 @@ export default class { } async ConnectPeer(addr: { pubkey: string, host: string }) { - // console.log("Connecting to peer") + this.log(DEBUG, "Connecting to peer") const res = await this.lightning.connectPeer({ addr, perm: true, @@ -674,7 +699,7 @@ export default class { } async GetPaymentFromHash(paymentHash: string): Promise { - // console.log("Getting payment from hash") + this.log(DEBUG, "Getting payment from hash") const abortController = new AbortController() const stream = this.router.trackPaymentV2({ paymentHash: Buffer.from(paymentHash, 'hex'), @@ -696,13 +721,12 @@ export default class { } async GetTx(txid: string) { - // console.log("Getting transaction") const res = await this.walletKit.getTransaction({ txid }, DeadLineMetadata()) return res.response } async AddPeer(pub: string, host: string, port: number) { - // console.log("Adding peer") + this.log(DEBUG, "Adding peer") const res = await this.lightning.connectPeer({ addr: { pubkey: pub, @@ -715,19 +739,19 @@ export default class { } async ListPeers() { - // console.log("Listing peers") + this.log(DEBUG, "Listing peers") const res = await this.lightning.listPeers({ latestError: true }, DeadLineMetadata()) return res.response } async OpenChannel(destination: string, closeAddress: string, fundingAmount: number, pushSats: number, satsPerVByte: number): Promise { - // console.log("Opening channel") + this.log(DEBUG, "Opening channel") const abortController = new AbortController() const req = OpenChannelReq(destination, closeAddress, fundingAmount, pushSats, satsPerVByte) const stream = this.lightning.openChannel(req, { abort: abortController.signal }) return new Promise((res, rej) => { stream.responses.onMessage(message => { - console.log("message", message) + this.log(DEBUG, "open channel message", message) switch (message.update.oneofKind) { case 'chanPending': res(message) @@ -735,14 +759,14 @@ export default class { } }) stream.responses.onError(error => { - console.log("error", error) + this.log(ERROR, "open channel error", error) rej(error) }) }) } async CloseChannel(fundingTx: string, outputIndex: number, force: boolean, satPerVByte: number): Promise { - // console.log("Closing channel") + this.log(DEBUG, "Closing channel") const stream = this.lightning.closeChannel({ deliveryAddress: "", force: force, @@ -761,7 +785,7 @@ export default class { }, DeadLineMetadata()) return new Promise((res, rej) => { stream.responses.onMessage(message => { - console.log("message", message) + this.log(DEBUG, "close channel message", message) switch (message.update.oneofKind) { case 'closePending': res(message.update.closePending) @@ -769,7 +793,7 @@ export default class { } }) stream.responses.onError(error => { - console.log("error", error) + this.log(ERROR, "close channel error", error) rej(error) }) }) diff --git a/src/services/lnd/swaps.ts b/src/services/lnd/swaps.ts deleted file mode 100644 index 727f0e88..00000000 --- a/src/services/lnd/swaps.ts +++ /dev/null @@ -1,657 +0,0 @@ -import zkpInit from '@vulpemventures/secp256k1-zkp'; -import axios from 'axios'; -import { crypto, initEccLib, Transaction, address, Network } from 'bitcoinjs-lib'; -// import bolt11 from 'bolt11'; -import { - Musig, SwapTreeSerializer, TaprootUtils, detectSwap, - constructClaimTransaction, targetFee, OutputType, - Networks, -} from 'boltz-core'; -import { randomBytes, createHash } from 'crypto'; -import { ECPairFactory, ECPairInterface } from 'ecpair'; -import * as ecc from 'tiny-secp256k1'; -import ws from 'ws'; -import { getLogger, PubLogger, ERROR } from '../helpers/logger.js'; -import SettingsManager from '../main/settingsManager.js'; -import * as Types from '../../../proto/autogenerated/ts/types.js'; -import { BTCNetwork } from '../main/settings.js'; -import Storage from '../storage/index.js'; -import LND from './lnd.js'; -import { UserInvoicePayment } from '../storage/entity/UserInvoicePayment.js'; -type InvoiceSwapResponse = { id: string, claimPublicKey: string, swapTree: string } -type InvoiceSwapInfo = { paymentHash: string, keys: ECPairInterface } -type InvoiceSwapData = { createdResponse: InvoiceSwapResponse, info: InvoiceSwapInfo } - -type TransactionSwapFees = { - percentage: number, - minerFees: { - claim: number, - lockup: number, - } -} - -type TransactionSwapFeesRes = { - BTC?: { - BTC?: { - fees: TransactionSwapFees - } - } -} - - -type TransactionSwapResponse = { - id: string, refundPublicKey: string, swapTree: string, - timeoutBlockHeight: number, lockupAddress: string, invoice: string, - onchainAmount?: number -} -type TransactionSwapInfo = { destinationAddress: string, preimage: Buffer, keys: ECPairInterface, chainFee: number } -export type TransactionSwapData = { createdResponse: TransactionSwapResponse, info: TransactionSwapInfo } -export class Swaps { - settings: SettingsManager - revSwappers: Record - // submarineSwaps: SubmarineSwaps - storage: Storage - lnd: LND - log = getLogger({ component: 'swaps' }) - constructor(settings: SettingsManager, storage: Storage) { - this.settings = settings - this.revSwappers = {} - const network = settings.getSettings().lndSettings.network - const { boltzHttpUrl, boltzWebSocketUrl, boltsHttpUrlAlt, boltsWebSocketUrlAlt } = settings.getSettings().swapsSettings - if (boltzHttpUrl && boltzWebSocketUrl) { - this.revSwappers[boltzHttpUrl] = new ReverseSwaps({ httpUrl: boltzHttpUrl, wsUrl: boltzWebSocketUrl, network }) - } - if (boltsHttpUrlAlt && boltsWebSocketUrlAlt) { - this.revSwappers[boltsHttpUrlAlt] = new ReverseSwaps({ httpUrl: boltsHttpUrlAlt, wsUrl: boltsWebSocketUrlAlt, network }) - } - this.storage = storage - } - - SetLnd = (lnd: LND) => { - this.lnd = lnd - } - - Stop = () => { } - - GetKeys = (privateKey: string) => { - const keys = ECPairFactory(ecc).fromPrivateKey(Buffer.from(privateKey, 'hex')) - return keys - } - - ListSwaps = async (appUserId: string, payments: UserInvoicePayment[], newOp: (p: UserInvoicePayment) => Types.UserOperation | undefined, getServiceFee: (amt: number) => number): Promise => { - const completedSwaps = await this.storage.paymentStorage.ListCompletedSwaps(appUserId, payments) - const pendingSwaps = await this.storage.paymentStorage.ListPendingTransactionSwaps(appUserId) - return { - swaps: completedSwaps.map(s => { - const p = s.payment - const op = p ? newOp(p) : undefined - return { - operation_payment: op, - swap_operation_id: s.swap.swap_operation_id, - address_paid: s.swap.address_paid, - failure_reason: s.swap.failure_reason, - } - }), - quotes: pendingSwaps.map(s => { - const serviceFee = getServiceFee(s.invoice_amount) - return { - swap_operation_id: s.swap_operation_id, - invoice_amount_sats: s.invoice_amount, - transaction_amount_sats: s.transaction_amount, - chain_fee_sats: s.chain_fee_sats, - service_fee_sats: serviceFee, - swap_fee_sats: s.swap_fee_sats, - service_url: s.service_url, - } - }) - } - } - GetTxSwapQuotes = async (appUserId: string, amt: number, getServiceFee: (decodedAmt: number) => number): Promise => { - if (!this.settings.getSettings().swapsSettings.enableSwaps) { - throw new Error("Swaps are not enabled") - } - const swappers = Object.values(this.revSwappers) - if (swappers.length === 0) { - throw new Error("No swap services available") - } - const res = await Promise.allSettled(swappers.map(sw => this.getTxSwapQuote(sw, appUserId, amt, getServiceFee))) - const failures: string[] = [] - const success: Types.TransactionSwapQuote[] = [] - for (const r of res) { - if (r.status === 'fulfilled') { - success.push(r.value) - } else { - failures.push(r.reason.message ? r.reason.message : r.reason.toString()) - } - } - if (success.length === 0) { - throw new Error(failures.join("\n")) - } - return success - } - - private async getTxSwapQuote(swapper: ReverseSwaps, appUserId: string, amt: number, getServiceFee: (decodedAmt: number) => number): Promise { - this.log("getting transaction swap quote") - const feesRes = await swapper.GetFees() - if (!feesRes.ok) { - throw new Error(feesRes.error) - } - const { claim, lockup } = feesRes.fees.minerFees - const minerFee = claim + lockup - const chainTotal = amt + minerFee - const res = await swapper.SwapTransaction(chainTotal) - if (!res.ok) { - throw new Error(res.error) - } - const decoded = await this.lnd.DecodeInvoice(res.createdResponse.invoice) - const swapFee = decoded.numSatoshis - chainTotal - const serviceFee = getServiceFee(decoded.numSatoshis) - const newSwap = await this.storage.paymentStorage.AddTransactionSwap({ - app_user_id: appUserId, - swap_quote_id: res.createdResponse.id, - swap_tree: JSON.stringify(res.createdResponse.swapTree), - lockup_address: res.createdResponse.lockupAddress, - refund_public_key: res.createdResponse.refundPublicKey, - timeout_block_height: res.createdResponse.timeoutBlockHeight, - invoice: res.createdResponse.invoice, - invoice_amount: decoded.numSatoshis, - transaction_amount: chainTotal, - swap_fee_sats: swapFee, - chain_fee_sats: minerFee, - preimage: res.preimage, - ephemeral_private_key: res.privKey, - ephemeral_public_key: res.pubkey, - service_url: swapper.getHttpUrl(), - }) - return { - swap_operation_id: newSwap.swap_operation_id, - swap_fee_sats: swapFee, - invoice_amount_sats: decoded.numSatoshis, - transaction_amount_sats: amt, - chain_fee_sats: minerFee, - service_fee_sats: serviceFee, - service_url: swapper.getHttpUrl(), - } - } - - async PayAddrWithSwap(appUserId: string, swapOpId: string, address: string, payInvoice: (invoice: string, amt: number) => Promise) { - if (!this.settings.getSettings().swapsSettings.enableSwaps) { - throw new Error("Swaps are not enabled") - } - this.log("paying address with swap", { appUserId, swapOpId, address }) - if (!swapOpId) { - throw new Error("request a swap quote before paying an external address") - } - const txSwap = await this.storage.paymentStorage.GetTransactionSwap(swapOpId, appUserId) - if (!txSwap) { - throw new Error("swap quote not found") - } - const info = await this.lnd.GetInfo() - if (info.blockHeight >= txSwap.timeout_block_height) { - throw new Error("swap timeout") - } - const swapper = this.revSwappers[txSwap.service_url] - if (!swapper) { - throw new Error("swapper service not found") - } - const keys = this.GetKeys(txSwap.ephemeral_private_key) - const data: TransactionSwapData = { - createdResponse: { - id: txSwap.swap_quote_id, - invoice: txSwap.invoice, - lockupAddress: txSwap.lockup_address, - refundPublicKey: txSwap.refund_public_key, - swapTree: txSwap.swap_tree, - timeoutBlockHeight: txSwap.timeout_block_height, - onchainAmount: txSwap.transaction_amount, - }, - info: { - destinationAddress: address, - keys, - chainFee: txSwap.chain_fee_sats, - preimage: Buffer.from(txSwap.preimage, 'hex'), - } - } - // the swap and the invoice payment are linked, swap will not start until the invoice payment is started, and will not complete once the invoice payment is completed - let swapResult = { ok: false, error: "swap never completed" } as { ok: true, txId: string } | { ok: false, error: string } - swapper.SubscribeToTransactionSwap(data, result => { - swapResult = result - }) - try { - await payInvoice(txSwap.invoice, txSwap.invoice_amount) - if (!swapResult.ok) { - this.log("invoice payment successful, but swap failed") - await this.storage.paymentStorage.FailTransactionSwap(swapOpId, address, swapResult.error) - throw new Error(swapResult.error) - } - this.log("swap completed successfully") - await this.storage.paymentStorage.FinalizeTransactionSwap(swapOpId, address, swapResult.txId) - } catch (err: any) { - if (swapResult.ok) { - this.log("failed to pay swap invoice, but swap completed successfully", swapResult.txId) - await this.storage.paymentStorage.FailTransactionSwap(swapOpId, address, err.message) - } else { - this.log("failed to pay swap invoice and swap failed", swapResult.error) - await this.storage.paymentStorage.FailTransactionSwap(swapOpId, address, swapResult.error) - } - throw err - } - const networkFeesTotal = txSwap.chain_fee_sats + txSwap.swap_fee_sats - return { - txId: swapResult.txId, - network_fee: networkFeesTotal - } - } -} - - - -export class ReverseSwaps { - // settings: SettingsManager - private httpUrl: string - private wsUrl: string - log: PubLogger - private network: BTCNetwork - constructor({ httpUrl, wsUrl, network }: { httpUrl: string, wsUrl: string, network: BTCNetwork }) { - this.httpUrl = httpUrl - this.wsUrl = wsUrl - this.network = network - this.log = getLogger({ component: 'ReverseSwaps' }) - initEccLib(ecc) - } - - getHttpUrl = () => { - return this.httpUrl - } - getWsUrl = () => { - return this.wsUrl - } - - calculateFees = (fees: TransactionSwapFees, receiveAmount: number) => { - const pct = fees.percentage / 100 - const minerFee = fees.minerFees.claim + fees.minerFees.lockup - - const preFee = receiveAmount + minerFee - const fee = Math.ceil(preFee * pct) - const total = preFee + fee - return { total, fee, minerFee } - } - - GetFees = async (): Promise<{ ok: true, fees: TransactionSwapFees, } | { ok: false, error: string }> => { - const url = `${this.httpUrl}/v2/swap/reverse` - const feesRes = await loggedGet(this.log, url) - if (!feesRes.ok) { - return { ok: false, error: feesRes.error } - } - if (!feesRes.data.BTC?.BTC?.fees) { - return { ok: false, error: 'No fees found for BTC to BTC swap' } - } - - return { ok: true, fees: feesRes.data.BTC.BTC.fees } - } - - SwapTransaction = async (txAmount: number): Promise<{ ok: true, createdResponse: TransactionSwapResponse, preimage: string, pubkey: string, privKey: string } | { ok: false, error: string }> => { - const preimage = randomBytes(32); - const keys = ECPairFactory(ecc).makeRandom() - if (!keys.privateKey) { - return { ok: false, error: 'Failed to generate keys' } - } - const url = `${this.httpUrl}/v2/swap/reverse` - const req: any = { - onchainAmount: txAmount, - to: 'BTC', - from: 'BTC', - claimPublicKey: Buffer.from(keys.publicKey).toString('hex'), - preimageHash: createHash('sha256').update(preimage).digest('hex'), - } - const createdResponseRes = await loggedPost(this.log, url, req) - if (!createdResponseRes.ok) { - return createdResponseRes - } - const createdResponse = createdResponseRes.data - this.log('Created transaction swap'); - this.log(createdResponse); - return { - ok: true, createdResponse, - preimage: Buffer.from(preimage).toString('hex'), - pubkey: Buffer.from(keys.publicKey).toString('hex'), - privKey: Buffer.from(keys.privateKey).toString('hex') - } - } - - SubscribeToTransactionSwap = async (data: TransactionSwapData, swapDone: (result: { ok: true, txId: string } | { ok: false, error: string }) => void) => { - const webSocket = new ws(`${this.wsUrl}/v2/ws`) - const subReq = { op: 'subscribe', channel: 'swap.update', args: [data.createdResponse.id] } - webSocket.on('open', () => { - webSocket.send(JSON.stringify(subReq)) - }) - let txId = "", isDone = false - const done = () => { - isDone = true - webSocket.close() - swapDone({ ok: true, txId }) - } - webSocket.on('error', (err) => { - this.log(ERROR, 'Error in WebSocket', err.message) - }) - webSocket.on('close', () => { - if (!isDone) { - this.log(ERROR, 'WebSocket closed before swap was done'); - swapDone({ ok: false, error: 'WebSocket closed before swap was done' }) - } - }) - webSocket.on('message', async (rawMsg) => { - try { - const result = await this.handleSwapTransactionMessage(rawMsg, data, done) - if (result) { - txId = result - } - } catch (err: any) { - this.log(ERROR, 'Error handling transaction WebSocket message', err.message) - isDone = true - webSocket.close() - swapDone({ ok: false, error: err.message }) - return - } - }) - } - - handleSwapTransactionMessage = async (rawMsg: ws.RawData, data: TransactionSwapData, done: () => void) => { - const msg = JSON.parse(rawMsg.toString('utf-8')); - if (msg.event !== 'update') { - return; - } - - this.log('Got WebSocket update'); - this.log(msg); - switch (msg.args[0].status) { - // "swap.created" means Boltz is waiting for the invoice to be paid - case 'swap.created': - this.log('Waiting invoice to be paid'); - return; - - // "transaction.mempool" means that Boltz sent an onchain transaction - case 'transaction.mempool': - const txIdRes = await this.handleTransactionMempool(data, msg.args[0].transaction.hex) - if (!txIdRes.ok) { - throw new Error(txIdRes.error) - } - return txIdRes.txId - case 'invoice.settled': - this.log('Transaction swap successful'); - done() - return; - } - } - - handleTransactionMempool = async (data: TransactionSwapData, txHex: string): Promise<{ ok: true, txId: string } | { ok: false, error: string }> => { - this.log('Creating claim transaction'); - const { createdResponse, info } = data - const { destinationAddress, keys, preimage, chainFee } = info - const boltzPublicKey = Buffer.from( - createdResponse.refundPublicKey, - 'hex', - ); - - // Create a musig signing session and tweak it with the Taptree of the swap scripts - const musig = new Musig(await zkpInit(), keys, randomBytes(32), [ - boltzPublicKey, - Buffer.from(keys.publicKey), - ]); - const tweakedKey = TaprootUtils.tweakMusig( - musig, - // swap tree can either be a string or an object - SwapTreeSerializer.deserializeSwapTree(createdResponse.swapTree).tree, - ); - - // Parse the lockup transaction and find the output relevant for the swap - const lockupTx = Transaction.fromHex(txHex); - const swapOutput = detectSwap(tweakedKey, lockupTx); - if (swapOutput === undefined) { - this.log(ERROR, 'No swap output found in lockup transaction'); - return { ok: false, error: 'No swap output found in lockup transaction' } - } - const network = getNetwork(this.network) - // Create a claim transaction to be signed cooperatively via a key path spend - const claimTx = constructClaimTransaction( - [ - { - ...swapOutput, - keys, - preimage, - cooperative: true, - type: OutputType.Taproot, - txHash: lockupTx.getHash(), - }, - ], - address.toOutputScript(destinationAddress, network), - chainFee, - ) - // Get the partial signature from Boltz - const claimUrl = `${this.httpUrl}/v2/swap/reverse/${createdResponse.id}/claim` - const claimReq = { - index: 0, - transaction: claimTx.toHex(), - preimage: preimage.toString('hex'), - pubNonce: Buffer.from(musig.getPublicNonce()).toString('hex'), - } - const boltzSigRes = await loggedPost<{ pubNonce: string, partialSignature: string }>(this.log, claimUrl, claimReq) - if (!boltzSigRes.ok) { - return boltzSigRes - } - const boltzSig = boltzSigRes.data - - // Aggregate the nonces - musig.aggregateNonces([ - [boltzPublicKey, Buffer.from(boltzSig.pubNonce, 'hex')], - ]); - - // Initialize the session to sign the claim transaction - musig.initializeSession( - claimTx.hashForWitnessV1( - 0, - [swapOutput.script], - [swapOutput.value], - Transaction.SIGHASH_DEFAULT, - ), - ); - - // Add the partial signature from Boltz - musig.addPartial( - boltzPublicKey, - Buffer.from(boltzSig.partialSignature, 'hex'), - ); - - // Create our partial signature - musig.signPartial(); - - // Witness of the input to the aggregated signature - claimTx.ins[0].witness = [musig.aggregatePartials()]; - - // Broadcast the finalized transaction - const broadcastUrl = `${this.httpUrl}/v2/chain/BTC/transaction` - const broadcastReq = { - hex: claimTx.toHex(), - } - - const broadcastResponse = await loggedPost(this.log, broadcastUrl, broadcastReq) - if (!broadcastResponse.ok) { - return broadcastResponse - } - this.log('Transaction broadcasted', broadcastResponse.data) - const txId = claimTx.getId() - return { ok: true, txId } - } -} - -const loggedPost = async (log: PubLogger, url: string, req: any): Promise<{ ok: true, data: T } | { ok: false, error: string }> => { - try { - const { data } = await axios.post(url, req) - return { ok: true, data: data as T } - } catch (err: any) { - if (err.response?.data) { - log(ERROR, 'Error sending request', err.response.data) - return { ok: false, error: JSON.stringify(err.response.data) } - } - log(ERROR, 'Error sending request', err.message) - return { ok: false, error: err.message } - } -} - -const loggedGet = async (log: PubLogger, url: string): Promise<{ ok: true, data: T } | { ok: false, error: string }> => { - try { - const { data } = await axios.get(url) - return { ok: true, data: data as T } - } catch (err: any) { - if (err.response?.data) { - log(ERROR, 'Error getting request', err.response.data) - return { ok: false, error: err.response.data } - } - log(ERROR, 'Error getting request', err.message) - return { ok: false, error: err.message } - } -} - -const getNetwork = (network: BTCNetwork): Network => { - switch (network) { - case 'mainnet': - return Networks.bitcoinMainnet - case 'testnet': - return Networks.bitcoinTestnet - case 'regtest': - return Networks.bitcoinRegtest - default: - throw new Error(`Invalid network: ${network}`) - } -} - -// Submarine swaps currently not supported, keeping the code for future reference -/* -export class SubmarineSwaps { - settings: SettingsManager - log: PubLogger - constructor(settings: SettingsManager) { - this.settings = settings - this.log = getLogger({ component: 'SubmarineSwaps' }) - } - - SwapInvoice = async (invoice: string, paymentHash: string) => { - if (!this.settings.getSettings().swapsSettings.enableSwaps) { - this.log(ERROR, 'Swaps are not enabled'); - return; - } - const keys = ECPairFactory(ecc).makeRandom() - const refundPublicKey = Buffer.from(keys.publicKey).toString('hex') - const req = { invoice, to: 'BTC', from: 'BTC', refundPublicKey } - const url = `${this.settings.getSettings().swapsSettings.boltzHttpUrl}/v2/swap/submarine` - this.log('Sending invoice swap request to', url); - const createdResponseRes = await loggedPost(this.log, url, req) - if (!createdResponseRes.ok) { - return createdResponseRes - } - const createdResponse = createdResponseRes.data - this.log('Created invoice swap'); - this.log(createdResponse); - - const webSocket = new ws(`${this.settings.getSettings().swapsSettings.boltzWebSocketUrl}/v2/ws`) - const subReq = { op: 'subscribe', channel: 'swap.update', args: [createdResponse.id] } - webSocket.on('open', () => { - webSocket.send(JSON.stringify(subReq)) - }) - - webSocket.on('message', async (rawMsg) => { - try { - await this.handleSwapInvoiceMessage(rawMsg, { createdResponse, info: { paymentHash, keys } }, () => webSocket.close()) - } catch (err: any) { - this.log(ERROR, 'Error handling invoice WebSocket message', err.message) - webSocket.close() - return - } - }); - } - - handleSwapInvoiceMessage = async (rawMsg: ws.RawData, data: InvoiceSwapData, closeWebSocket: () => void) => { - const msg = JSON.parse(rawMsg.toString('utf-8')); - if (msg.event !== 'update') { - return; - } - - this.log('Got invoice WebSocket update'); - this.log(msg); - switch (msg.args[0].status) { - // "invoice.set" means Boltz is waiting for an onchain transaction to be sent - case 'invoice.set': - this.log('Waiting for onchain transaction'); - return; - // Create a partial signature to allow Boltz to do a key path spend to claim the mainchain coins - case 'transaction.claim.pending': - await this.handleInvoiceClaimPending(data) - return; - - case 'transaction.claimed': - this.log('Invoice swap successful'); - closeWebSocket() - return; - } - - } - - handleInvoiceClaimPending = async (data: InvoiceSwapData) => { - this.log('Creating cooperative claim transaction'); - const { createdResponse, info } = data - const { paymentHash, keys } = info - const { boltzHttpUrl } = this.settings.getSettings().swapsSettings - // Get the information request to create a partial signature - const url = `${boltzHttpUrl}/v2/swap/submarine/${createdResponse.id}/claim` - const claimTxDetailsRes = await loggedGet<{ preimage: string, transactionHash: string, pubNonce: string }>(this.log, url) - if (!claimTxDetailsRes.ok) { - return claimTxDetailsRes - } - const claimTxDetails = claimTxDetailsRes.data - // Verify that Boltz actually paid the invoice by comparing the preimage hash - // of the invoice to the SHA256 hash of the preimage from the response - const claimTxPreimageHash = createHash('sha256').update(Buffer.from(claimTxDetails.preimage, 'hex')).digest() - const invoicePreimageHash = Buffer.from(paymentHash, 'hex') - - if (!claimTxPreimageHash.equals(invoicePreimageHash)) { - this.log(ERROR, 'Boltz provided invalid preimage'); - return; - } - - const boltzPublicKey = Buffer.from(createdResponse.claimPublicKey, 'hex') - - // Create a musig signing instance - const musig = new Musig(await zkpInit(), keys, randomBytes(32), [ - boltzPublicKey, - Buffer.from(keys.publicKey), - ]); - // Tweak that musig with the Taptree of the swap scripts - TaprootUtils.tweakMusig( - musig, - SwapTreeSerializer.deserializeSwapTree(createdResponse.swapTree).tree, - ); - - // Aggregate the nonces - musig.aggregateNonces([ - [boltzPublicKey, Buffer.from(claimTxDetails.pubNonce, 'hex')], - ]); - // Initialize the session to sign the transaction hash from the response - musig.initializeSession( - Buffer.from(claimTxDetails.transactionHash, 'hex'), - ); - - // Give our public nonce and the partial signature to Boltz - const claimUrl = `${boltzHttpUrl}/v2/swap/submarine/${createdResponse.id}/claim` - const claimReq = { - pubNonce: Buffer.from(musig.getPublicNonce()).toString('hex'), - partialSignature: Buffer.from(musig.signPartial()).toString('hex'), - } - const claimResponseRes = await loggedPost<{ pubNonce: string, partialSignature: string }>(this.log, claimUrl, claimReq) - if (!claimResponseRes.ok) { - return claimResponseRes - } - const claimResponse = claimResponseRes.data - this.log('Claim response', claimResponse) - } -} -*/ \ No newline at end of file diff --git a/src/services/lnd/swaps/reverseSwaps.ts b/src/services/lnd/swaps/reverseSwaps.ts new file mode 100644 index 00000000..83dddc37 --- /dev/null +++ b/src/services/lnd/swaps/reverseSwaps.ts @@ -0,0 +1,299 @@ +import secp256k1ZkpModule from '@vulpemventures/secp256k1-zkp'; +const zkpInit = (secp256k1ZkpModule as any).default || secp256k1ZkpModule; +import { initEccLib, Transaction, address } from 'bitcoinjs-lib'; +// import bolt11 from 'bolt11'; +import { + Musig, SwapTreeSerializer, TaprootUtils, detectSwap, + constructClaimTransaction, OutputType, constructRefundTransaction +} from 'boltz-core'; +import { randomBytes, createHash } from 'crypto'; +import { ECPairFactory, ECPairInterface } from 'ecpair'; +import * as ecc from 'tiny-secp256k1'; +import ws from 'ws'; +import { getLogger, PubLogger, ERROR } from '../../helpers/logger.js'; +import { BTCNetwork } from '../../main/settings.js'; +import { loggedGet, loggedPost, getNetwork } from './swapHelpers.js'; + + +type TransactionSwapFees = { + percentage: number, + minerFees: { + claim: number, + lockup: number, + } +} + +type TransactionSwapFeesRes = { + BTC?: { + BTC?: { + fees: TransactionSwapFees + } + } +} + + +type TransactionSwapResponse = { + id: string, refundPublicKey: string, swapTree: string, + timeoutBlockHeight: number, lockupAddress: string, invoice: string, + onchainAmount?: number +} +type TransactionSwapInfo = { destinationAddress: string, preimage: Buffer, keys: ECPairInterface, chainFee: number } +export type TransactionSwapData = { createdResponse: TransactionSwapResponse, info: TransactionSwapInfo } + + + +export class ReverseSwaps { + // settings: SettingsManager + private httpUrl: string + private wsUrl: string + log: PubLogger + private network: BTCNetwork + constructor({ httpUrl, wsUrl, network }: { httpUrl: string, wsUrl: string, network: BTCNetwork }) { + this.httpUrl = httpUrl + this.wsUrl = wsUrl + this.network = network + this.log = getLogger({ component: 'ReverseSwaps' }) + initEccLib(ecc) + } + + getHttpUrl = () => { + return this.httpUrl + } + getWsUrl = () => { + return this.wsUrl + } + + calculateFees = (fees: TransactionSwapFees, receiveAmount: number) => { + const pct = fees.percentage / 100 + const minerFee = fees.minerFees.claim + fees.minerFees.lockup + + const preFee = receiveAmount + minerFee + const fee = Math.ceil(preFee * pct) + const total = preFee + fee + return { total, fee, minerFee } + } + + GetFees = async (): Promise<{ ok: true, fees: TransactionSwapFees, } | { ok: false, error: string }> => { + const url = `${this.httpUrl}/v2/swap/reverse` + const feesRes = await loggedGet(this.log, url) + if (!feesRes.ok) { + return { ok: false, error: feesRes.error } + } + if (!feesRes.data.BTC?.BTC?.fees) { + return { ok: false, error: 'No fees found for BTC to BTC swap' } + } + + return { ok: true, fees: feesRes.data.BTC.BTC.fees } + } + + SwapTransaction = async (txAmount: number): Promise<{ ok: true, createdResponse: TransactionSwapResponse, preimage: string, pubkey: string, privKey: string } | { ok: false, error: string }> => { + const preimage = randomBytes(32); + const keys = ECPairFactory(ecc).makeRandom() + if (!keys.privateKey) { + return { ok: false, error: 'Failed to generate keys' } + } + const url = `${this.httpUrl}/v2/swap/reverse` + const req: any = { + onchainAmount: txAmount, + to: 'BTC', + from: 'BTC', + claimPublicKey: Buffer.from(keys.publicKey).toString('hex'), + preimageHash: createHash('sha256').update(preimage).digest('hex'), + } + const createdResponseRes = await loggedPost(this.log, url, req) + if (!createdResponseRes.ok) { + return createdResponseRes + } + const createdResponse = createdResponseRes.data + this.log('Created transaction swap'); + this.log(createdResponse); + return { + ok: true, createdResponse, + preimage: Buffer.from(preimage).toString('hex'), + pubkey: Buffer.from(keys.publicKey).toString('hex'), + privKey: Buffer.from(keys.privateKey).toString('hex') + } + } + + SubscribeToTransactionSwap = async (data: TransactionSwapData, swapDone: (result: { ok: true, txId: string } | { ok: false, error: string }) => void) => { + const webSocket = new ws(`${this.wsUrl}/v2/ws`) + const subReq = { op: 'subscribe', channel: 'swap.update', args: [data.createdResponse.id] } + webSocket.on('open', () => { + webSocket.send(JSON.stringify(subReq)) + }) + 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(this.log, broadcastUrl, broadcastReq) + if (!broadcastResponse.ok) { + return broadcastResponse + } + this.log('Transaction broadcasted', broadcastResponse.data) + const txId = claimTx.getId() + return { ok: true, txId } + } +} \ No newline at end of file diff --git a/src/services/lnd/swaps/submarineSwaps.ts b/src/services/lnd/swaps/submarineSwaps.ts new file mode 100644 index 00000000..6e4beba8 --- /dev/null +++ b/src/services/lnd/swaps/submarineSwaps.ts @@ -0,0 +1,539 @@ +import secp256k1ZkpModule from '@vulpemventures/secp256k1-zkp'; +const zkpInit = (secp256k1ZkpModule as any).default || secp256k1ZkpModule; +// import bolt11 from 'bolt11'; +import { + Musig, SwapTreeSerializer, TaprootUtils, constructRefundTransaction, + detectSwap, OutputType, targetFee +} from 'boltz-core'; +import { randomBytes, createHash } from 'crypto'; +import { ECPairFactory, ECPairInterface } from 'ecpair'; +import * as ecc from 'tiny-secp256k1'; +import { Transaction, address } from 'bitcoinjs-lib'; +import ws from 'ws'; +import { getLogger, PubLogger, ERROR } from '../../helpers/logger.js'; +import { loggedGet, loggedPost, getNetwork } from './swapHelpers.js'; +import { BTCNetwork } from '../../main/settings.js'; + +/* type InvoiceSwapFees = { + hash: string, + rate: number, + limits: { + maximal: number, + minimal: number, + maximalZeroConf: number + }, + fees: { + percentage: number, + minerFees: number, + } +} */ + +type InvoiceSwapFees = { + percentage: number, + minerFees: number, +} + +type InvoiceSwapFeesRes = { + BTC?: { + BTC?: { + fees: InvoiceSwapFees + } + } +} +type InvoiceSwapResponse = { + id: string, claimPublicKey: string, swapTree: string, timeoutBlockHeight: number, + expectedAmount: number, address: string +} +type InvoiceSwapInfo = { paymentHash: string, keys: ECPairInterface } +export type InvoiceSwapData = { createdResponse: InvoiceSwapResponse, info: InvoiceSwapInfo } + +export class SubmarineSwaps { + private httpUrl: string + private wsUrl: string + private network: BTCNetwork + log: PubLogger + constructor({ httpUrl, wsUrl, network }: { httpUrl: string, wsUrl: string, network: BTCNetwork }) { + this.httpUrl = httpUrl + this.wsUrl = wsUrl + this.network = network + this.log = getLogger({ component: 'SubmarineSwaps' }) + } + + getHttpUrl = () => { + return this.httpUrl + } + getWsUrl = () => { + return this.wsUrl + } + + GetFees = async (): Promise<{ ok: true, fees: InvoiceSwapFees, } | { ok: false, error: string }> => { + const url = `${this.httpUrl}/v2/swap/submarine` + const feesRes = await loggedGet(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(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) + } +} diff --git a/src/services/lnd/swaps/swapHelpers.ts b/src/services/lnd/swaps/swapHelpers.ts new file mode 100644 index 00000000..8c34f4fd --- /dev/null +++ b/src/services/lnd/swaps/swapHelpers.ts @@ -0,0 +1,50 @@ +import axios from 'axios'; +import { Network } from 'bitcoinjs-lib'; +// import bolt11 from 'bolt11'; +import { + Networks, +} from 'boltz-core'; +import { PubLogger, ERROR } from '../../helpers/logger.js'; +import { BTCNetwork } from '../../main/settings.js'; + + +export const loggedPost = async (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 (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}`) + } +} \ No newline at end of file diff --git a/src/services/lnd/swaps/swaps.ts b/src/services/lnd/swaps/swaps.ts new file mode 100644 index 00000000..0aaf9e32 --- /dev/null +++ b/src/services/lnd/swaps/swaps.ts @@ -0,0 +1,448 @@ +import { ECPairFactory } from 'ecpair'; +import * as ecc from 'tiny-secp256k1'; +import { getLogger } from '../../helpers/logger.js'; +import SettingsManager from '../../main/settingsManager.js'; +import * as Types from '../../../../proto/autogenerated/ts/types.js'; +import Storage from '../../storage/index.js'; +import LND from '../lnd.js'; +import { UserInvoicePayment } from '../../storage/entity/UserInvoicePayment.js'; +import { ReverseSwaps, TransactionSwapData } from './reverseSwaps.js'; +import { SubmarineSwaps, InvoiceSwapData } from './submarineSwaps.js'; +import { InvoiceSwap } from '../../storage/entity/InvoiceSwap.js'; +import { TransactionSwap } from '../../storage/entity/TransactionSwap.js'; + + +export class Swaps { + settings: SettingsManager + revSwappers: Record + subSwappers: Record + storage: Storage + lnd: LND + waitingSwaps: Record = {} + 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 => { + 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 => { + 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 => { + 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 { + 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 => { + 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 => { + if (!this.settings.getSettings().swapsSettings.enableSwaps) { + throw new Error("Swaps are not enabled") + } + const swappers = Object.values(this.revSwappers) + if (swappers.length === 0) { + throw new Error("No swap services available") + } + const res = await Promise.allSettled(swappers.map(sw => this.getTxSwapQuote(sw, appUserId, amt, getServiceFee))) + const failures: string[] = [] + const success: Types.TransactionSwapQuote[] = [] + for (const r of res) { + if (r.status === 'fulfilled') { + success.push(r.value) + } else { + failures.push(r.reason.message ? r.reason.message : r.reason.toString()) + } + } + if (success.length === 0) { + throw new Error(failures.join("\n")) + } + return success + } + + private async getTxSwapQuote(swapper: ReverseSwaps, appUserId: string, amt: number, getServiceFee: (decodedAmt: number) => number): Promise { + this.log("getting transaction swap quote") + const feesRes = await swapper.GetFees() + if (!feesRes.ok) { + throw new Error(feesRes.error) + } + const { claim, lockup } = feesRes.fees.minerFees + const minerFee = claim + lockup + const chainTotal = amt + minerFee + const res = await swapper.SwapTransaction(chainTotal) + if (!res.ok) { + throw new Error(res.error) + } + const decoded = await this.lnd.DecodeInvoice(res.createdResponse.invoice) + const swapFee = decoded.numSatoshis - chainTotal + const serviceFee = getServiceFee(decoded.numSatoshis) + const newSwap = await this.storage.paymentStorage.AddTransactionSwap({ + app_user_id: appUserId, + swap_quote_id: res.createdResponse.id, + swap_tree: JSON.stringify(res.createdResponse.swapTree), + lockup_address: res.createdResponse.lockupAddress, + refund_public_key: res.createdResponse.refundPublicKey, + timeout_block_height: res.createdResponse.timeoutBlockHeight, + invoice: res.createdResponse.invoice, + invoice_amount: decoded.numSatoshis, + transaction_amount: chainTotal, + swap_fee_sats: swapFee, + chain_fee_sats: minerFee, + preimage: res.preimage, + ephemeral_private_key: res.privKey, + ephemeral_public_key: res.pubkey, + service_url: swapper.getHttpUrl(), + }) + return { + swap_operation_id: newSwap.swap_operation_id, + swap_fee_sats: swapFee, + invoice_amount_sats: decoded.numSatoshis, + transaction_amount_sats: amt, + chain_fee_sats: minerFee, + service_fee_sats: serviceFee, + service_url: swapper.getHttpUrl(), + 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) { + 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 + } + } +} \ No newline at end of file diff --git a/src/services/main/adminManager.ts b/src/services/main/adminManager.ts index 294635ed..dbde290d 100644 --- a/src/services/main/adminManager.ts +++ b/src/services/main/adminManager.ts @@ -5,9 +5,50 @@ import Storage from "../storage/index.js"; import * as Types from '../../../proto/autogenerated/ts/types.js' import LND from "../lnd/lnd.js"; import SettingsManager from "./settingsManager.js"; -import { Swaps } from "../lnd/swaps.js"; +import { Swaps } from "../lnd/swaps/swaps.js"; +import { defaultInvoiceExpiry } from "../storage/paymentStorage.js"; +import { TrackedProvider } from "../storage/entity/TrackedProvider.js"; +import { NodeInfo } from "../lnd/settings.js"; +import { Invoice, Payment, OutputDetail, Transaction, Payment_PaymentStatus, Invoice_InvoiceState } from "../../../proto/lnd/lightning.js"; +import { LiquidityProvider } from "./liquidityProvider.js"; +/* type TrackedOperation = { + ts: number + amount: number + type: 'user' | 'root' +} + +type AssetOperation = { + ts: number + amount: number + tracked?: TrackedOperation +} +type TrackedLndProvider = { + confirmedBalance: number + unconfirmedBalance: number + channelsBalace: number + payments: AssetOperation[] + invoices: AssetOperation[] + incomingTx: AssetOperation[] + outgoingTx: AssetOperation[] +} +type LndAssetProvider = { + pubkey: string + tracked?: TrackedLndProvider +} +type TrackedLiquidityProvider = { + balance: number + payments: AssetOperation[] + invoices: AssetOperation[] +} +type ProviderAssetProvider = { + pubkey: string + tracked?: TrackedLiquidityProvider +} */ +const ROOT_OP = Types.TrackedOperationType.ROOT +const USER_OP = Types.TrackedOperationType.USER export class AdminManager { settings: SettingsManager + liquidityProvider: LiquidityProvider | null = null storage: Storage log = getLogger({ component: "adminManager" }) adminNpub = "" @@ -40,6 +81,10 @@ export class AdminManager { this.start() } + attachLiquidityProvider(liquidityProvider: LiquidityProvider) { + this.liquidityProvider = liquidityProvider + } + attachNostrReset(f: () => Promise) { this.nostrReset = f } @@ -260,15 +305,64 @@ export class AdminManager { } } - async ListAdminSwaps(): Promise { - return this.swaps.ListSwaps("admin", [], p => undefined, amt => 0) + async ListAdminInvoiceSwaps(): Promise { + return this.swaps.ListInvoiceSwaps("admin") + } + + async GetAdminInvoiceSwapQuotes(req: Types.InvoiceSwapRequest): Promise { + 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 { + const resolvedTxId = await new Promise(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 { + 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 { + return this.swaps.ListTxSwaps("admin", [], p => undefined, amt => 0) } async GetAdminTransactionSwapQuotes(req: Types.TransactionSwapRequest): Promise { const quotes = await this.swaps.GetTxSwapQuotes("admin", req.transaction_amount_sats, () => 0) return { quotes } } - async PayAdminTransactionSwap(req: Types.PayAdminTransactionSwapRequest): Promise { + async PayAdminTransactionSwap(req: Types.PayAdminTransactionSwapRequest): Promise { const routingFloor = this.settings.getSettings().lndSettings.routingFeeFloor const routingLimit = this.settings.getSettings().lndSettings.routingFeeLimitBps / 10000 @@ -282,6 +376,222 @@ export class AdminManager { network_fee: swap.network_fee, } } + + async GetAssetsAndLiabilities(req: Types.AssetsAndLiabilitiesReq): Promise { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + await this.lnd.BumpFee(req.txid, req.output_index, req.sat_per_vbyte) + } + } const getDataPath = (dataDir: string, dataPath: string) => { diff --git a/src/services/main/appUserManager.ts b/src/services/main/appUserManager.ts index 9db8b5e6..b9bf8742 100644 --- a/src/services/main/appUserManager.ts +++ b/src/services/main/appUserManager.ts @@ -66,7 +66,7 @@ export default class { const appUser = await this.storage.applicationStorage.GetAppUserFromUser(app, user.user_id) if (!appUser) { - throw new Error(`app user ${ctx.user_id} not found`) // TODO: fix logs doxing + throw new Error("app user not found") } const nostrSettings = this.settings.getSettings().nostrRelaySettings const { max, serviceFeeFloor, serviceFeeBps } = this.applicationManager.paymentManager.GetMaxPayableInvoice(user.balance_sats) @@ -82,7 +82,8 @@ export default class { ndebit: ndebitEncode({ pubkey: app.nostr_public_key!, pointer: appUser.identifier, relay: nostrSettings.relays[0] }), nmanage: nmanageEncode({ pubkey: app.nostr_public_key!, pointer: appUser.identifier, relay: nostrSettings.relays[0] }), callback_url: appUser.callback_url, - bridge_url: this.settings.getSettings().serviceSettings.bridgeUrl + bridge_url: this.settings.getSettings().serviceSettings.bridgeUrl, + topic_id: appUser.topic_id } } @@ -131,12 +132,12 @@ export default class { } this.log("Found", toDelete.length, "inactive users to delete") - // await this.RemoveUsers(toDelete) + await this.LockUsers(toDelete.map(u => u.userId)) } async CleanupNeverActiveUsers() { this.log("Cleaning up never active users") - const inactiveUsers = await this.storage.userStorage.GetInactiveUsers(30) + const inactiveUsers = await this.storage.userStorage.GetInactiveUsers(90) const toDelete: { userId: string, appUserIds: string[] }[] = [] for (const u of inactiveUsers) { const user = await this.storage.userStorage.GetUser(u.user_id) @@ -160,13 +161,26 @@ export default class { } this.log("Found", toDelete.length, "never active users to delete") - // await this.RemoveUsers(toDelete) TODO: activate deletion + await this.RemoveUsers(toDelete) + } + + async LockUsers(toLock: string[]) { + this.log("Locking", toLock.length, "users") + for (const userId of toLock) { + await this.storage.userStorage.BanUser(userId) + } + this.log("Locked users") } async RemoveUsers(toDelete: { userId: string, appUserIds: string[] }[]) { this.log("Deleting", toDelete.length, "inactive users") for (let i = 0; i < toDelete.length; i++) { const { userId, appUserIds } = toDelete[i] + const user = await this.storage.userStorage.FindUser(userId) + if (!user || user.balance_sats > 0) { + if (user) this.log("Skipping user", userId, "has balance", user.balance_sats) + continue + } this.log("Deleting user", userId, "progress", i + 1, "/", toDelete.length) await this.storage.StartTransaction(async tx => { for (const appUserId of appUserIds) { @@ -174,13 +188,18 @@ export default class { await this.storage.offerStorage.DeleteUserOffers(appUserId, tx) await this.storage.debitStorage.RemoveUserDebitAccess(appUserId, tx) await this.storage.applicationStorage.RemoveAppUserDevices(appUserId, tx) - } await this.storage.paymentStorage.RemoveUserInvoices(userId, tx) await this.storage.productStorage.RemoveUserProducts(userId, tx) await this.storage.paymentStorage.RemoveUserEphemeralKeys(userId, tx) + await this.storage.paymentStorage.RemoveUserInvoicePayments(userId, tx) + await this.storage.paymentStorage.RemoveUserTransactionPayments(userId, tx) + await this.storage.paymentStorage.RemoveUserToUserPayments(userId, tx) + await this.storage.paymentStorage.RemoveUserReceivingAddresses(userId, tx) + await this.storage.userStorage.DeleteUserAccess(userId, tx) + await this.storage.applicationStorage.RemoveAppUsersAndBaseUsers(appUserIds, userId, tx) }) } this.log("Cleaned up inactive users") } -} \ No newline at end of file +} diff --git a/src/services/main/applicationManager.ts b/src/services/main/applicationManager.ts index aaafd8a7..ecfc7b85 100644 --- a/src/services/main/applicationManager.ts +++ b/src/services/main/applicationManager.ts @@ -169,7 +169,8 @@ export default class { ndebit: ndebitEncode({ pubkey: app.nostr_public_key!, pointer: u.identifier, relay: nostrSettings.relays[0] }), nmanage: nmanageEncode({ pubkey: app.nostr_public_key!, pointer: u.identifier, relay: nostrSettings.relays[0] }), callback_url: u.callback_url, - bridge_url: this.settings.getSettings().serviceSettings.bridgeUrl + bridge_url: this.settings.getSettings().serviceSettings.bridgeUrl, + topic_id: u.topic_id }, max_withdrawable: max @@ -227,7 +228,8 @@ export default class { ndebit: ndebitEncode({ pubkey: app.nostr_public_key!, pointer: user.identifier, relay: nostrSettings.relays[0] }), nmanage: nmanageEncode({ pubkey: app.nostr_public_key!, pointer: user.identifier, relay: nostrSettings.relays[0] }), callback_url: user.callback_url, - bridge_url: this.settings.getSettings().serviceSettings.bridgeUrl + bridge_url: this.settings.getSettings().serviceSettings.bridgeUrl, + topic_id: user.topic_id }, } } diff --git a/src/services/main/debitManager.ts b/src/services/main/debitManager.ts index 28579d31..53375217 100644 --- a/src/services/main/debitManager.ts +++ b/src/services/main/debitManager.ts @@ -153,13 +153,14 @@ export class DebitManager { } notifyPaymentSuccess = (debitRes: NdebitSuccess, event: { pub: string, id: string, appId: string }) => { + this.logger("✅ [DEBIT REQUEST] Payment successful, sending OK response to", event.pub.slice(0, 16) + "...", "for event", event.id.slice(0, 16) + "...") this.sendDebitResponse(debitRes, event) } sendDebitResponse = (debitRes: NdebitFailure | NdebitSuccess, event: { pub: string, id: string, appId: string }) => { + this.logger("📤 [DEBIT RESPONSE] Sending Kind 21002 response:", JSON.stringify(debitRes), "to", event.pub.slice(0, 16) + "...") const e = newNdebitResponse(JSON.stringify(debitRes), event) this.storage.NostrSender().Send({ type: 'app', appId: event.appId }, { type: 'event', event: e, encrypt: { toPub: event.pub } }) - } payNdebitInvoice = async (event: NostrEvent, pointerdata: NdebitData): Promise => { diff --git a/src/services/main/index.ts b/src/services/main/index.ts index 3d885681..81de5ada 100644 --- a/src/services/main/index.ts +++ b/src/services/main/index.ts @@ -72,6 +72,7 @@ export default class { this.unlocker = unlocker const updateProviderBalance = (b: number) => this.storage.liquidityStorage.IncrementTrackedProviderBalance('lnPub', settings.getSettings().liquiditySettings.liquidityProviderPub, b) this.liquidityProvider = new LiquidityProvider(() => this.settings.getSettings().liquiditySettings, this.utils, this.invoicePaidCb, updateProviderBalance) + adminManager.attachLiquidityProvider(this.liquidityProvider) this.rugPullTracker = new RugPullTracker(this.storage, this.liquidityProvider) const lndGetSettings = () => ({ lndSettings: settings.getSettings().lndSettings, @@ -161,19 +162,23 @@ export default class { NewBlockHandler = async (height: number, skipMetrics?: boolean) => { let confirmed: (PendingTx & { confs: number; })[] let log = getLogger({}) + log("NewBlockHandler called", JSON.stringify({ height, skipMetrics })) this.storage.paymentStorage.DeleteExpiredTransactionSwaps(height) .catch(err => log(ERROR, "failed to delete expired transaction swaps", err.message || err)) + this.storage.paymentStorage.DeleteExpiredInvoiceSwaps(height) + .catch(err => log(ERROR, "failed to delete expired invoice swaps", err.message || err)) try { const balanceEvents = await this.paymentManager.GetLndBalance() if (!skipMetrics) { await this.metricsManager.NewBlockCb(height, balanceEvents) } - confirmed = await this.paymentManager.CheckNewlyConfirmedTxs(height) + confirmed = await this.paymentManager.CheckNewlyConfirmedTxs() await this.liquidityManager.onNewBlock() } catch (err: any) { log(ERROR, "failed to check transactions after new block", err.message || err) return } + log("NewBlockHandler new confirmed transactions", confirmed.length) await Promise.all(confirmed.map(async c => { if (c.type === 'outgoing') { await this.storage.paymentStorage.UpdateUserTransactionPayment(c.tx.serial_id, { confs: c.confs }) @@ -208,6 +213,7 @@ export default class { addressPaidCb: AddressPaidCb = (txOutput, address, amount, used, broadcastHeight) => { return this.storage.StartTransaction(async tx => { + getLogger({})("addressPaidCb called", JSON.stringify({ txOutput, address, amount, used, broadcastHeight })) // On-chain payments not supported when bypass is enabled if (this.liquidityProvider.getSettings().useOnlyLiquidityProvider) { getLogger({})("addressPaidCb called but USE_ONLY_LIQUIDITY_PROVIDER is enabled, ignoring") @@ -419,13 +425,36 @@ export default class { if (devices.length === 0 || !app.nostr_public_key || !app.nostr_private_key || !appUser.nostr_public_key) { return } + const tokens = devices.map(d => d.firebase_messaging_token) const ck = nip44.getConversationKey(Buffer.from(app.nostr_private_key, 'hex'), appUser.nostr_public_key) - const j = JSON.stringify(op) + + let payloadToEncrypt: Types.PushNotificationPayload; + if (op.inbound) { + payloadToEncrypt = { + data: { + type: Types.PushNotificationPayload_data_type.RECEIVED_OPERATION, + received_operation: op + } + } + } else { + payloadToEncrypt = { + data: { + type: Types.PushNotificationPayload_data_type.SENT_OPERATION, + sent_operation: op + } + } + } + const j = JSON.stringify(payloadToEncrypt) const encrypted = nip44.encrypt(j, ck) - const encryptedData: { encrypted: string, app_npub_hex: string } = { encrypted, app_npub_hex: app.nostr_public_key } + + const envelope: Types.PushNotificationEnvelope = { + topic_id: appUser.topic_id, + app_npub_hex: app.nostr_public_key, + encrypted_payload: encrypted + } const notification: ShockPushNotification = { - message: JSON.stringify(encryptedData), + message: JSON.stringify(envelope), body, title } diff --git a/src/services/main/init.ts b/src/services/main/init.ts index 3cbba602..946061fb 100644 --- a/src/services/main/init.ts +++ b/src/services/main/init.ts @@ -11,7 +11,7 @@ import { AdminManager } from "./adminManager.js" import SettingsManager from "./settingsManager.js" import { LoadStorageSettingsFromEnv } from "../storage/index.js" import { NostrSender } from "../nostr/sender.js" -import { Swaps } from "../lnd/swaps.js" +import { Swaps } from "../lnd/swaps/swaps.js" export type AppData = { privateKey: string; publicKey: string; @@ -79,6 +79,7 @@ export const initMainHandler = async (log: PubLogger, settingsManager: SettingsM await mainHandler.paymentManager.CleanupOldUnpaidInvoices() await mainHandler.appUserManager.CleanupInactiveUsers() await mainHandler.appUserManager.CleanupNeverActiveUsers() + await swaps.ResumeInvoiceSwaps() await mainHandler.paymentManager.watchDog.Start() return { mainHandler, apps, localProviderClient, wizard, adminManager } } diff --git a/src/services/main/liquidityProvider.ts b/src/services/main/liquidityProvider.ts index 1f8fe2dd..a39f86a6 100644 --- a/src/services/main/liquidityProvider.ts +++ b/src/services/main/liquidityProvider.ts @@ -277,14 +277,14 @@ export class LiquidityProvider { return res } - GetOperations = async () => { + GetOperations = async (max = 200) => { if (!this.IsReady()) { throw new Error("liquidity provider is not ready yet, disabled or unreachable") } const res = await this.client.GetUserOperations({ latestIncomingInvoice: { ts: 0, id: 0 }, latestOutgoingInvoice: { ts: 0, id: 0 }, latestIncomingTx: { ts: 0, id: 0 }, latestOutgoingTx: { ts: 0, id: 0 }, latestIncomingUserToUserPayment: { ts: 0, id: 0 }, - latestOutgoingUserToUserPayment: { ts: 0, id: 0 }, max_size: 200 + latestOutgoingUserToUserPayment: { ts: 0, id: 0 }, max_size: max }) if (res.status === 'ERROR') { this.log("error getting operations", res.reason) diff --git a/src/services/main/paymentManager.ts b/src/services/main/paymentManager.ts index c68020dc..7d96898d 100644 --- a/src/services/main/paymentManager.ts +++ b/src/services/main/paymentManager.ts @@ -18,7 +18,7 @@ import { LiquidityManager } from './liquidityManager.js' import { Utils } from '../helpers/utilsWrapper.js' import { UserInvoicePayment } from '../storage/entity/UserInvoicePayment.js' import SettingsManager from './settingsManager.js' -import { Swaps, TransactionSwapData } from '../lnd/swaps.js' +import { Swaps } from '../lnd/swaps/swaps.js' import { Transaction, OutputDetail } from '../../../proto/lnd/lightning.js' import { LndAddress } from '../lnd/lnd.js' import Metrics from '../metrics/index.js' @@ -201,24 +201,11 @@ export default class { } else { log("no missed chain transactions found") } - await this.reprocessStuckPendingTx(log, currentHeight) } catch (err: any) { log(ERROR, "failed to check for missed chain transactions:", err.message || err) } } - reprocessStuckPendingTx = async (log: PubLogger, currentHeight: number) => { - const { incoming } = await this.storage.paymentStorage.GetPendingTransactions() - const found = incoming.find(t => t.broadcast_height < currentHeight - 100) - if (found) { - log("found a possibly stuck pending transaction, reprocessing with full transaction history") - // There is a pending transaction more than 100 blocks old, this is likely a transaction - // that has a broadcast height higher than it actually is, so its not getting picked up when being processed - // by calling new block cb with height of 1, we make sure that even if the transaction has a newer height, it will still be processed - await this.newBlockCb(1, true) - } - } - private async getLatestTransactions(log: PubLogger): Promise<{ txs: Transaction[], currentHeight: number, lndPubkey: string, startHeight: number }> { const lndInfo = await this.lnd.GetInfo() const lndPubkey = lndInfo.identityPubkey @@ -273,14 +260,14 @@ export default class { private async processRootAddressOutput(output: OutputDetail, tx: Transaction, addresses: LndAddress[], log: PubLogger): Promise { const addr = addresses.find(a => a.address === output.address) if (!addr) { - throw new Error(`address ${output.address} not found in list of addresses`) + throw new Error(`root address ${output.address} not found in list of addresses`) } if (addr.change) { log(`ignoring change address ${output.address}`) return false } const outputIndex = Number(output.outputIndex) - const existingRootOp = await this.metrics.GetRootAddressTransaction(output.address, tx.txHash, outputIndex) + const existingRootOp = await this.storage.metricsStorage.GetRootAddressTransaction(output.address, tx.txHash, outputIndex) if (existingRootOp) { return false } @@ -302,8 +289,11 @@ export default class { const amount = Number(output.amount) const outputIndex = Number(output.outputIndex) log(`processing missed chain tx: address=${output.address}, txHash=${tx.txHash}, amount=${amount}, outputIndex=${outputIndex}`) - this.addressPaidCb({ hash: tx.txHash, index: outputIndex }, output.address, amount, 'lnd', startHeight) - .catch(err => log(ERROR, "failed to process user address output:", err.message || err)) + try { + await this.addressPaidCb({ hash: tx.txHash, index: outputIndex }, output.address, amount, 'lnd', startHeight) + } catch (err: any) { + log(ERROR, "failed to process user address output:", err.message || err) + } return true } @@ -605,9 +595,15 @@ export default class { async PayInternalAddress(ctx: Types.UserContext, req: Types.PayAddressRequest): Promise { this.log("paying internal address") + let amount = req.amountSats if (req.swap_operation_id) { + const swap = await this.storage.paymentStorage.GetTransactionSwap(req.swap_operation_id, ctx.app_user_id) + amount = amount > 0 ? amount : swap?.invoice_amount || 0 await this.storage.paymentStorage.DeleteTransactionSwap(req.swap_operation_id) } + if (amount <= 0) { + throw new Error("invalid tx amount") + } const { blockHeight } = await this.lnd.GetInfo() const app = await this.storage.applicationStorage.GetApplication(ctx.app_id) const isManagedUser = ctx.user_id !== app.owner.user_id @@ -634,11 +630,13 @@ export default class { } } - async ListSwaps(ctx: Types.UserContext): Promise { - const payments = await this.storage.paymentStorage.ListSwapPayments(ctx.app_user_id) + async ListTxSwaps(ctx: Types.UserContext): Promise { + console.log("listing tx swaps", { appUserId: ctx.app_user_id }) + const payments = await this.storage.paymentStorage.ListTxSwapPayments(ctx.app_user_id) + console.log("payments", payments.length) const app = await this.storage.applicationStorage.GetApplication(ctx.app_id) const isManagedUser = ctx.user_id !== app.owner.user_id - return this.swaps.ListSwaps(ctx.app_user_id, payments, p => { + return this.swaps.ListTxSwaps(ctx.app_user_id, payments, p => { const opId = `${Types.UserOperationType.OUTGOING_TX}-${p.serial_id}` return this.newInvoicePaymentOperation({ amount: p.paid_amount, confirmed: p.paid_at_unix !== 0, invoice: p.invoice, opId, networkFee: p.routing_fees, serviceFee: p.service_fees, paidAtUnix: p.paid_at_unix }) }, amt => this.getSendServiceFee(Types.UserOperationType.OUTGOING_INVOICE, amt, isManagedUser)) @@ -973,29 +971,38 @@ export default class { return { amount: payment.paid_amount, fees: payment.service_fees } } - async CheckNewlyConfirmedTxs(height: number) { - const pending = await this.storage.paymentStorage.GetPendingTransactions() - let lowestHeight = height - const map: Record = {} - - const checkTx = (t: PendingTx) => { - if (t.tx.broadcast_height < lowestHeight) { lowestHeight = t.tx.broadcast_height } - map[t.tx.tx_hash] = t - } - pending.incoming.forEach(t => checkTx({ type: "incoming", tx: t })) - pending.outgoing.forEach(t => checkTx({ type: "outgoing", tx: t })) - const { transactions } = await this.lnd.GetTransactions(lowestHeight) - const newlyConfirmedTxs = transactions.map(tx => { - const { txHash, numConfirmations: confs, amount: amt } = tx - const t = map[txHash] - if (!t || confs === 0) { - return - } + private async getTxConfs(txHash: string): Promise { + try { + const info = await this.lnd.GetTx(txHash) + const { numConfirmations: confs, amount: amt } = info if (confs > 2 || (amt <= confInTwo && confs > 1) || (amt <= confInOne && confs > 0)) { - return { ...t, confs } + return confs } - }) - return newlyConfirmedTxs.filter(t => t !== undefined) as (PendingTx & { confs: number })[] + } catch (err: any) { + getLogger({})("failed to get tx info", err.message || err) + } + return 0 + } + + async CheckNewlyConfirmedTxs() { + const pending = await this.storage.paymentStorage.GetPendingTransactions() + let log = getLogger({}) + log("CheckNewlyConfirmedTxs ", pending.incoming.length, "incoming", pending.outgoing.length, "outgoing") + const confirmedIncoming: (PendingTx & { confs: number })[] = [] + const confirmedOutgoing: (PendingTx & { confs: number })[] = [] + for (const tx of pending.incoming) { + const confs = await this.getTxConfs(tx.tx_hash) + if (confs > 0) { + confirmedIncoming.push({ type: "incoming", tx: tx, confs }) + } + } + for (const tx of pending.outgoing) { + const confs = await this.getTxConfs(tx.tx_hash) + if (confs > 0) { + confirmedOutgoing.push({ type: "outgoing", tx: tx, confs }) + } + } + return confirmedIncoming.concat(confirmedOutgoing) } async CleanupOldUnpaidInvoices() { diff --git a/src/services/main/sanityChecker.ts b/src/services/main/sanityChecker.ts index 6ef7419a..cb0ecb75 100644 --- a/src/services/main/sanityChecker.ts +++ b/src/services/main/sanityChecker.ts @@ -226,7 +226,7 @@ export default class SanityChecker { async VerifyEventsLog() { this.events = await this.storage.eventsLog.GetAllLogs() - this.invoices = (await this.lnd.GetAllPaidInvoices(1000)).invoices + this.invoices = (await this.lnd.GetAllInvoices(1000)).invoices this.payments = (await this.lnd.GetAllPayments(1000)).payments this.incrementSources = {} diff --git a/src/services/main/watchdog.ts b/src/services/main/watchdog.ts index 778e09ce..3c12b676 100644 --- a/src/services/main/watchdog.ts +++ b/src/services/main/watchdog.ts @@ -29,6 +29,7 @@ export class Watchdog { ready = false interval: NodeJS.Timer; lndPubKey: string; + lastHandlerRootOpsAtUnix = 0 constructor(settings: SettingsManager, liquidityManager: LiquidityManager, lnd: LND, storage: Storage, utils: Utils, rugPullTracker: RugPullTracker) { this.lnd = lnd; this.settings = settings; @@ -67,7 +68,7 @@ export class Watchdog { await this.getTracker() const totalUsersBalance = await this.storage.paymentStorage.GetTotalUsersBalance() this.utils.stateBundler.AddBalancePoint('usersBalance', totalUsersBalance) - const { totalExternal, otherExternal } = await this.getAggregatedExternalBalance() + const { totalExternal } = await this.getAggregatedExternalBalance() this.initialLndBalance = totalExternal this.initialUsersBalance = totalUsersBalance const fwEvents = await this.lnd.GetForwardingHistory(0, this.startedAtUnix) @@ -76,8 +77,6 @@ export class Watchdog { const paymentFound = await this.storage.paymentStorage.GetMaxPaymentIndex() const knownMaxIndex = paymentFound.length > 0 ? Math.max(paymentFound[0].paymentIndex, 0) : 0 this.latestPaymentIndexOffset = await this.lnd.GetLatestPaymentIndex(knownMaxIndex) - const other = { ilnd: this.initialLndBalance, hf: this.accumulatedHtlcFees, iu: this.initialUsersBalance, tu: totalUsersBalance, oext: otherExternal } - //getLogger({ component: 'watchdog_debug2' })(JSON.stringify({ deltaLnd: 0, deltaUsers: 0, totalExternal, latestIndex: this.latestPaymentIndexOffset, other })) this.interval = setInterval(() => { if (this.latestCheckStart + (1000 * 58) < Date.now()) { this.PaymentRequested() @@ -93,7 +92,49 @@ export class Watchdog { fwEvents.forwardingEvents.forEach((event) => { this.accumulatedHtlcFees += Number(event.fee) }) + } + handleRootOperations = async () => { + let pendingChange = 0 + const pendingChainPayments = await this.storage.metricsStorage.GetPendingChainPayments() + for (const payment of pendingChainPayments) { + try { + const tx = await this.lnd.GetTx(payment.operation_identifier) + if (tx.numConfirmations > 0) { + await this.storage.metricsStorage.SetRootOpConfirmed(payment.serial_id) + continue + } + tx.outputDetails.forEach(o => pendingChange += o.isOurAddress ? Number(o.amount) : 0) + } catch (err: any) { + this.log("Error getting tx for root operation", err.message || err) + } + + } + let newReceived = 0 + let newSpent = 0 + if (this.lastHandlerRootOpsAtUnix === 0) { + this.lastHandlerRootOpsAtUnix = Math.floor(Date.now() / 1000) + return { newReceived, newSpent, pendingChange } + } + + const newOps = await this.storage.metricsStorage.GetRootOperations({ from: this.lastHandlerRootOpsAtUnix }) + newOps.forEach(o => { + switch (o.operation_type) { + case 'chain_payment': + newSpent += Number(o.operation_amount) + break + case 'invoice_payment': + newSpent += Number(o.operation_amount) + break + case 'chain': + newReceived += Number(o.operation_amount) + break + case 'invoice': + newReceived += Number(o.operation_amount) + break + } + }) + return { newReceived, newSpent, pendingChange } } getAggregatedExternalBalance = async () => { @@ -101,8 +142,9 @@ export class Watchdog { const feesPaidForLiquidity = this.liquidityManager.GetPaidFees() const pb = await this.rugPullTracker.CheckProviderBalance() const providerBalance = pb.prevBalance || pb.balance - const otherExternal = { pb: providerBalance, f: feesPaidForLiquidity, lnd: totalLndBalance, olnd: othersFromLnd } - return { totalExternal: totalLndBalance + providerBalance + feesPaidForLiquidity, otherExternal } + const { newReceived, newSpent, pendingChange } = await this.handleRootOperations() + const opsTotal = newReceived + pendingChange - newSpent + return { totalExternal: totalLndBalance + providerBalance + feesPaidForLiquidity + opsTotal } } checkBalanceUpdate = async (deltaLnd: number, deltaUsers: number) => { @@ -187,7 +229,7 @@ export class Watchdog { } const totalUsersBalance = await this.storage.paymentStorage.GetTotalUsersBalance() this.utils.stateBundler.AddBalancePoint('usersBalance', totalUsersBalance) - const { totalExternal, otherExternal } = await this.getAggregatedExternalBalance() + const { totalExternal } = await this.getAggregatedExternalBalance() this.utils.stateBundler.AddBalancePoint('accumulatedHtlcFees', this.accumulatedHtlcFees) const deltaLnd = totalExternal - (this.initialLndBalance + this.accumulatedHtlcFees) const deltaUsers = totalUsersBalance - this.initialUsersBalance @@ -196,15 +238,17 @@ 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") - this.lnd.LockOutgoingOperations() - return + this.log("Payment index advanced from", knownMaxIndex, "to", newLatest, "- updating offset (likely LND restart or external payment)") + this.latestPaymentIndexOffset = newLatest } + const deny = await this.checkBalanceUpdate(deltaLnd, deltaUsers) if (deny) { + if (historyMismatch) { + getLogger({ component: 'bark' })("Balance mismatch with unexpected payment history, locking outgoing operations") + this.lnd.LockOutgoingOperations() + return + } this.log("Balance mismatch detected in absolute update, but history is ok") } this.lnd.UnlockOutgoingOperations() diff --git a/src/services/metrics/index.ts b/src/services/metrics/index.ts index 3dbafedb..9e23b665 100644 --- a/src/services/metrics/index.ts +++ b/src/services/metrics/index.ts @@ -414,9 +414,7 @@ export default class Handler { await this.storage.metricsStorage.AddRootOperation("chain", `${address}:${txOutput.hash}:${txOutput.index}`, amount) } - async GetRootAddressTransaction(address: string, txHash: string, index: number) { - return this.storage.metricsStorage.GetRootOperation("chain", `${address}:${txHash}:${index}`) - } + async AddRootInvoicePaid(paymentRequest: string, amount: number) { await this.storage.metricsStorage.AddRootOperation("invoice", paymentRequest, amount) @@ -425,8 +423,13 @@ export default class Handler { const mapRootOpType = (opType: string): Types.OperationType => { switch (opType) { - case "chain": return Types.OperationType.CHAIN_OP - case "invoice": return Types.OperationType.INVOICE_OP + case "chain_payment": + case "chain": + return Types.OperationType.CHAIN_OP + case "invoice_payment": + case "invoice": + return Types.OperationType.INVOICE_OP + default: throw new Error("Unknown operation type") } } \ No newline at end of file diff --git a/src/services/nostr/handler.ts b/src/services/nostr/handler.ts index d1fa46ec..b9ecfe70 100644 --- a/src/services/nostr/handler.ts +++ b/src/services/nostr/handler.ts @@ -132,12 +132,12 @@ const handleNostrSettings = (settings: NostrSettings) => { send(event) }) } */ -const sendToNostr: NostrSend = (initiator, data, relays) => { +const sendToNostr: NostrSend = async (initiator, data, relays) => { if (!subProcessHandler) { getLogger({ component: "nostrMiddleware" })(ERROR, "nostr was not initialized") return } - subProcessHandler.Send(initiator, data, relays) + await subProcessHandler.Send(initiator, data, relays) } send({ type: 'ready' }) diff --git a/src/services/nostr/index.ts b/src/services/nostr/index.ts index 3dce7b41..ffa636b6 100644 --- a/src/services/nostr/index.ts +++ b/src/services/nostr/index.ts @@ -11,28 +11,69 @@ export default class NostrSubprocess { utils: Utils awaitingPongs: (() => void)[] = [] log = getLogger({}) + latestRestart = 0 + private settings: NostrSettings + private eventCallback: EventCallback + private beaconCallback: BeaconCallback + private isShuttingDown = false + constructor(settings: NostrSettings, utils: Utils, eventCallback: EventCallback, beaconCallback: BeaconCallback) { this.utils = utils + this.settings = settings + this.eventCallback = eventCallback + this.beaconCallback = beaconCallback + this.startSubProcess() + } + + private cleanupProcess() { + if (this.childProcess) { + this.childProcess.removeAllListeners() + if (!this.childProcess.killed) { + this.childProcess.kill('SIGTERM') + } + } + } + + private startSubProcess() { + this.cleanupProcess() + this.childProcess = fork("./build/src/services/nostr/handler") + this.childProcess.on("error", (error) => { this.log(ERROR, "nostr subprocess error", error) }) - this.childProcess.on("exit", (code) => { - this.log(ERROR, `nostr subprocess exited with code ${code}`) - if (!code) { + this.childProcess.on("exit", (code, signal) => { + if (this.isShuttingDown) { + this.log("nostr subprocess stopped") return } - throw new Error(`nostr subprocess exited with code ${code}`) + + if (code === 0) { + this.log("nostr subprocess exited cleanly") + return + } + + this.log(ERROR, `nostr subprocess exited with code ${code} and signal ${signal}`) + + const now = Date.now() + if (now - this.latestRestart < 5000) { + this.log(ERROR, "nostr subprocess exited too quickly, not restarting") + throw new Error("nostr subprocess crashed repeatedly") + } + + this.log("restarting nostr subprocess...") + this.latestRestart = now + setTimeout(() => this.startSubProcess(), 100) }) this.childProcess.on("message", (message: ChildProcessResponse) => { switch (message.type) { case 'ready': - this.sendToChildProcess({ type: 'settings', settings: settings }) + this.sendToChildProcess({ type: 'settings', settings: this.settings }) break; case 'event': - eventCallback(message.event) + this.eventCallback(message.event) break case 'processMetrics': this.utils.tlvStorageFactory.ProcessMetrics(message.metrics, 'nostr') @@ -42,7 +83,7 @@ export default class NostrSubprocess { this.awaitingPongs = [] break case 'beacon': - beaconCallback({ content: message.content, pub: message.pub }) + this.beaconCallback({ content: message.content, pub: message.pub }) break default: console.error("unknown nostr event response", message) @@ -50,11 +91,15 @@ export default class NostrSubprocess { } }) } + sendToChildProcess(message: ChildProcessRequest) { - this.childProcess.send(message) + if (this.childProcess && !this.childProcess.killed) { + this.childProcess.send(message) + } } Reset(settings: NostrSettings) { + this.settings = settings this.sendToChildProcess({ type: 'settings', settings }) } @@ -68,7 +113,9 @@ export default class NostrSubprocess { Send(initiator: SendInitiator, data: SendData, relays?: string[]) { this.sendToChildProcess({ type: 'send', data, initiator, relays }) } + Stop() { - this.childProcess.kill() + this.isShuttingDown = true + this.cleanupProcess() } } diff --git a/src/services/nostr/nostrPool.ts b/src/services/nostr/nostrPool.ts index d41da382..ee610a35 100644 --- a/src/services/nostr/nostrPool.ts +++ b/src/services/nostr/nostrPool.ts @@ -16,7 +16,7 @@ export type SendDataContent = { type: "content", content: string, pub: string } export type SendDataEvent = { type: "event", event: UnsignedEvent, encrypt?: { toPub: string } } export type SendData = SendDataContent | SendDataEvent export type SendInitiator = { type: 'app', appId: string } | { type: 'client', clientId: string } -export type NostrSend = (initiator: SendInitiator, data: SendData, relays?: string[] | undefined) => void +export type NostrSend = (initiator: SendInitiator, data: SendData, relays?: string[] | undefined) => Promise export type LinkedProviderInfo = { pubkey: string, clientId: string, relayUrl: string } export type AppInfo = { appId: string, publicKey: string, privateKey: string, name: string, provider?: LinkedProviderInfo } @@ -203,21 +203,26 @@ export class NostrPool { const signed = finalizeEvent(event, Buffer.from(keys.privateKey, 'hex')) let sent = false const log = getLogger({ appName: keys.name }) - // const r = relays ? relays : this.getServiceRelays() + this.log(`📤 Publishing Kind ${event.kind} event to ${relays.length} relay(s): ${relays.join(', ')}`) const pool = new SimplePool() - await Promise.all(pool.publish(relays, signed).map(async p => { - try { - await p - sent = true - } catch (e: any) { - console.log(e) - log(e) + try { + await Promise.all(pool.publish(relays, signed).map(async p => { + try { + await p + sent = true + } catch (e: any) { + this.log(ERROR, `Failed to publish Kind ${event.kind} event:`, e.message || e) + log(e) + } + })) + if (!sent) { + this.log(ERROR, `Failed to send Kind ${event.kind} event to any relay`) + log("failed to send event") + } else { + this.log(`✅ Kind ${event.kind} event published successfully (id: ${signed.id.slice(0, 16)}...)`) } - })) - if (!sent) { - log("failed to send event") - } else { - //log("sent event") + } finally { + pool.close(relays) } } diff --git a/src/services/nostr/sender.ts b/src/services/nostr/sender.ts index 1fd336a5..8437b9af 100644 --- a/src/services/nostr/sender.ts +++ b/src/services/nostr/sender.ts @@ -1,7 +1,7 @@ import { NostrSend, SendData, SendInitiator } from "./nostrPool.js" -import { getLogger } from "../helpers/logger.js" +import { ERROR, getLogger } from "../helpers/logger.js" export class NostrSender { - private _nostrSend: NostrSend = () => { throw new Error('nostr send not initialized yet') } + private _nostrSend: NostrSend = async () => { throw new Error('nostr send not initialized yet') } private isReady: boolean = false private onReadyCallbacks: (() => void)[] = [] private pendingSends: { initiator: SendInitiator, data: SendData, relays?: string[] | undefined }[] = [] @@ -12,7 +12,12 @@ export class NostrSender { this.isReady = true this.onReadyCallbacks.forEach(cb => cb()) this.onReadyCallbacks = [] - this.pendingSends.forEach(send => this._nostrSend(send.initiator, send.data, send.relays)) + // Process pending sends with proper error handling + this.pendingSends.forEach(send => { + this._nostrSend(send.initiator, send.data, send.relays).catch(e => { + this.log(ERROR, "failed to send pending event", e.message || e) + }) + }) this.pendingSends = [] } OnReady(callback: () => void) { @@ -22,13 +27,16 @@ export class NostrSender { this.onReadyCallbacks.push(callback) } } - Send(initiator: SendInitiator, data: SendData, relays?: string[] | undefined) { + Send(initiator: SendInitiator, data: SendData, relays?: string[] | undefined): void { if (!this.isReady) { this.log("tried to send before nostr was ready, caching request") this.pendingSends.push({ initiator, data, relays }) return } - this._nostrSend(initiator, data, relays) + // Fire and forget but log errors + this._nostrSend(initiator, data, relays).catch(e => { + this.log(ERROR, "failed to send event", e.message || e) + }) } IsReady() { return this.isReady diff --git a/src/services/serverMethods/index.ts b/src/services/serverMethods/index.ts index 8b9b80a3..d74e67fd 100644 --- a/src/services/serverMethods/index.ts +++ b/src/services/serverMethods/index.ts @@ -91,6 +91,14 @@ export default (mainHandler: Main): Types.ServerMethods => { if (err != null) throw new Error(err.message) return mainHandler.adminManager.CloseChannel(req) }, + BumpTx: async ({ ctx, req }) => { + const err = Types.BumpTxValidate(req, { + txid_CustomCheck: txid => txid !== '', + sat_per_vbyte_CustomCheck: spv => spv > 0 + }) + if (err != null) throw new Error(err.message) + return mainHandler.adminManager.BumpTx(req) + }, GetAdminTransactionSwapQuotes: async ({ ctx, req }) => { const err = Types.TransactionSwapRequestValidate(req, { transaction_amount_sats_CustomCheck: amt => amt > 0 @@ -106,6 +114,36 @@ export default (mainHandler: Main): Types.ServerMethods => { if (err != null) throw new Error(err.message) return mainHandler.adminManager.PayAdminTransactionSwap(req) }, + ListAdminTxSwaps: async ({ ctx }) => { + return mainHandler.adminManager.ListAdminTxSwaps() + }, + GetAdminInvoiceSwapQuotes: async ({ ctx, req }) => { + const err = Types.InvoiceSwapRequestValidate(req, { + amount_sats_CustomCheck: amt => amt > 0 + }) + if (err != null) throw new Error(err.message) + return mainHandler.adminManager.GetAdminInvoiceSwapQuotes(req) + }, + RefundAdminInvoiceSwap: async ({ ctx, req }) => { + const err = Types.RefundAdminInvoiceSwapRequestValidate(req, { + swap_operation_id_CustomCheck: id => id !== '', + }) + if (err != null) throw new Error(err.message) + return mainHandler.adminManager.RefundAdminInvoiceSwap(req) + }, + ListAdminInvoiceSwaps: async ({ ctx }) => { + return mainHandler.adminManager.ListAdminInvoiceSwaps() + }, + PayAdminInvoiceSwap: async ({ ctx, req }) => { + const err = Types.PayAdminInvoiceSwapRequestValidate(req, { + swap_operation_id_CustomCheck: id => id !== '', + }) + if (err != null) throw new Error(err.message) + return mainHandler.adminManager.PayAdminInvoiceSwap(req) + }, + GetAssetsAndLiabilities: async ({ ctx, req }) => { + return mainHandler.adminManager.GetAssetsAndLiabilities(req) + }, GetProvidersDisruption: async () => { return mainHandler.metricsManager.GetProvidersDisruption() }, @@ -145,9 +183,7 @@ export default (mainHandler: Main): Types.ServerMethods => { GetUserOperations: async ({ ctx, req }) => { return mainHandler.paymentManager.GetUserOperations(ctx.user_id, req) }, - ListAdminSwaps: async ({ ctx }) => { - return mainHandler.adminManager.ListAdminSwaps() - }, + GetPaymentState: async ({ ctx, req }) => { const err = Types.GetPaymentStateRequestValidate(req, { invoice_CustomCheck: invoice => invoice !== "" @@ -159,14 +195,14 @@ export default (mainHandler: Main): Types.ServerMethods => { PayAddress: async ({ ctx, req }) => { const err = Types.PayAddressRequestValidate(req, { address_CustomCheck: addr => addr !== '', - amountSats_CustomCheck: amt => amt > 0, + // amountSats_CustomCheck: amt => amt > 0, // satsPerVByte_CustomCheck: spb => spb > 0 }) if (err != null) throw new Error(err.message) return mainHandler.paymentManager.PayAddress(ctx, req) }, - ListSwaps: async ({ ctx }) => { - return mainHandler.paymentManager.ListSwaps(ctx) + ListTxSwaps: async ({ ctx }) => { + return mainHandler.paymentManager.ListTxSwaps(ctx) }, GetTransactionSwapQuotes: async ({ ctx, req }) => { return mainHandler.paymentManager.GetTransactionSwapQuotes(ctx, req) diff --git a/src/services/storage/applicationStorage.ts b/src/services/storage/applicationStorage.ts index 21f6f9c9..402e7773 100644 --- a/src/services/storage/applicationStorage.ts +++ b/src/services/storage/applicationStorage.ts @@ -1,5 +1,5 @@ import crypto from 'crypto'; -import { Between, FindOperator, IsNull, LessThanOrEqual, MoreThanOrEqual, In } from "typeorm" +import { Between, FindOperator, IsNull, LessThanOrEqual, MoreThanOrEqual } from "typeorm" import { generateSecretKey, getPublicKey } from 'nostr-tools'; import { Application } from "./entity/Application.js" import UserStorage from './userStorage.js'; @@ -72,7 +72,8 @@ export default class { user: user, application, identifier: userIdentifier, - nostr_public_key: nostrPub + nostr_public_key: nostrPub, + topic_id: crypto.randomBytes(32).toString('hex') }, txId) }) } @@ -160,10 +161,16 @@ export default class { this.dbs.Remove('User', baseUser, txId) } - async RemoveAppUsersAndBaseUsers(appUserIds: string[],baseUser:string, txId?: string) { - await this.dbs.Delete('ApplicationUser', { identifier: In(appUserIds) }, txId) - await this.dbs.Delete('User', { user_id: baseUser }, txId) - + async RemoveAppUsersAndBaseUsers(appUserIds: string[], baseUser: string, txId?: string) { + for (const appUserId of appUserIds) { + const appUser = await this.dbs.FindOne('ApplicationUser', { where: { identifier: appUserId } }, txId) + if (appUser) { + await this.dbs.Delete('ApplicationUser', appUser.serial_id, txId) + } + } + const user = await this.userStorage.FindUser(baseUser, txId) + if (!user) return + await this.dbs.Delete('User', user.serial_id, txId) } diff --git a/src/services/storage/db/db.ts b/src/services/storage/db/db.ts index 335f6848..63397c58 100644 --- a/src/services/storage/db/db.ts +++ b/src/services/storage/db/db.ts @@ -30,6 +30,7 @@ import * as fs from 'fs' import { UserAccess } from "../entity/UserAccess.js" import { AdminSettings } from "../entity/AdminSettings.js" import { TransactionSwap } from "../entity/TransactionSwap.js" +import { InvoiceSwap } from "../entity/InvoiceSwap.js" export type DbSettings = { @@ -76,7 +77,8 @@ export const MainDbEntities = { 'AppUserDevice': AppUserDevice, 'UserAccess': UserAccess, 'AdminSettings': AdminSettings, - 'TransactionSwap': TransactionSwap + 'TransactionSwap': TransactionSwap, + 'InvoiceSwap': InvoiceSwap } export type MainDbNames = keyof typeof MainDbEntities export const MainDbEntitiesNames = Object.keys(MainDbEntities) diff --git a/src/services/storage/db/serializationHelpers.ts b/src/services/storage/db/serializationHelpers.ts index eb27f678..ee19201f 100644 --- a/src/services/storage/db/serializationHelpers.ts +++ b/src/services/storage/db/serializationHelpers.ts @@ -8,10 +8,19 @@ type SerializedFindOperator = { } export function serializeFindOperator(operator: FindOperator): SerializedFindOperator { + let value: any; + if (Array.isArray(operator['value']) && operator['type'] !== 'between') { + value = operator['value'].map(serializeFindOperator); + } else if ((operator as any).child !== undefined) { + // Not(IsNull()) etc.: TypeORM's .value getter unwraps nested FindOperators, so we'd lose the inner operator. Use .child to serialize the nested operator. + value = serializeFindOperator((operator as any).child); + } else { + value = operator['value']; + } return { _type: 'FindOperator', type: operator['type'], - value: (Array.isArray(operator['value']) && operator['type'] !== 'between') ? operator["value"].map(serializeFindOperator) : operator["value"], + value, }; } @@ -51,7 +60,8 @@ export function deserializeFindOperator(serialized: SerializedFindOperator): Fin } } -export function serializeRequest(r: object): T { +export function serializeRequest(r: object, debug = false): T { + if (debug) console.log("serializeRequest", r) if (!r || typeof r !== 'object') { return r; } @@ -61,23 +71,24 @@ export function serializeRequest(r: object): T { } if (Array.isArray(r)) { - return r.map(item => serializeRequest(item)) as any; + return r.map(item => serializeRequest(item, debug)) as any; } const result: any = {}; for (const [key, value] of Object.entries(r)) { - result[key] = serializeRequest(value); + result[key] = serializeRequest(value, debug); } return result; } -export function deserializeRequest(r: object): T { +export function deserializeRequest(r: object, debug = false): T { + if (debug) console.log("deserializeRequest", r) if (!r || typeof r !== 'object') { return r; } if (Array.isArray(r)) { - return r.map(item => deserializeRequest(item)) as any; + return r.map(item => deserializeRequest(item, debug)) as any; } if (r && typeof r === 'object' && (r as any)._type === 'FindOperator') { @@ -86,7 +97,7 @@ export function deserializeRequest(r: object): T { const result: any = {}; for (const [key, value] of Object.entries(r)) { - result[key] = deserializeRequest(value); + result[key] = deserializeRequest(value, debug); } return result; } diff --git a/src/services/storage/db/storageInterface.ts b/src/services/storage/db/storageInterface.ts index 941d9533..976ec20d 100644 --- a/src/services/storage/db/storageInterface.ts +++ b/src/services/storage/db/storageInterface.ts @@ -29,7 +29,7 @@ export class StorageInterface extends EventEmitter { private debug: boolean = false; private utils: Utils private dbType: 'main' | 'metrics' - private log = getLogger({component: 'StorageInterface'}) + private log = getLogger({ component: 'StorageInterface' }) constructor(utils: Utils) { super(); this.initializeSubprocess(); @@ -61,13 +61,13 @@ export class StorageInterface extends EventEmitter { this.isConnected = false; }); - this.process.on('exit', (code: number) => { - this.log(ERROR, `Storage processor exited with code ${code}`); + this.process.on('exit', (code: number, signal: string) => { + this.log(ERROR, `Storage processor exited with code ${code} and signal ${signal}`); this.isConnected = false; - if (!code) { + if (code === 0) { return } - throw new Error(`Storage processor exited with code ${code}`) + throw new Error(`Storage processor exited with code ${code} and signal ${signal}`) }); this.isConnected = true; @@ -104,9 +104,10 @@ export class StorageInterface extends EventEmitter { return this.handleOp(findOp) } - Find(entity: DBNames, q: QueryOptions, txId?: string): Promise { + Find(entity: DBNames, q: QueryOptions, txId?: string, debug = false): Promise { + if (debug) console.log("Find", { entity }) const opId = Math.random().toString() - const findOp: FindOperation = { type: 'find', entity, opId, q, txId } + const findOp: FindOperation = { type: 'find', entity, opId, q, txId, debug } return this.handleOp(findOp) } @@ -166,32 +167,33 @@ export class StorageInterface extends EventEmitter { } private handleOp(op: IStorageOperation): Promise { - if (this.debug) console.log('handleOp', op) + if (this.debug || op.debug) console.log('handleOp', op) this.checkConnected() return new Promise((resolve, reject) => { const responseHandler = (response: OperationResponse) => { - if (this.debug) console.log('responseHandler', response) + if (this.debug || op.debug) console.log('responseHandler', response) if (!response.success) { reject(new Error(response.error)); return } + if (this.debug || op.debug) console.log("response", response, op) if (response.type !== op.type) { reject(new Error('Invalid storage response type')); return } - resolve(deserializeResponseData(response.data)); + resolve(deserializeResponseData(response.data)); } this.once(op.opId, responseHandler) this.process.send(this.serializeOperation(op)) }) } - private serializeOperation(operation: IStorageOperation): IStorageOperation { + private serializeOperation(operation: IStorageOperation, debug = false): IStorageOperation { const serialized = { ...operation }; if ('q' in serialized) { - (serialized as any).q = serializeRequest((serialized as any).q); + (serialized as any).q = serializeRequest((serialized as any).q, debug); } - if (this.debug) { + if (this.debug || debug) { serialized.debug = true } return serialized; @@ -205,7 +207,7 @@ export class StorageInterface extends EventEmitter { public disconnect() { if (this.process) { - this.process.kill(); + this.process.kill(0); this.isConnected = false; this.debug = false; } diff --git a/src/services/storage/entity/ApplicationUser.ts b/src/services/storage/entity/ApplicationUser.ts index 2284b9bd..c4717721 100644 --- a/src/services/storage/entity/ApplicationUser.ts +++ b/src/services/storage/entity/ApplicationUser.ts @@ -26,6 +26,9 @@ export class ApplicationUser { @Column({ default: "" }) callback_url: string + @Column({ unique: true }) + topic_id: string; + @CreateDateColumn() created_at: Date diff --git a/src/services/storage/entity/InvoiceSwap.ts b/src/services/storage/entity/InvoiceSwap.ts new file mode 100644 index 00000000..746756b8 --- /dev/null +++ b/src/services/storage/entity/InvoiceSwap.ts @@ -0,0 +1,94 @@ +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, ManyToOne, JoinColumn, UpdateDateColumn } from "typeorm"; +import { User } from "./User"; + +@Entity() +export class InvoiceSwap { + @PrimaryGeneratedColumn('uuid') + swap_operation_id: string + + @Column() + app_user_id: string + + @Column() + swap_quote_id: string + + @Column() + swap_tree: string + + @Column() + claim_public_key: string + + @Column() + payment_hash: string + + /* @Column() + lockup_address: string */ + + /* @Column() + refund_public_key: string */ + + @Column() + timeout_block_height: number + + @Column() + invoice: string + + @Column() + invoice_amount: number + + @Column() + transaction_amount: number + + @Column() + swap_fee_sats: number + + @Column() + chain_fee_sats: number + + + + @Column() + ephemeral_public_key: string + + @Column() + address: string + + // the private key is used on to perform a swap, it does not hold any funds once the swap is completed + // the swap should only last a few seconds, so it is not a security risk to store the private key in the database + // the key is stored here mostly for recovery purposes, in case something goes wrong with the swap + @Column() + ephemeral_private_key: string + + @Column({ default: false }) + used: boolean + + @Column({ default: 0 }) + completed_at_unix: number + + @Column({ default: 0 }) + paid_at_unix: number + + @Column({ default: "" }) + preimage: string + + @Column({ default: "" }) + failure_reason: string + + @Column({ default: "" }) + tx_id: string + + @Column({ default: "", type: "text" }) + lockup_tx_hex: string + + /* @Column({ default: "" }) + address_paid: string */ + + @Column({ default: "" }) + service_url: string + + @CreateDateColumn() + created_at: Date + + @UpdateDateColumn() + updated_at: Date +} \ No newline at end of file diff --git a/src/services/storage/entity/RootOperation.ts b/src/services/storage/entity/RootOperation.ts index 3a2bd1f9..ec4914bc 100644 --- a/src/services/storage/entity/RootOperation.ts +++ b/src/services/storage/entity/RootOperation.ts @@ -17,6 +17,9 @@ export class RootOperation { @Column({ default: 0 }) at_unix: number + @Column({ default: false }) + pending: boolean + @CreateDateColumn() created_at: Date diff --git a/src/services/storage/entity/TransactionSwap.ts b/src/services/storage/entity/TransactionSwap.ts index 7403e2a1..d2a99f52 100644 --- a/src/services/storage/entity/TransactionSwap.ts +++ b/src/services/storage/entity/TransactionSwap.ts @@ -60,6 +60,12 @@ export class TransactionSwap { @Column({ default: "" }) tx_id: string + @Column({ default: 0 }) + completed_at_unix: number + + @Column({ default: 0 }) + paid_at_unix: number + @Column({ default: "" }) address_paid: string diff --git a/src/services/storage/metricsStorage.ts b/src/services/storage/metricsStorage.ts index 44da304c..d80d11c9 100644 --- a/src/services/storage/metricsStorage.ts +++ b/src/services/storage/metricsStorage.ts @@ -10,6 +10,7 @@ import { StorageInterface } from "./db/storageInterface.js"; import { Utils } from "../helpers/utilsWrapper.js"; import { Channel, ChannelEventUpdate } from "../../../proto/lnd/lightning.js"; import { ChannelEvent } from "./entity/ChannelEvent.js"; +export type RootOperationType = 'chain' | 'invoice' | 'chain_payment' | 'invoice_payment' export default class { //DB: DataSource | EntityManager settings: StorageSettings @@ -145,13 +146,27 @@ export default class { } } - async AddRootOperation(opType: string, id: string, amount: number, txId?: string) { - return this.dbs.CreateAndSave('RootOperation', { operation_type: opType, operation_amount: amount, operation_identifier: id, at_unix: Math.floor(Date.now() / 1000) }, txId) + async AddRootOperation(opType: RootOperationType, id: string, amount: number, pending = false, dbTxId?: string) { + return this.dbs.CreateAndSave('RootOperation', { + operation_type: opType, operation_amount: amount, + operation_identifier: id, at_unix: Math.floor(Date.now() / 1000), pending + }, dbTxId) } - async GetRootOperation(opType: string, id: string, txId?: string) { + async GetRootOperation(opType: RootOperationType, id: string, txId?: string) { return this.dbs.FindOne('RootOperation', { 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', { where: { operation_type: 'chain_payment', pending: true } }) + } + + async SetRootOpConfirmed(serialId: number) { + return this.dbs.Update('RootOperation', serialId, { pending: false }) + } async GetRootOperations({ from, to }: { from?: number, to?: number }, txId?: string) { const q = getTimeQuery({ from, to }) diff --git a/src/services/storage/migrations/1769529793283-invoice_swaps.ts b/src/services/storage/migrations/1769529793283-invoice_swaps.ts new file mode 100644 index 00000000..f7b93755 --- /dev/null +++ b/src/services/storage/migrations/1769529793283-invoice_swaps.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class InvoiceSwaps1769529793283 implements MigrationInterface { + name = 'InvoiceSwaps1769529793283' + + public async up(queryRunner: QueryRunner): Promise { + 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 { + await queryRunner.query(`DROP TABLE "invoice_swap"`); + } + +} diff --git a/src/services/storage/migrations/1769805357459-invoice_swaps_fixes.ts b/src/services/storage/migrations/1769805357459-invoice_swaps_fixes.ts new file mode 100644 index 00000000..3ba13031 --- /dev/null +++ b/src/services/storage/migrations/1769805357459-invoice_swaps_fixes.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class InvoiceSwapsFixes1769805357459 implements MigrationInterface { + name = 'InvoiceSwapsFixes1769805357459' + + public async up(queryRunner: QueryRunner): Promise { + 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 { + 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"`); + } + +} diff --git a/src/services/storage/migrations/1770038768784-application_user_topic_id.ts b/src/services/storage/migrations/1770038768784-application_user_topic_id.ts new file mode 100644 index 00000000..b520cbe9 --- /dev/null +++ b/src/services/storage/migrations/1770038768784-application_user_topic_id.ts @@ -0,0 +1,24 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class ApplicationUserTopicId1770038768784 implements MigrationInterface { + name = 'ApplicationUserTopicId1770038768784' + + public async up(queryRunner: QueryRunner): Promise { + 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 { + 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") `); + } + +} diff --git a/src/services/storage/migrations/1771347307798-swap_timestamps.ts b/src/services/storage/migrations/1771347307798-swap_timestamps.ts new file mode 100644 index 00000000..ce5c0b39 --- /dev/null +++ b/src/services/storage/migrations/1771347307798-swap_timestamps.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class SwapTimestamps1771347307798 implements MigrationInterface { + name = 'SwapTimestamps1771347307798' + + public async up(queryRunner: QueryRunner): Promise { + 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 { + 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"`); + } + +} diff --git a/src/services/storage/migrations/1771524665409-root_op_pending.ts b/src/services/storage/migrations/1771524665409-root_op_pending.ts new file mode 100644 index 00000000..e65461f3 --- /dev/null +++ b/src/services/storage/migrations/1771524665409-root_op_pending.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class RootOpPending1771524665409 implements MigrationInterface { + name = 'RootOpPending1771524665409' + + public async up(queryRunner: QueryRunner): Promise { + 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 { + 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"`); + } + +} diff --git a/src/services/storage/migrations/1771878683383-tx_swap_timestamps.ts b/src/services/storage/migrations/1771878683383-tx_swap_timestamps.ts new file mode 100644 index 00000000..be9ccade --- /dev/null +++ b/src/services/storage/migrations/1771878683383-tx_swap_timestamps.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class TxSwapTimestamps1771878683383 implements MigrationInterface { + name = 'TxSwapTimestamps1771878683383' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "temporary_transaction_swap" ("swap_operation_id" varchar PRIMARY KEY NOT NULL, "app_user_id" varchar NOT NULL, "swap_quote_id" varchar NOT NULL, "swap_tree" varchar NOT NULL, "lockup_address" varchar NOT NULL, "refund_public_key" varchar NOT NULL, "timeout_block_height" integer NOT NULL, "invoice" varchar NOT NULL, "invoice_amount" integer NOT NULL, "transaction_amount" integer NOT NULL, "swap_fee_sats" integer NOT NULL, "chain_fee_sats" integer NOT NULL, "preimage" varchar NOT NULL, "ephemeral_public_key" varchar NOT NULL, "ephemeral_private_key" varchar NOT NULL, "used" boolean NOT NULL DEFAULT (0), "failure_reason" varchar NOT NULL DEFAULT (''), "tx_id" varchar NOT NULL DEFAULT (''), "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')), "address_paid" varchar NOT NULL DEFAULT (''), "service_url" varchar NOT NULL DEFAULT (''), "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 { + 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"`); + } + +} diff --git a/src/services/storage/migrations/runner.ts b/src/services/storage/migrations/runner.ts index d14b8381..c78af2eb 100644 --- a/src/services/storage/migrations/runner.ts +++ b/src/services/storage/migrations/runner.ts @@ -1,28 +1,21 @@ import { Initial1703170309875 } from './1703170309875-initial.js' -import { LndMetrics1703170330183 } from './1703170330183-lnd_metrics.js' -import { ChannelRouting1709316653538 } from './1709316653538-channel_routing.js' import { LspOrder1718387847693 } from './1718387847693-lsp_order.js' import { LiquidityProvider1719335699480 } from './1719335699480-liquidity_provider.js' import { LndNodeInfo1720187506189 } from './1720187506189-lnd_node_info.js' import { TrackedProvider1720814323679 } from './1720814323679-tracked_provider.js' import { CreateInviteTokenTable1721751414878 } from "./1721751414878-create_invite_token_table.js" import { PaymentIndex1721760297610 } from './1721760297610-payment_index.js' -import { HtlcCount1724266887195 } from './1724266887195-htlc_count.js' -import { BalanceEvents1724860966825 } from './1724860966825-balance_events.js' import { DebitAccess1726496225078 } from './1726496225078-debit_access.js' import { DebitAccessFixes1726685229264 } from './1726685229264-debit_access_fixes.js' import { DebitToPub1727105758354 } from './1727105758354-debit_to_pub.js' import { UserCbUrl1727112281043 } from './1727112281043-user_cb_url.js' -import { RootOps1732566440447 } from './1732566440447-root_ops.js' import { UserOffer1733502626042 } from './1733502626042-user_offer.js' -import { RootOpsTime1745428134124 } from './1745428134124-root_ops_time.js' -import { ChannelEvents1750777346411 } from './1750777346411-channel_events.js' import { ManagementGrant1751307732346 } from './1751307732346-management_grant.js' import { ManagementGrantBanned1751989251513 } from './1751989251513-management_grant_banned.js' import { InvoiceCallbackUrls1752425992291 } from './1752425992291-invoice_callback_urls.js' -import { AppUserDevice1753285173175 } from './1753285173175-app_user_device.js' import { OldSomethingLeftover1753106599604 } from './1753106599604-old_something_leftover.js' import { UserReceivingInvoiceIdx1753109184611 } from './1753109184611-user_receiving_invoice_idx.js' +import { AppUserDevice1753285173175 } from './1753285173175-app_user_device.js' import { UserAccess1759426050669 } from './1759426050669-user_access.js' import { AddBlindToUserOffer1760000000000 } from './1760000000000-add_blind_to_user_offer.js' import { ApplicationAvatarUrl1761000001000 } from './1761000001000-application_avatar_url.js' @@ -32,6 +25,23 @@ import { TxSwapAddress1764779178945 } from './1764779178945-tx_swap_address.js' import { ClinkRequester1765497600000 } from './1765497600000-clink_requester.js' import { TrackedProviderHeight1766504040000 } from './1766504040000-tracked_provider_height.js' import { SwapsServiceUrl1768413055036 } from './1768413055036-swaps_service_url.js' +import { InvoiceSwaps1769529793283 } from './1769529793283-invoice_swaps.js' +import { InvoiceSwapsFixes1769805357459 } from './1769805357459-invoice_swaps_fixes.js' +import { ApplicationUserTopicId1770038768784 } from './1770038768784-application_user_topic_id.js' +import { SwapTimestamps1771347307798 } from './1771347307798-swap_timestamps.js' +import { TxSwapTimestamps1771878683383 } from './1771878683383-tx_swap_timestamps.js' + +import { LndMetrics1703170330183 } from './1703170330183-lnd_metrics.js' +import { ChannelRouting1709316653538 } from './1709316653538-channel_routing.js' +import { HtlcCount1724266887195 } from './1724266887195-htlc_count.js' +import { BalanceEvents1724860966825 } from './1724860966825-balance_events.js' +import { RootOps1732566440447 } from './1732566440447-root_ops.js' +import { RootOpsTime1745428134124 } from './1745428134124-root_ops_time.js' +import { ChannelEvents1750777346411 } from './1750777346411-channel_events.js' + +import { RootOpPending1771524665409 } from './1771524665409-root_op_pending.js' + + export const allMigrations = [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189, @@ -39,9 +49,13 @@ export const allMigrations = [Initial1703170309875, LspOrder1718387847693, Liqui DebitToPub1727105758354, UserCbUrl1727112281043, UserOffer1733502626042, ManagementGrant1751307732346, ManagementGrantBanned1751989251513, InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611, AppUserDevice1753285173175, UserAccess1759426050669, AddBlindToUserOffer1760000000000, ApplicationAvatarUrl1761000001000, AdminSettings1761683639419, TxSwap1762890527098, - TxSwapAddress1764779178945, ClinkRequester1765497600000, TrackedProviderHeight1766504040000, SwapsServiceUrl1768413055036] + TxSwapAddress1764779178945, ClinkRequester1765497600000, TrackedProviderHeight1766504040000, SwapsServiceUrl1768413055036, + InvoiceSwaps1769529793283, InvoiceSwapsFixes1769805357459, ApplicationUserTopicId1770038768784, SwapTimestamps1771347307798, + TxSwapTimestamps1771878683383] -export const allMetricsMigrations = [LndMetrics1703170330183, ChannelRouting1709316653538, HtlcCount1724266887195, BalanceEvents1724860966825, RootOps1732566440447, RootOpsTime1745428134124, ChannelEvents1750777346411] + +export const allMetricsMigrations = [LndMetrics1703170330183, ChannelRouting1709316653538, HtlcCount1724266887195, BalanceEvents1724860966825, + RootOps1732566440447, RootOpsTime1745428134124, ChannelEvents1750777346411, RootOpPending1771524665409] /* export const TypeOrmMigrationRunner = async (log: PubLogger, storageManager: Storage, settings: DbSettings, arg: string | undefined): Promise => { await connectAndMigrate(log, storageManager, allMigrations, allMetricsMigrations) return false diff --git a/src/services/storage/paymentStorage.ts b/src/services/storage/paymentStorage.ts index ed3f84dd..af96b6c5 100644 --- a/src/services/storage/paymentStorage.ts +++ b/src/services/storage/paymentStorage.ts @@ -15,6 +15,7 @@ import TransactionsQueue from "./db/transactionsQueue.js"; import { LoggedEvent } from './eventsLog.js'; import { StorageInterface } from './db/storageInterface.js'; import { TransactionSwap } from './entity/TransactionSwap.js'; +import { InvoiceSwap } from './entity/InvoiceSwap.js'; export type InboundOptionals = { product?: Product, callbackUrl?: string, expiry: number, expectedPayer?: User, linkedApplication?: Application, zapInfo?: ZapInfo, offerId?: string, payerData?: Record, rejectUnauthorized?: boolean, token?: string, blind?: boolean, clinkRequesterPub?: string, clinkRequesterEventId?: string } export const defaultInvoiceExpiry = 60 * 60 export default class { @@ -137,7 +138,15 @@ export default class { } async RemoveUserInvoices(userId: string, txId?: string) { - return this.dbs.Delete('UserReceivingInvoice', { user: { user_id: userId } }, txId) + const invoices = await this.dbs.Find('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', invoice.serial_id, txId) + } + return deleted } async GetAddressOwner(address: string, txId?: string): Promise { @@ -151,6 +160,10 @@ export default class { return this.dbs.FindOne('UserTransactionPayment', { where: { address, tx_hash: txHash } }, txId) } + async GetTxHashPaymentOwner(txHash: string, txId?: string): Promise { + return this.dbs.FindOne('UserTransactionPayment', { where: { tx_hash: txHash } }, txId) + } + async GetInvoiceOwner(paymentRequest: string, txId?: string): Promise { return this.dbs.FindOne('UserReceivingInvoice', { where: { invoice: paymentRequest } }, txId) } @@ -317,7 +330,51 @@ export default class { } async RemoveUserEphemeralKeys(userId: string, txId?: string) { - return this.dbs.Delete('UserEphemeralKey', { user: { user_id: userId } }, txId) + const keys = await this.dbs.Find('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', key.serial_id, txId) + } + return deleted + } + + async RemoveUserReceivingAddresses(userId: string, txId?: string) { + const addresses = await this.dbs.Find('UserReceivingAddress', { where: { user: { user_id: userId } } }, txId) + for (const addr of addresses) { + const txs = await this.dbs.Find('AddressReceivingTransaction', { where: { user_address: { serial_id: addr.serial_id } } }, txId) + for (const tx of txs) { + await this.dbs.Delete('AddressReceivingTransaction', tx.serial_id, txId) + } + await this.dbs.Delete('UserReceivingAddress', addr.serial_id, txId) + } + } + + async RemoveUserInvoicePayments(userId: string, txId?: string) { + const payments = await this.dbs.Find('UserInvoicePayment', { where: { user: { user_id: userId } } }, txId) + for (const p of payments) { + await this.dbs.Delete('UserInvoicePayment', p.serial_id, txId) + } + } + + async RemoveUserTransactionPayments(userId: string, txId?: string) { + const payments = await this.dbs.Find('UserTransactionPayment', { where: { user: { user_id: userId } } }, txId) + for (const p of payments) { + await this.dbs.Delete('UserTransactionPayment', p.serial_id, txId) + } + } + + async RemoveUserToUserPayments(userId: string, txId?: string) { + const asSender = await this.dbs.Find('UserToUserPayment', { where: { from_user: { user_id: userId } } }, txId) + const asReceiver = await this.dbs.Find('UserToUserPayment', { where: { to_user: { user_id: userId } } }, txId) + const seen = new Set() + for (const p of [...asSender, ...asReceiver]) { + if (seen.has(p.serial_id)) continue + seen.add(p.serial_id) + await this.dbs.Delete('UserToUserPayment', p.serial_id, txId) + } } async AddPendingUserToUserPayment(fromUserId: string, toUserId: string, amount: number, fee: number, linkedApplication: Application, txId: string) { @@ -447,8 +504,12 @@ export default class { } } - async GetTotalUsersBalance(txId?: string) { - const total = await this.dbs.Sum('User', "balance_sats", {}) + async GetTotalUsersBalance(excludeLocked?: boolean, txId?: string) { + const where: { locked?: boolean } = {} + if (excludeLocked) { + where.locked = false + } + const total = await this.dbs.Sum('User', "balance_sats", where, txId) return total || 0 } @@ -472,20 +533,31 @@ export default class { return this.dbs.FindOne('TransactionSwap', { where: { swap_operation_id: swapOperationId, used: false, app_user_id: appUserId } }, txId) } - async FinalizeTransactionSwap(swapOperationId: string, address: string, txId: string) { + async SetTransactionSwapPaid(swapOperationId: string, txId?: string) { + const now = Math.floor(Date.now() / 1000) return this.dbs.Update('TransactionSwap', { swap_operation_id: swapOperationId }, { - used: true, - tx_id: txId, - address_paid: address, - }) + paid_at_unix: now, + }, txId) } - async FailTransactionSwap(swapOperationId: string, address: string, failureReason: string) { + async FinalizeTransactionSwap(swapOperationId: string, address: string, chainTxId: string, txId?: string) { + const now = Math.floor(Date.now() / 1000) + return this.dbs.Update('TransactionSwap', { swap_operation_id: swapOperationId }, { + used: true, + tx_id: chainTxId, + address_paid: address, + completed_at_unix: now, + }, txId) + } + + async FailTransactionSwap(swapOperationId: string, address: string, failureReason: string, txId?: string) { + const now = Math.floor(Date.now() / 1000) return this.dbs.Update('TransactionSwap', { swap_operation_id: swapOperationId }, { used: true, failure_reason: failureReason, address_paid: address, - }) + completed_at_unix: now, + }, txId) } async DeleteTransactionSwap(swapOperationId: string, txId?: string) { @@ -493,18 +565,18 @@ export default class { } async DeleteExpiredTransactionSwaps(currentHeight: number, txId?: string) { - return this.dbs.Delete('TransactionSwap', { timeout_block_height: LessThan(currentHeight) }, txId) + return this.dbs.Delete('TransactionSwap', { timeout_block_height: LessThan(currentHeight), used: false }, txId) } async ListPendingTransactionSwaps(appUserId: string, txId?: string) { return this.dbs.Find('TransactionSwap', { where: { used: false, app_user_id: appUserId } }, txId) } - async ListSwapPayments(userId: string, txId?: string) { + async ListTxSwapPayments(userId: string, txId?: string) { return this.dbs.Find('UserInvoicePayment', { where: { swap_operation_id: Not(IsNull()), user: { user_id: userId } } }, txId) } - async ListCompletedSwaps(appUserId: string, payments: UserInvoicePayment[], txId?: string) { + async ListCompletedTxSwaps(appUserId: string, payments: UserInvoicePayment[], txId?: string) { const completed = await this.dbs.Find('TransactionSwap', { where: { used: true, app_user_id: appUserId } }, txId) // const payments = await this.dbs.Find('UserInvoicePayment', { where: { swap_operation_id: Not(IsNull()), } }, txId) const paymentsMap = new Map() @@ -515,6 +587,85 @@ export default class { swap: c, payment: paymentsMap.get(c.swap_operation_id) })) } + + async AddInvoiceSwap(swap: Partial) { + return this.dbs.CreateAndSave('InvoiceSwap', swap) + } + + async GetInvoiceSwap(swapOperationId: string, appUserId: string, txId?: string) { + const swap = await this.dbs.FindOne('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', { swap_operation_id: swapOperationId }, { + used: true, + completed_at_unix: now, + }, txId) + } + + async UpdateInvoiceSwap(swapOperationId: string, update: Partial, txId?: string) { + return this.dbs.Update('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 = { + tx_id: chainTxId, + paid_at_unix: now, + chain_fee_sats: chainFeeSats, + } + if (lockupTxHex) { + update.lockup_tx_hex = lockupTxHex + } + return this.dbs.Update('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', { 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', { swap_operation_id: swapOperationId }, txId) + } + + async DeleteExpiredInvoiceSwaps(currentHeight: number, txId?: string) { + return this.dbs.Delete('InvoiceSwap', { timeout_block_height: LessThan(currentHeight), used: false, tx_id: "" }, txId) + } + + async ListCompletedInvoiceSwaps(appUserId: string, txId?: string) { + return this.dbs.Find('InvoiceSwap', { where: { used: true, app_user_id: appUserId } }, txId) + } + + async ListPendingInvoiceSwaps(appUserId: string, txId?: string) { + return this.dbs.Find('InvoiceSwap', { where: { used: false, app_user_id: appUserId } }, txId) + } + + async ListUnfinishedInvoiceSwaps(txId?: string) { + const swaps = await this.dbs.Find('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', { 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 (resultPromise: Promise) => { diff --git a/src/services/storage/productStorage.ts b/src/services/storage/productStorage.ts index e6f2f662..75f3da0d 100644 --- a/src/services/storage/productStorage.ts +++ b/src/services/storage/productStorage.ts @@ -21,6 +21,14 @@ export default class { } async RemoveUserProducts(userId: string, txId?: string) { - return this.dbs.Delete('Product', { owner: { user_id: userId } }, txId) + const products = await this.dbs.Find('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_id: product.product_id }, txId) + } + return deleted } } \ No newline at end of file diff --git a/src/services/storage/tlv/tlvFilesStorageFactory.ts b/src/services/storage/tlv/tlvFilesStorageFactory.ts index 0e396703..3eb2d117 100644 --- a/src/services/storage/tlv/tlvFilesStorageFactory.ts +++ b/src/services/storage/tlv/tlvFilesStorageFactory.ts @@ -53,13 +53,13 @@ export class TlvStorageFactory extends EventEmitter { this.isConnected = false; }); - this.process.on('exit', (code: number) => { - this.log(ERROR, `Tlv Storage processor exited with code ${code}`); + this.process.on('exit', (code: number, signal: string) => { + this.log(ERROR, `Tlv Storage processor exited with code ${code} and signal ${signal}`); this.isConnected = false; - if (!code) { + if (code === 0) { return } - throw new Error(`Tlv Storage processor exited with code ${code}`) + throw new Error(`Tlv Storage processor exited with code ${code} and signal ${signal}`) }); this.isConnected = true; @@ -173,7 +173,7 @@ export class TlvStorageFactory extends EventEmitter { public disconnect() { if (this.process) { - this.process.kill(); + this.process.kill(0); this.isConnected = false; this.debug = false; } diff --git a/src/services/storage/tlv/tlvFilesStorageProcessor.ts b/src/services/storage/tlv/tlvFilesStorageProcessor.ts index 2b4189e4..caccb949 100644 --- a/src/services/storage/tlv/tlvFilesStorageProcessor.ts +++ b/src/services/storage/tlv/tlvFilesStorageProcessor.ts @@ -126,7 +126,7 @@ class TlvFilesStorageProcessor { throw new Error('Unknown metric type: ' + t) } }) - this.wrtc.attachNostrSend((initiator: SendInitiator, data: SendData, relays?: string[] | undefined) => { + this.wrtc.attachNostrSend(async (initiator: SendInitiator, data: SendData, relays?: string[] | undefined) => { this.sendResponse({ success: true, type: 'nostrSend', diff --git a/src/services/storage/userStorage.ts b/src/services/storage/userStorage.ts index 58dd6cf5..a1541bac 100644 --- a/src/services/storage/userStorage.ts +++ b/src/services/storage/userStorage.ts @@ -42,7 +42,7 @@ export default class { async GetUser(userId: string, txId?: string): Promise { const user = await this.FindUser(userId, txId) if (!user) { - throw new Error(`user ${userId} not found`) // TODO: fix logs doxing + throw new Error(`user not found`) } return user } @@ -50,7 +50,7 @@ export default class { async UnbanUser(userId: string, txId?: string) { const affected = await this.dbs.Update('User', { user_id: userId }, { locked: false }, txId) if (!affected) { - throw new Error("unaffected user unlock for " + userId) // TODO: fix logs doxing + throw new Error("unaffected user unlock") } } @@ -58,7 +58,7 @@ export default class { const user = await this.GetUser(userId, txId) const affected = await this.dbs.Update('User', { user_id: userId }, { balance_sats: 0, locked: true }, txId) if (!affected) { - throw new Error("unaffected ban user for " + userId) // TODO: fix logs doxing + throw new Error("unaffected ban user") } if (user.balance_sats > 0) { this.eventsLog.LogEvent({ type: 'balance_decrement', userId, appId: "", appUserId: "", balance: user.balance_sats, data: 'ban', amount: user.balance_sats }) @@ -80,7 +80,7 @@ export default class { const affected = await this.dbs.Increment('User', { user_id: userId }, "balance_sats", increment, txId) if (!affected) { getLogger({ userId: userId, component: "balanceUpdates" })("user unaffected by increment") - throw new Error("unaffected balance increment for " + userId) // TODO: fix logs doxing + throw new Error("unaffected balance increment") } getLogger({ userId: userId, component: "balanceUpdates" })("incremented balance from", user.balance_sats, "sats, by", increment, "sats") this.eventsLog.LogEvent({ type: 'balance_increment', userId, appId: "", appUserId: "", balance: user.balance_sats, data: reason, amount: increment }) @@ -105,7 +105,7 @@ export default class { const affected = await this.dbs.Decrement('User', { user_id: userId }, "balance_sats", decrement, txId) if (!affected) { getLogger({ userId: userId, component: "balanceUpdates" })("user unaffected by decrement") - throw new Error("unaffected balance decrement for " + userId) // TODO: fix logs doxing + throw new Error("unaffected balance decrement") } getLogger({ userId: userId, component: "balanceUpdates" })("decremented balance from", user.balance_sats, "sats, by", decrement, "sats") this.eventsLog.LogEvent({ type: 'balance_decrement', userId, appId: "", appUserId: "", balance: user.balance_sats, data: reason, amount: decrement }) @@ -126,4 +126,8 @@ export default class { const lastSeenAtUnix = now - seconds return this.dbs.Find('UserAccess', { where: { last_seen_at_unix: LessThan(lastSeenAtUnix) } }) } -} \ No newline at end of file + + async DeleteUserAccess(userId: string, txId?: string) { + return this.dbs.Delete('UserAccess', { user_id: userId }, txId) + } +} diff --git a/src/services/webRTC/index.ts b/src/services/webRTC/index.ts index 8ee8d884..a2cc90af 100644 --- a/src/services/webRTC/index.ts +++ b/src/services/webRTC/index.ts @@ -27,11 +27,11 @@ export default class webRTC { attachNostrSend(f: NostrSend) { this._nostrSend = f } - private nostrSend: NostrSend = (initiator: SendInitiator, data: SendData, relays?: string[] | undefined) => { + private nostrSend: NostrSend = async (initiator: SendInitiator, data: SendData, relays?: string[] | undefined) => { if (!this._nostrSend) { throw new Error("No nostrSend attached") } - this._nostrSend(initiator, data, relays) + await this._nostrSend(initiator, data, relays) } private sendCandidate = (u: WebRtcUserInfo, candidate: string) => {