diff --git a/.dockerignore b/.dockerignore index 7fab354e..135641f4 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,3 +2,21 @@ .github build node_modules + +# Runtime state files (should not be baked into image) +*.sqlite +*.sqlite-journal +*.sqlite-wal +*.sqlite-shm +*.db +admin.connect +admin.enroll +admin.npub +app.nprofile +.jwt_secret + +# Runtime data directories +metric_cache/ +metric_events/ +bundler_events/ +logs/ diff --git a/Dockerfile b/Dockerfile index 6001419a..1d3df65b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:18 +FROM node:20 WORKDIR /app 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/package-lock.json b/package-lock.json index 701b7072..47556424 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,6 +59,7 @@ "zip-a-folder": "^3.1.9" }, "devDependencies": { + "@types/better-sqlite3": "^7.6.13", "@types/chai": "^4.3.4", "@types/chai-string": "^1.4.5", "@types/cors": "^2.8.17", @@ -82,19 +83,19 @@ "license": "MIT" }, "node_modules/@bufbuild/protobuf": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.7.0.tgz", - "integrity": "sha512-qn6tAIZEw5i/wiESBF4nQxZkl86aY4KoO0IkUa2Lh+rya64oTOdJQFlZuMwI1Qz9VBJQrQC4QlSA2DNek5gCOA==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.11.0.tgz", + "integrity": "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==", "license": "(Apache-2.0 AND BSD-3-Clause)" }, "node_modules/@bufbuild/protoplugin": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@bufbuild/protoplugin/-/protoplugin-2.7.0.tgz", - "integrity": "sha512-yUdg8hXzFGR6K8ren7aXly2hT9BxClId814VB142YeZPatY0wqD3c0D8KfIz5nIeMdoPt0/Pm/RycFJCNGMD6w==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protoplugin/-/protoplugin-2.11.0.tgz", + "integrity": "sha512-lyZVNFUHArIOt4W0+dwYBe5GBwbKzbOy8ObaloEqsw9Mmiwv2O48TwddDoHN4itylC+BaEGqFdI1W8WQt2vWJQ==", "license": "Apache-2.0", "dependencies": { - "@bufbuild/protobuf": "2.7.0", - "@typescript/vfs": "^1.5.2", + "@bufbuild/protobuf": "2.11.0", + "@typescript/vfs": "^1.6.2", "typescript": "5.4.5" } }, @@ -134,21 +135,13 @@ "node": ">=12" } }, - "node_modules/@gar/promisify": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", - "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/@grpc/grpc-js": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.4.tgz", - "integrity": "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==", + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", + "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", "license": "Apache-2.0", "dependencies": { - "@grpc/proto-loader": "^0.7.13", + "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" }, "engines": { @@ -156,14 +149,14 @@ } }, "node_modules/@grpc/proto-loader": { - "version": "0.7.15", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", - "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", + "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", "license": "Apache-2.0", "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", - "protobufjs": "^7.2.5", + "protobufjs": "^7.5.3", "yargs": "^17.7.2" }, "bin": { @@ -173,121 +166,25 @@ "node": ">=6" } }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", - "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", - "license": "MIT", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", "license": "ISC", "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + "minipass": "^7.0.4" }, "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz", - "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "license": "MIT" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": ">=18.0.0" } }, "node_modules/@js-sdsl/ordered-map": { @@ -301,29 +198,30 @@ } }, "node_modules/@mapbox/node-pre-gyp": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", - "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-2.0.3.tgz", + "integrity": "sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg==", "license": "BSD-3-Clause", "dependencies": { + "consola": "^3.2.3", "detect-libc": "^2.0.0", - "https-proxy-agent": "^5.0.0", - "make-dir": "^3.1.0", + "https-proxy-agent": "^7.0.5", "node-fetch": "^2.6.7", - "nopt": "^5.0.0", - "npmlog": "^5.0.1", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.11" + "nopt": "^8.0.0", + "semver": "^7.5.3", + "tar": "^7.4.0" }, "bin": { "node-pre-gyp": "bin/node-pre-gyp" + }, + "engines": { + "node": ">=18" } }, "node_modules/@mapbox/node-pre-gyp/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -333,33 +231,39 @@ } }, "node_modules/@noble/ciphers": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.5.3.tgz", - "integrity": "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.1.1.tgz", + "integrity": "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==", "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@noble/curves": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", - "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.0.1.tgz", + "integrity": "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==", "license": "MIT", "dependencies": { - "@noble/hashes": "1.3.2" + "@noble/hashes": "2.0.1" + }, + "engines": { + "node": ">= 20.19.0" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@noble/curves/node_modules/@noble/hashes": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", - "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", "license": "MIT", "engines": { - "node": ">= 16" + "node": ">= 20.19.0" }, "funding": { "url": "https://paulmillr.com/funding/" @@ -412,48 +316,6 @@ "node": ">= 8" } }, - "node_modules/@npmcli/fs": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", - "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", - "license": "ISC", - "optional": true, - "peer": true, - "dependencies": { - "@gar/promisify": "^1.0.1", - "semver": "^7.3.5" - } - }, - "node_modules/@npmcli/fs/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@npmcli/move-file": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", - "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", - "deprecated": "This functionality has been moved to @npmcli/fs", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "mkdirp": "^1.0.4", - "rimraf": "^3.0.2" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@openzeppelin/contracts": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-5.4.0.tgz", @@ -612,6 +474,121 @@ } }, "node_modules/@scure/bip32": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-2.0.1.tgz", + "integrity": "sha512-4Md1NI5BzoVP+bhyJaY3K6yMesEFzNS1sE/cP+9nuvE7p/b0kx9XbpDHHFl8dHtufcbdHRUUQdRqLIPHN/s7yA==", + "license": "MIT", + "dependencies": { + "@noble/curves": "2.0.1", + "@noble/hashes": "2.0.1", + "@scure/base": "2.0.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/hashes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@scure/base": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.0.0.tgz", + "integrity": "sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-2.0.1.tgz", + "integrity": "sha512-PsxdFj/d2AcJcZDX1FXN3dDgitDDTmwf78rKZq1a6c1P1Nan1X/Sxc7667zU3U+AN60g7SxxP0YCVw2H/hBycg==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.0.1", + "@scure/base": "2.0.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39/node_modules/@noble/hashes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39/node_modules/@scure/base": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.0.0.tgz", + "integrity": "sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@shocknet/clink-sdk": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@shocknet/clink-sdk/-/clink-sdk-1.5.4.tgz", + "integrity": "sha512-YrKR7oFjzUmBhm4p8pT6mP3YC7UdWbIlh5PMXHSwSLfJhL8Ddx91NLIfOHUTcQa1cDf27uvillMFXYrVUMCRuQ==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.8.0", + "@scure/base": "^1.2.5", + "nostr-tools": "2.15.1", + "rimraf": "^6.0.1", + "typescript": "^5.8.3" + } + }, + "node_modules/@shocknet/clink-sdk/node_modules/@noble/ciphers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.5.3.tgz", + "integrity": "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@shocknet/clink-sdk/node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@shocknet/clink-sdk/node_modules/@noble/curves/node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@shocknet/clink-sdk/node_modules/@scure/bip32": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.1.tgz", "integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==", @@ -625,7 +602,7 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@scure/bip32/node_modules/@noble/curves": { + "node_modules/@shocknet/clink-sdk/node_modules/@scure/bip32/node_modules/@noble/curves": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz", "integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==", @@ -637,7 +614,7 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@scure/bip32/node_modules/@noble/curves/node_modules/@noble/hashes": { + "node_modules/@shocknet/clink-sdk/node_modules/@scure/bip32/node_modules/@noble/curves/node_modules/@noble/hashes": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", @@ -649,7 +626,7 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@scure/bip32/node_modules/@noble/hashes": { + "node_modules/@shocknet/clink-sdk/node_modules/@scure/bip32/node_modules/@noble/hashes": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", @@ -661,7 +638,7 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@scure/bip32/node_modules/@scure/base": { + "node_modules/@shocknet/clink-sdk/node_modules/@scure/bip32/node_modules/@scure/base": { "version": "1.1.9", "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", @@ -670,7 +647,7 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@scure/bip39": { + "node_modules/@shocknet/clink-sdk/node_modules/@scure/bip39": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz", "integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==", @@ -683,7 +660,7 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@scure/bip39/node_modules/@noble/hashes": { + "node_modules/@shocknet/clink-sdk/node_modules/@scure/bip39/node_modules/@noble/hashes": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", @@ -695,7 +672,7 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@scure/bip39/node_modules/@scure/base": { + "node_modules/@shocknet/clink-sdk/node_modules/@scure/bip39/node_modules/@scure/base": { "version": "1.1.9", "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", @@ -704,35 +681,40 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@shocknet/clink-sdk": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@shocknet/clink-sdk/-/clink-sdk-1.4.0.tgz", - "integrity": "sha512-J0PWE8CVRJrFF1Zi/UhChhvOrlmDj7LRJTpR6rbHlFPmjC5TGIW6891tVWWv+JmUR0jzez9QHFrHnc8DgIJYCQ==", + "node_modules/@shocknet/clink-sdk/node_modules/balanced-match": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.2.tgz", + "integrity": "sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==", "license": "MIT", "dependencies": { - "@noble/hashes": "^1.8.0", - "@scure/base": "^1.2.5", - "nostr-tools": "^2.13.0", - "rimraf": "^6.0.1", - "typescript": "^5.8.3" + "jackspeak": "^4.2.3" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@shocknet/clink-sdk/node_modules/brace-expansion": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz", + "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" } }, "node_modules/@shocknet/clink-sdk/node_modules/glob": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", - "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", - "license": "ISC", + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.3.tgz", + "integrity": "sha512-/g3B0mC+4x724v1TgtBlBtt2hPi/EWptsIAmXUx9Z2rvBYleQcsrmaOzd5LyL50jf/Soi83ZDJmw2+XqvH/EeA==", + "license": "BlueOak-1.0.0", "dependencies": { - "foreground-child": "^3.3.1", - "jackspeak": "^4.1.1", - "minimatch": "^10.0.3", + "minimatch": "^10.2.0", "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, "engines": { "node": "20 || >=22" }, @@ -741,12 +723,12 @@ } }, "node_modules/@shocknet/clink-sdk/node_modules/minimatch": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", - "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", - "license": "ISC", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.0.tgz", + "integrity": "sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w==", + "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" + "brace-expansion": "^5.0.2" }, "engines": { "node": "20 || >=22" @@ -755,23 +737,61 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@shocknet/clink-sdk/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" + "node_modules/@shocknet/clink-sdk/node_modules/nostr-tools": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.15.1.tgz", + "integrity": "sha512-LpetHDR9ltnkpJDkva/SONgyKBbsoV+5yLB8DWc0/U3lCWGtoWJw6Nbc2vR2Ai67RIQYrBQeZLyMlhwVZRK/9A==", + "license": "Unlicense", + "dependencies": { + "@noble/ciphers": "^0.5.1", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.1", + "@scure/base": "1.1.1", + "@scure/bip32": "1.3.1", + "@scure/bip39": "1.2.1", + "nostr-wasm": "0.1.0" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, + "node_modules/@shocknet/clink-sdk/node_modules/nostr-tools/node_modules/@noble/hashes": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", + "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@shocknet/clink-sdk/node_modules/nostr-tools/node_modules/@scure/base": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", + "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT" + }, "node_modules/@shocknet/clink-sdk/node_modules/rimraf": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", - "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", - "license": "ISC", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.2.tgz", + "integrity": "sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g==", + "license": "BlueOak-1.0.0", "dependencies": { - "glob": "^11.0.0", - "package-json-from-dist": "^1.0.0" + "glob": "^13.0.0", + "package-json-from-dist": "^1.0.1" }, "bin": { "rimraf": "dist/esm/bin.mjs" @@ -784,9 +804,9 @@ } }, "node_modules/@shocknet/clink-sdk/node_modules/typescript": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", - "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -844,21 +864,10 @@ "@stablelib/wipe": "^1.0.1" } }, - "node_modules/@tootallnate/once": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 6" - } - }, "node_modules/@tsconfig/node10": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", - "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", "devOptional": true, "license": "MIT" }, @@ -883,6 +892,16 @@ "devOptional": true, "license": "MIT" }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/body-parser": { "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", @@ -948,21 +967,21 @@ "license": "MIT" }, "node_modules/@types/express": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz", - "integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==", + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", "license": "MIT", "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", - "@types/serve-static": "*" + "@types/serve-static": "^1" } }, "node_modules/@types/express-serve-static-core": { - "version": "4.19.6", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", - "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", "license": "MIT", "dependencies": { "@types/node": "*", @@ -989,9 +1008,9 @@ } }, "node_modules/@types/lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==", "dev": true, "license": "MIT" }, @@ -1048,33 +1067,42 @@ "license": "MIT" }, "node_modules/@types/secp256k1": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@types/secp256k1/-/secp256k1-4.0.6.tgz", - "integrity": "sha512-hHxJU6PAEUn0TP4S/ZOzuTUvJWuZ6eIKeNKb5RBpODvSl6hp1Wrw4s7ATY50rklRCScUDpHzVA/DQdSjJ3UoYQ==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@types/secp256k1/-/secp256k1-4.0.7.tgz", + "integrity": "sha512-Rcvjl6vARGAKRO6jHeKMatGrvOMGrR/AR11N1x2LqintPCyDZ7NBhrh238Z2VZc7aM7KIwnFpFQ7fnfK4H/9Qw==", "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/send": { - "version": "0.17.5", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", - "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", "license": "MIT", "dependencies": { - "@types/mime": "^1", "@types/node": "*" } }, "node_modules/@types/serve-static": { - "version": "1.15.8", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", - "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", "license": "MIT", "dependencies": { "@types/http-errors": "*", "@types/node": "*", - "@types/send": "*" + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" } }, "node_modules/@types/uuid": { @@ -1105,9 +1133,9 @@ } }, "node_modules/@typescript/vfs": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@typescript/vfs/-/vfs-1.6.1.tgz", - "integrity": "sha512-JwoxboBh7Oz1v38tPbkrZ62ZXNHAk9bJ7c9x0eI5zBfBnBYGhURdbnh7Z4smN/MV48Y5OCcZb58n972UtbazsA==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@typescript/vfs/-/vfs-1.6.2.tgz", + "integrity": "sha512-hoBwJwcbKHmvd2QVebiytN1aELvpk9B74B4L1mFm/XT1Q/VOYAWl2vQ9AWRFtQq8zmz6enTpfTV8WRc4ATjW/g==", "license": "MIT", "dependencies": { "debug": "^4.1.1" @@ -1154,10 +1182,13 @@ } }, "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "license": "ISC" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", + "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } }, "node_modules/abort-controller": { "version": "3.0.0", @@ -1223,44 +1254,12 @@ } }, "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "license": "MIT", - "dependencies": { - "debug": "4" - }, "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/agentkeepalive": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", - "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "humanize-ms": "^1.2.1" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" + "node": ">= 14" } }, "node_modules/ajv": { @@ -1304,9 +1303,9 @@ } }, "node_modules/ansis": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/ansis/-/ansis-3.17.0.tgz", - "integrity": "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", + "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", "license": "ISC", "engines": { "node": ">=14" @@ -1335,12 +1334,6 @@ "node": ">= 6.0.0" } }, - "node_modules/aproba": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", - "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", - "license": "ISC" - }, "node_modules/archiver": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", @@ -1377,6 +1370,47 @@ "node": ">= 14" } }, + "node_modules/archiver-utils/node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/archiver-utils/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/archiver-utils/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/archiver-utils/node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -1410,10 +1444,17 @@ "ieee754": "^1.2.1" } }, + "node_modules/archiver-utils/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, "node_modules/archiver-utils/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", @@ -1466,15 +1507,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/archiver-utils/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/archiver-utils/node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", @@ -1516,6 +1548,55 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/archiver-utils/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/archiver-utils/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/archiver-utils/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/archiver/node_modules/buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -1576,43 +1657,6 @@ "streamx": "^2.15.0" } }, - "node_modules/are-we-there-yet": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", - "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/are-we-there-yet/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/are-we-there-yet/node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -1696,21 +1740,29 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", - "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "node_modules/b4a": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", - "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", - "license": "Apache-2.0" + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.4.tgz", + "integrity": "sha512-u20zJLDaSWpxaZ+zaAkEIB2dZZ1o+DF4T/MRbmsvGp9nletHOyiai19OzX1fF8xUBYsO1bPXxODvcd0978pnug==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } }, "node_modules/balanced-match": { "version": "1.0.2", @@ -1719,16 +1771,23 @@ "license": "MIT" }, "node_modules/bare-events": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.6.1.tgz", - "integrity": "sha512-AuTJkq9XmE6Vk0FJVNq5QxETrSA/vKHarWVBG5l/JbdCL1prJemiyJqUS0jrlXO0MftuPq4m3YVYhoNc5+aE/g==", + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", "license": "Apache-2.0", - "optional": true + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } }, "node_modules/base-x": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz", - "integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.1.tgz", + "integrity": "sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw==", "license": "MIT" }, "node_modules/base64-js": { @@ -1767,9 +1826,9 @@ "license": "MIT" }, "node_modules/better-sqlite3": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.2.0.tgz", - "integrity": "sha512-eGbYq2CT+tos1fBwLQ/tkBt9J5M3JEHjku4hbvQUePCckkvVf14xWj+1m7dGoK81M/fOjFT7yM9UMeKT/+vFLQ==", + "version": "12.6.2", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.6.2.tgz", + "integrity": "sha512-8VYKM3MjCa9WcaSAI3hzwhmyHVlH8tiGFwf0RlTsZPWJ1I5MkzjiudCo4KC4DxOaL/53A5B1sI/IbldNFDbsKA==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -1777,7 +1836,7 @@ "prebuild-install": "^7.1.1" }, "engines": { - "node": "20.x || 22.x || 23.x || 24.x" + "node": "20.x || 22.x || 23.x || 24.x || 25.x" } }, "node_modules/bignumber.js": { @@ -1855,13 +1914,10 @@ } }, "node_modules/bip66": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz", - "integrity": "sha512-nemMHz95EmS38a26XbbdxIYj5csHd3RMP3H5bwQknX0WYHF01qhpufP42mLOwVICuH2JmhIhXiWs89MfUGL7Xw==", - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.0.1" - } + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bip66/-/bip66-2.0.0.tgz", + "integrity": "sha512-kBG+hSpgvZBrkIm9dt5T1Hd/7xGCPEX2npoxAWZfsK1FvjgaxySEh2WizjyIstWXriKo9K9uJ4u0OnsyLDUPXQ==", + "license": "MIT" }, "node_modules/bitcoin-core": { "version": "4.2.0", @@ -1898,40 +1954,6 @@ "node": ">=8.0.0" } }, - "node_modules/bitcoinjs-lib/node_modules/base-x": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.1.tgz", - "integrity": "sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw==", - "license": "MIT" - }, - "node_modules/bitcoinjs-lib/node_modules/bs58": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz", - "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==", - "license": "MIT", - "dependencies": { - "base-x": "^4.0.0" - } - }, - "node_modules/bitcoinjs-lib/node_modules/bs58check": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-3.0.1.tgz", - "integrity": "sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "^1.2.0", - "bs58": "^5.0.0" - } - }, - "node_modules/bitcoinjs-lib/node_modules/varuint-bitcoin": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/varuint-bitcoin/-/varuint-bitcoin-1.1.2.tgz", - "integrity": "sha512-4EVb+w4rx+YfVM32HQX42AbbT7/1f5zwAYhIujKXKk8NQK+JfRVl3pqT3hjNn/L+RstigmGGKVwHA/P0wgITZw==", - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.1" - } - }, "node_modules/bitset": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/bitset/-/bitset-5.2.3.tgz", @@ -1971,15 +1993,6 @@ "node": ">= 6" } }, - "node_modules/bl/node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, "node_modules/blech32": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/blech32/-/blech32-1.1.2.tgz", @@ -2001,29 +2014,29 @@ "peer": true }, "node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz", + "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==", "license": "MIT" }, "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", + "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", "type-is": "~1.6.18", - "unpipe": "1.0.0" + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8", @@ -2046,19 +2059,19 @@ "license": "MIT" }, "node_modules/boltz-core": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/boltz-core/-/boltz-core-3.0.0.tgz", - "integrity": "sha512-L0jzUnTIb/vir9k6yVlCkV8I29ZeBK7LWq3rrDcp/KP7bB0VvKR5JTceua8g2KBCBM5HOMVDEuPW3c9/82/Nmg==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/boltz-core/-/boltz-core-3.1.2.tgz", + "integrity": "sha512-FxDcOuANAvRKI/1IHKySjfBXDcXCXR3ImM9ndjYGN5km8lXGJWwYEt85KtTBNFTPSdBcNnkOPQ1nXJK6R62gCg==", "license": "AGPL-3.0", "dependencies": { "@boltz/bitcoin-ops": "^2.0.0", - "@openzeppelin/contracts": "^5.2.0", + "@openzeppelin/contracts": "^5.4.0", "@vulpemventures/secp256k1-zkp": "^3.2.1", "bip32": "^4.0.0", "bip65": "^1.0.3", "bip66": "^2.0.0", "bitcoinjs-lib": "^6.1.7", - "bn.js": "^5.2.1", + "bn.js": "^5.2.2", "ecpair": "^3.0.0", "varuint-bitcoin": "^2.0.0" }, @@ -2069,17 +2082,14 @@ "liquidjs-lib": "^6.0.2-liquid.37" } }, - "node_modules/boltz-core/node_modules/bip66": { + "node_modules/boltz-core/node_modules/varuint-bitcoin": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/bip66/-/bip66-2.0.0.tgz", - "integrity": "sha512-kBG+hSpgvZBrkIm9dt5T1Hd/7xGCPEX2npoxAWZfsK1FvjgaxySEh2WizjyIstWXriKo9K9uJ4u0OnsyLDUPXQ==", - "license": "MIT" - }, - "node_modules/boltz-core/node_modules/bn.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz", - "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==", - "license": "MIT" + "resolved": "https://registry.npmjs.org/varuint-bitcoin/-/varuint-bitcoin-2.0.0.tgz", + "integrity": "sha512-6QZbU/rHO2ZQYpWFDALCDSRsXbAs1VOEmXAxtbtjLtKuMJ/FQ8YbhfxlaiKv5nklci0M6lZtlZyxo9Q+qNnyog==", + "license": "MIT", + "dependencies": { + "uint8array-tools": "^0.0.8" + } }, "node_modules/brace-expansion": { "version": "1.1.12", @@ -2125,22 +2135,22 @@ } }, "node_modules/bs58": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", - "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz", + "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==", "license": "MIT", "dependencies": { - "base-x": "^5.0.0" + "base-x": "^4.0.0" } }, "node_modules/bs58check": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-4.0.0.tgz", - "integrity": "sha512-FsGDOnFg9aVI9erdriULkd/JjEWONV/lQE5aYziB5PoBsXRind56lh8doIZIc9X4HoxT5x4bLjMWN1/NB8Zp5g==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-3.0.1.tgz", + "integrity": "sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ==", "license": "MIT", "dependencies": { "@noble/hashes": "^1.2.0", - "bs58": "^6.0.0" + "bs58": "^5.0.0" } }, "node_modules/buffer": { @@ -2189,21 +2199,6 @@ "license": "MIT", "optional": true }, - "node_modules/bufferutil": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.9.tgz", - "integrity": "sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">=6.14.2" - } - }, "node_modules/bunyan": { "version": "1.8.15", "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.15.tgz", @@ -2231,65 +2226,6 @@ "node": ">= 0.8" } }, - "node_modules/cacache": { - "version": "15.3.0", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", - "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", - "license": "ISC", - "optional": true, - "peer": true, - "dependencies": { - "@npmcli/fs": "^1.0.0", - "@npmcli/move-file": "^1.0.1", - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "glob": "^7.1.4", - "infer-owner": "^1.0.4", - "lru-cache": "^6.0.0", - "minipass": "^3.1.1", - "minipass-collect": "^1.0.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.2", - "mkdirp": "^1.0.3", - "p-map": "^4.0.0", - "promise-inflight": "^1.0.1", - "rimraf": "^3.0.2", - "ssri": "^8.0.1", - "tar": "^6.0.2", - "unique-filename": "^1.1.1" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/cacache/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "license": "ISC", - "optional": true, - "peer": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/cacache/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "optional": true, - "peer": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -2420,38 +2356,28 @@ } }, "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "license": "ISC", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/cipher-base": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.6.tgz", - "integrity": "sha512-3Ek9H3X6pj5TgenXYtNWdaBon1tgYCaebd+XPg0keyjEbEfkD4KkmAxkQ/i1vYvxdcT5nscLBfq9VJRmCBcFSw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz", + "integrity": "sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==", "license": "MIT", "dependencies": { "inherits": "^2.0.4", - "safe-buffer": "^5.2.1" + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.2" }, "engines": { "node": ">= 0.10" } }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=6" - } - }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -2484,15 +2410,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, - "node_modules/color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "license": "ISC", - "bin": { - "color-support": "bin.js" - } - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -2576,11 +2493,14 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "license": "MIT" }, - "node_modules/console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "license": "ISC" + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } }, "node_modules/content-disposition": { "version": "0.5.4", @@ -2604,18 +2524,18 @@ } }, "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", "license": "MIT" }, "node_modules/copyfiles": { @@ -2682,9 +2602,9 @@ "license": "MIT" }, "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", "license": "MIT", "dependencies": { "object-assign": "^4", @@ -2692,6 +2612,10 @@ }, "engines": { "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/crc-32": { @@ -2862,15 +2786,15 @@ } }, "node_modules/dayjs": { - "version": "1.11.13", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", - "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", "license": "MIT" }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2928,9 +2852,9 @@ } }, "node_modules/dedent": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", - "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz", + "integrity": "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==", "license": "MIT", "peerDependencies": { "babel-plugin-macros": "^3.1.0" @@ -2988,12 +2912,6 @@ "node": ">=0.4.0" } }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "license": "MIT" - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -3014,18 +2932,18 @@ } }, "node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "license": "Apache-2.0", "engines": { "node": ">=8" } }, "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", "devOptional": true, "license": "BSD-3-Clause", "engines": { @@ -3170,6 +3088,23 @@ "secp256k1": "3.7.1" } }, + "node_modules/eccrypto/node_modules/bip66": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz", + "integrity": "sha512-nemMHz95EmS38a26XbbdxIYj5csHd3RMP3H5bwQknX0WYHF01qhpufP42mLOwVICuH2JmhIhXiWs89MfUGL7Xw==", + "license": "MIT", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/eccrypto/node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "license": "MIT", + "optional": true + }, "node_modules/eccrypto/node_modules/nan": { "version": "2.14.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", @@ -3207,40 +3142,42 @@ } }, "node_modules/ecpair": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ecpair/-/ecpair-3.0.0.tgz", - "integrity": "sha512-kf4JxjsRQoD4EBzpYjGAcR0t9i/4oAeRPtyCpKvSwyotgkc6oA4E4M0/e+kep7cXe+mgxAvoeh/jdgH9h5+Wxw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ecpair/-/ecpair-3.0.1.tgz", + "integrity": "sha512-uz8wMFvtdr58TLrXnAesBsoMEyY8UudLOfApcyg40XfZjP+gt1xO4cuZSIkZ8hTMTQ8+ETgt7xSIV4eM7M6VNw==", "license": "MIT", "dependencies": { "uint8array-tools": "^0.0.8", - "valibot": "^0.37.0", + "valibot": "^1.2.0", "wif": "^5.0.0" }, "engines": { "node": ">=20.0.0" } }, - "node_modules/ecpair/node_modules/uint8array-tools": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.8.tgz", - "integrity": "sha512-xS6+s8e0Xbx++5/0L+yyexukU7pz//Yg6IHg3BKhXotg1JcYtgxVcUctQ0HxLByiJzpAkNFawz1Nz5Xadzo82g==", + "node_modules/ecpair/node_modules/base-x": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz", + "integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==", + "license": "MIT" + }, + "node_modules/ecpair/node_modules/bs58": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", + "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", "license": "MIT", - "engines": { - "node": ">=14.0.0" + "dependencies": { + "base-x": "^5.0.0" } }, - "node_modules/ecpair/node_modules/valibot": { - "version": "0.37.0", - "resolved": "https://registry.npmjs.org/valibot/-/valibot-0.37.0.tgz", - "integrity": "sha512-FQz52I8RXgFgOHym3XHYSREbNtkgSjF9prvMFH1nBsRyfL6SfCzoT1GuSDTlbsuPubM7/6Kbw0ZMQb8A+V+VsQ==", + "node_modules/ecpair/node_modules/bs58check": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-4.0.0.tgz", + "integrity": "sha512-FsGDOnFg9aVI9erdriULkd/JjEWONV/lQE5aYziB5PoBsXRind56lh8doIZIc9X4HoxT5x4bLjMWN1/NB8Zp5g==", "license": "MIT", - "peerDependencies": { - "typescript": ">=5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "dependencies": { + "@noble/hashes": "^1.2.0", + "bs58": "^6.0.0" } }, "node_modules/ecpair/node_modules/wif": { @@ -3273,6 +3210,12 @@ "minimalistic-crypto-utils": "^1.0.1" } }, + "node_modules/elliptic/node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "license": "MIT" + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -3288,31 +3231,6 @@ "node": ">= 0.8" } }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, - "node_modules/encoding/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -3322,25 +3240,6 @@ "once": "^1.4.0" } }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/err-code": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -3434,6 +3333,15 @@ "node": ">=0.8.x" } }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, "node_modules/evp_bytestokey": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", @@ -3455,39 +3363,39 @@ } }, "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", + "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.13.0", + "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", + "send": "~0.19.0", + "serve-static": "~1.16.2", "setprototypeof": "1.2.0", - "statuses": "2.0.1", + "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" @@ -3565,9 +3473,9 @@ "license": "MIT" }, "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -3592,17 +3500,17 @@ } }, "node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", "license": "MIT", "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "statuses": "2.0.1", + "statuses": "~2.0.2", "unpipe": "~1.0.0" }, "engines": { @@ -3675,18 +3583,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -3697,9 +3593,9 @@ } }, "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -3736,30 +3632,6 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "license": "MIT" }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3790,27 +3662,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gauge": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", - "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.2", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.1", - "object-assign": "^4.1.1", - "signal-exit": "^3.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.2" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -3885,7 +3736,7 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -3952,12 +3803,12 @@ "license": "ISC" }, "node_modules/grpc-tools": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/grpc-tools/-/grpc-tools-1.13.0.tgz", - "integrity": "sha512-7CbkJ1yWPfX0nHjbYG58BQThNhbICXBZynzCUxCb3LzX5X9B3hQbRY2STiRgIEiLILlK9fgl0z0QVGwPCdXf5g==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/grpc-tools/-/grpc-tools-1.13.1.tgz", + "integrity": "sha512-0sttMUxThNIkCTJq5qI0xXMz5zWqV2u3yG1kR3Sj9OokGIoyRBFjoInK9NyW7x5fH7knj48Roh1gq5xbl0VoDQ==", "hasInstallScript": true, "dependencies": { - "@mapbox/node-pre-gyp": "^1.0.5" + "@mapbox/node-pre-gyp": "^2.0.0" }, "bin": { "grpc_tools_node_protoc": "bin/protoc.js", @@ -4036,47 +3887,19 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "license": "ISC" - }, "node_modules/hash-base": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", - "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.2.tgz", + "integrity": "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==", "license": "MIT", "dependencies": { "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.1" }, "engines": { - "node": ">=4" - } - }, - "node_modules/hash-base/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/hash-base/node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" + "node": ">= 0.8" } }, "node_modules/hash.js": { @@ -4112,44 +3935,24 @@ "minimalistic-crypto-utils": "^1.0.1" } }, - "node_modules/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", - "license": "BSD-2-Clause", - "optional": true, - "peer": true - }, "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" - } - }, - "node_modules/http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" }, - "engines": { - "node": ">= 6" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/http-signature": { @@ -4168,27 +3971,16 @@ } }, "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "license": "MIT", "dependencies": { - "agent-base": "6", + "agent-base": "^7.1.2", "debug": "4" }, "engines": { - "node": ">= 6" - } - }, - "node_modules/humanize-ms": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "ms": "^2.0.0" + "node": ">= 14" } }, "node_modules/iconv-lite": { @@ -4239,36 +4031,6 @@ "dev": true, "license": "ISC" }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/infer-owner": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", - "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", - "license": "ISC", - "optional": true, - "peer": true - }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -4292,17 +4054,6 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, - "node_modules/ip-address": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", - "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 12" - } - }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -4367,14 +4118,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-lambda": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", - "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -4418,9 +4161,9 @@ "license": "MIT" }, "node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "license": "MIT" }, "node_modules/isexe": { @@ -4436,12 +4179,12 @@ "license": "MIT" }, "node_modules/jackspeak": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", - "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/cliui": "^8.0.2" + "@isaacs/cliui": "^9.0.0" }, "engines": { "node": "20 || >=22" @@ -4484,12 +4227,12 @@ "license": "ISC" }, "node_modules/jsonwebtoken": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", - "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", "license": "MIT", "dependencies": { - "jws": "^3.2.2", + "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", @@ -4506,9 +4249,9 @@ } }, "node_modules/jsonwebtoken/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -4533,9 +4276,9 @@ } }, "node_modules/jwa": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", - "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", "license": "MIT", "dependencies": { "buffer-equal-constant-time": "^1.0.1", @@ -4544,12 +4287,12 @@ } }, "node_modules/jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", "license": "MIT", "dependencies": { - "jwa": "^1.4.1", + "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, @@ -4565,42 +4308,6 @@ "node": ">= 0.6.3" } }, - "node_modules/lazystream/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, - "node_modules/lazystream/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/lazystream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/lazystream/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/light-bolt11-decoder": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/light-bolt11-decoder/-/light-bolt11-decoder-3.2.0.tgz", @@ -4657,6 +4364,16 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/liquidjs-lib/node_modules/bip66": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz", + "integrity": "sha512-nemMHz95EmS38a26XbbdxIYj5csHd3RMP3H5bwQknX0WYHF01qhpufP42mLOwVICuH2JmhIhXiWs89MfUGL7Xw==", + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/liquidjs-lib/node_modules/bs58": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", @@ -4694,20 +4411,10 @@ "node": ">=8.0.0" } }, - "node_modules/liquidjs-lib/node_modules/varuint-bitcoin": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/varuint-bitcoin/-/varuint-bitcoin-1.1.2.tgz", - "integrity": "sha512-4EVb+w4rx+YfVM32HQX42AbbT7/1f5zwAYhIujKXKk8NQK+JfRVl3pqT3hjNn/L+RstigmGGKVwHA/P0wgITZw==", - "license": "MIT", - "peer": true, - "dependencies": { - "safe-buffer": "^5.1.1" - } - }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "license": "MIT" }, "node_modules/lodash.camelcase": { @@ -4774,38 +4481,14 @@ } }, "node_modules/lru-cache": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", - "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", - "license": "ISC", + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" } }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "license": "MIT", - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -4813,63 +4496,6 @@ "devOptional": true, "license": "ISC" }, - "node_modules/make-fetch-happen": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", - "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", - "license": "ISC", - "optional": true, - "peer": true, - "dependencies": { - "agentkeepalive": "^4.1.3", - "cacache": "^15.2.0", - "http-cache-semantics": "^4.1.0", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "is-lambda": "^1.0.1", - "lru-cache": "^6.0.0", - "minipass": "^3.1.3", - "minipass-collect": "^1.0.2", - "minipass-fetch": "^1.3.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.2", - "promise-retry": "^2.0.1", - "socks-proxy-agent": "^6.0.0", - "ssri": "^8.0.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/make-fetch-happen/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "license": "ISC", - "optional": true, - "peer": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/make-fetch-happen/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "optional": true, - "peer": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -5018,182 +4644,24 @@ } }, "node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "license": "ISC", "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-collect": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", - "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", - "license": "ISC", - "optional": true, - "peer": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-collect/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "optional": true, - "peer": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-fetch": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", - "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "minipass": "^3.1.0", - "minipass-sized": "^1.0.3", - "minizlib": "^2.0.0" - }, - "engines": { - "node": ">=8" - }, - "optionalDependencies": { - "encoding": "^0.1.12" - } - }, - "node_modules/minipass-fetch/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "optional": true, - "peer": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-flush": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", - "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", - "license": "ISC", - "optional": true, - "peer": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-flush/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "optional": true, - "peer": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-pipeline": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", - "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", - "license": "ISC", - "optional": true, - "peer": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-pipeline/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "optional": true, - "peer": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-sized": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", - "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", - "license": "ISC", - "optional": true, - "peer": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-sized/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "optional": true, - "peer": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" + "node": ">=16 || 14 >=14.17" } }, "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "license": "MIT", "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" + "minipass": "^7.1.2" }, "engines": { - "node": ">= 8" - } - }, - "node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" + "node": ">= 18" } }, "node_modules/mkdirp": { @@ -5249,7 +4717,7 @@ "version": "6.0.4", "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", "integrity": "sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "license": "ISC", "optional": true, "dependencies": { @@ -5291,9 +4759,9 @@ } }, "node_modules/nan": { - "version": "2.23.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz", - "integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==", + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.25.0.tgz", + "integrity": "sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g==", "license": "MIT", "optional": true }, @@ -5323,9 +4791,9 @@ } }, "node_modules/node-abi": { - "version": "3.75.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.75.0.tgz", - "integrity": "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==", + "version": "3.87.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz", + "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==", "license": "MIT", "dependencies": { "semver": "^7.3.5" @@ -5335,9 +4803,9 @@ } }, "node_modules/node-abi/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -5372,32 +4840,6 @@ } } }, - "node_modules/node-gyp": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", - "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "env-paths": "^2.2.0", - "glob": "^7.1.4", - "graceful-fs": "^4.2.6", - "make-fetch-happen": "^9.1.0", - "nopt": "^5.0.0", - "npmlog": "^6.0.0", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.2", - "which": "^2.0.2" - }, - "bin": { - "node-gyp": "bin/node-gyp.js" - }, - "engines": { - "node": ">= 10.12.0" - } - }, "node_modules/node-gyp-build": { "version": "4.8.4", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", @@ -5409,103 +4851,6 @@ "node-gyp-build-test": "build-test.js" } }, - "node_modules/node-gyp/node_modules/are-we-there-yet": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", - "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "optional": true, - "peer": true, - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/node-gyp/node_modules/gauge": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", - "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "optional": true, - "peer": true, - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.3", - "console-control-strings": "^1.1.0", - "has-unicode": "^2.0.1", - "signal-exit": "^3.0.7", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.5" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/node-gyp/node_modules/npmlog": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", - "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "optional": true, - "peer": true, - "dependencies": { - "are-we-there-yet": "^3.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^4.0.3", - "set-blocking": "^2.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/node-gyp/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/node-gyp/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-gyp/node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, "node_modules/nodemon": { "version": "2.0.22", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz", @@ -5555,19 +4900,43 @@ "readable-stream": "~1.0.31" } }, + "node_modules/noms/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "license": "MIT" + }, + "node_modules/noms/node_modules/readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/noms/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "license": "MIT" + }, "node_modules/nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", + "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", "license": "ISC", "dependencies": { - "abbrev": "1" + "abbrev": "^3.0.0" }, "bin": { "nopt": "bin/nopt.js" }, "engines": { - "node": ">=6" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/normalize-path": { @@ -5580,17 +4949,17 @@ } }, "node_modules/nostr-tools": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.16.2.tgz", - "integrity": "sha512-ZxH9EbSt5ypURZj2TGNJxZd0Omb5ag5KZSu8IyJMCdLyg2KKz+2GA0sP/cSawCQEkyviIN4eRT4G2gB/t9lMRw==", + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.23.1.tgz", + "integrity": "sha512-Q5SJ1omrseBFXtLwqDhufpFLA6vX3rS/IuBCc974qaYX6YKGwEPxa/ZsyxruUOr+b+5EpWL2hFmCB5AueYrfBw==", "license": "Unlicense", "dependencies": { - "@noble/ciphers": "^0.5.1", - "@noble/curves": "1.2.0", - "@noble/hashes": "1.3.1", - "@scure/base": "1.1.1", - "@scure/bip32": "1.3.1", - "@scure/bip39": "1.2.1", + "@noble/ciphers": "2.1.1", + "@noble/curves": "2.0.1", + "@noble/hashes": "2.0.1", + "@scure/base": "2.0.0", + "@scure/bip32": "2.0.1", + "@scure/bip39": "2.0.1", "nostr-wasm": "0.1.0" }, "peerDependencies": { @@ -5603,28 +4972,25 @@ } }, "node_modules/nostr-tools/node_modules/@noble/hashes": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", - "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", "license": "MIT", "engines": { - "node": ">= 16" + "node": ">= 20.19.0" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/nostr-tools/node_modules/@scure/base": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", - "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "license": "MIT" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.0.0.tgz", + "integrity": "sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } }, "node_modules/nostr-wasm": { "version": "0.1.0", @@ -5632,19 +4998,6 @@ "integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==", "license": "MIT" }, - "node_modules/npmlog": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", - "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "dependencies": { - "are-we-there-yet": "^2.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^3.0.0", - "set-blocking": "^2.0.0" - } - }, "node_modules/oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", @@ -5696,23 +5049,6 @@ "wrappy": "1" } }, - "node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -5747,9 +5083,9 @@ } }, "node_modules/path-scurry": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", - "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", + "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^11.0.0", @@ -5762,15 +5098,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/path-scurry/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/path-to-regexp": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", @@ -5808,14 +5135,14 @@ "license": "MIT" }, "node_modules/pg": { - "version": "8.16.3", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", - "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.18.0.tgz", + "integrity": "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==", "license": "MIT", "dependencies": { - "pg-connection-string": "^2.9.1", - "pg-pool": "^3.10.1", - "pg-protocol": "^1.10.3", + "pg-connection-string": "^2.11.0", + "pg-pool": "^3.11.0", + "pg-protocol": "^1.11.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, @@ -5823,7 +5150,7 @@ "node": ">= 16.0.0" }, "optionalDependencies": { - "pg-cloudflare": "^1.2.7" + "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" @@ -5835,16 +5162,16 @@ } }, "node_modules/pg-cloudflare": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", - "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", "license": "MIT", "optional": true }, "node_modules/pg-connection-string": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", - "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.11.0.tgz", + "integrity": "sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ==", "license": "MIT" }, "node_modules/pg-int8": { @@ -5857,18 +5184,18 @@ } }, "node_modules/pg-pool": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", - "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.11.0.tgz", + "integrity": "sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w==", "license": "MIT", "peerDependencies": { "pg": ">=8.0" } }, "node_modules/pg-protocol": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", - "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.11.0.tgz", + "integrity": "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==", "license": "MIT" }, "node_modules/pg-types": { @@ -5927,9 +5254,9 @@ } }, "node_modules/postgres-bytea": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", - "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5997,29 +5324,6 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "license": "MIT" }, - "node_modules/promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", - "license": "ISC", - "optional": true, - "peer": true - }, - "node_modules/promise-retry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/protobufjs": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", @@ -6110,12 +5414,12 @@ } }, "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -6164,15 +5468,15 @@ } }, "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8" @@ -6194,17 +5498,26 @@ } }, "node_modules/readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/readdir-glob": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", @@ -6301,9 +5614,9 @@ } }, "node_modules/request/node_modules/qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.5.tgz", + "integrity": "sha512-mzR4sElr1bfCaPJe7m8ilJ6ZXdDaGoObcYR0ZHSsktM/Lt21MVHj5De30GQH2eiZ1qGRTO7LCAzQsUeXTNexWQ==", "license": "BSD-3-Clause", "engines": { "node": ">=0.6" @@ -6328,17 +5641,6 @@ "node": ">=0.10.0" } }, - "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 4" - } - }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -6366,13 +5668,16 @@ } }, "node_modules/ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.3.tgz", + "integrity": "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==", "license": "MIT", "dependencies": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" + "hash-base": "^3.1.2", + "inherits": "^2.0.4" + }, + "engines": { + "node": ">= 0.8" } }, "node_modules/run-parallel": { @@ -6455,6 +5760,12 @@ "node": ">=18.0.0" } }, + "node_modules/secp256k1/node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "license": "MIT" + }, "node_modules/secp256k1/node_modules/elliptic": { "version": "6.6.1", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", @@ -6480,24 +5791,24 @@ } }, "node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", "license": "MIT", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "range-parser": "~1.2.1", - "statuses": "2.0.1" + "statuses": "~2.0.2" }, "engines": { "node": ">= 0.8.0" @@ -6518,36 +5829,21 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, - "node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", "license": "MIT", "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.19.0" + "send": "~0.19.1" }, "engines": { "node": ">= 0.8.0" } }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "license": "ISC" - }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -6685,10 +5981,16 @@ } }, "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC" + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, "node_modules/simple-concat": { "version": "1.0.1", @@ -6792,50 +6094,6 @@ "license": "MIT", "peer": true }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.7", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", - "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "ip-address": "^10.0.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", - "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "agent-base": "^6.0.2", - "debug": "^4.3.3", - "socks": "^2.6.2" - }, - "engines": { - "node": ">= 10" - } - }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -6861,40 +6119,6 @@ "node": ">=14" } }, - "node_modules/sqlite3": { - "version": "5.1.7", - "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz", - "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "optional": true, - "peer": true, - "dependencies": { - "bindings": "^1.5.0", - "node-addon-api": "^7.0.0", - "prebuild-install": "^7.1.1", - "tar": "^6.1.11" - }, - "optionalDependencies": { - "node-gyp": "8.x" - }, - "peerDependencies": { - "node-gyp": "8.x" - }, - "peerDependenciesMeta": { - "node-gyp": { - "optional": true - } - } - }, - "node_modules/sqlite3/node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/sshpk": { "version": "1.18.0", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", @@ -6920,43 +6144,15 @@ "node": ">=0.10.0" } }, - "node_modules/ssri": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", - "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", - "license": "ISC", - "optional": true, - "peer": true, - "dependencies": { - "minipass": "^3.1.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/ssri/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "optional": true, - "peer": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/standard-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/standard-error/-/standard-error-1.1.0.tgz", "integrity": "sha512-4v7qzU7oLJfMI5EltUSHCaaOd65J6S4BqKRWgzMi4EYaE5fvNabPxmAPGdxpGXqrcWjhDGI/H09CIdEuUOUeXg==" }, "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -6969,22 +6165,29 @@ "license": "MIT" }, "node_modules/streamx": { - "version": "2.22.1", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.1.tgz", - "integrity": "sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==", + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", "license": "MIT", "dependencies": { + "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" - }, - "optionalDependencies": { - "bare-events": "^2.2.0" } }, "node_modules/string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, "node_modules/string-width": { @@ -7064,26 +6267,25 @@ } }, "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "license": "ISC", + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", + "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", + "license": "BlueOak-1.0.0", "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" }, "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/tar-fs": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", - "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", "license": "MIT", "dependencies": { "chownr": "^1.1.1", @@ -7128,19 +6330,10 @@ "node": ">= 6" } }, - "node_modules/tar-stream/node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, "node_modules/text-decoder": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", - "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.6.tgz", + "integrity": "sha512-27FeW5GQFDfw0FpwMQhMagB7BztOOlmjcSRi97t2oplhKVTZtp0DZbSegSaXS5IIC6mxMvBG4AR1Sgc6BX3CQg==", "license": "Apache-2.0", "dependencies": { "b4a": "^1.6.4" @@ -7156,42 +6349,6 @@ "xtend": "~4.0.1" } }, - "node_modules/through2/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, - "node_modules/through2/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/through2/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/through2/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/tiny-secp256k1": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-2.2.4.tgz", @@ -7214,9 +6371,9 @@ } }, "node_modules/to-buffer": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz", - "integrity": "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", "license": "MIT", "dependencies": { "isarray": "^2.0.5", @@ -7441,22 +6598,23 @@ "license": "MIT" }, "node_modules/typeorm": { - "version": "0.3.26", - "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.26.tgz", - "integrity": "sha512-o2RrBNn3lczx1qv4j+JliVMmtkPSqEGpG0UuZkt9tCfWkoXKu8MZnjvp2GjWPll1SehwemQw6xrbVRhmOglj8Q==", + "version": "0.3.28", + "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.28.tgz", + "integrity": "sha512-6GH7wXhtfq2D33ZuRXYwIsl/qM5685WZcODZb7noOOcRMteM9KF2x2ap3H0EBjnSV0VO4gNAfJT5Ukp0PkOlvg==", "license": "MIT", "dependencies": { "@sqltools/formatter": "^1.2.5", - "ansis": "^3.17.0", + "ansis": "^4.2.0", "app-root-path": "^3.1.0", "buffer": "^6.0.3", - "dayjs": "^1.11.13", - "debug": "^4.4.0", - "dedent": "^1.6.0", - "dotenv": "^16.4.7", - "glob": "^10.4.5", - "sha.js": "^2.4.11", - "sql-highlight": "^6.0.0", + "dayjs": "^1.11.19", + "debug": "^4.4.3", + "dedent": "^1.7.0", + "dotenv": "^16.6.1", + "glob": "^10.5.0", + "reflect-metadata": "^0.2.2", + "sha.js": "^2.4.12", + "sql-highlight": "^6.1.0", "tslib": "^2.8.1", "uuid": "^11.1.0", "yargs": "^17.7.2" @@ -7473,19 +6631,18 @@ "url": "https://opencollective.com/typeorm" }, "peerDependencies": { - "@google-cloud/spanner": "^5.18.0 || ^6.0.0 || ^7.0.0", + "@google-cloud/spanner": "^5.18.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@sap/hana-client": "^2.14.22", "better-sqlite3": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0", "ioredis": "^5.0.4", "mongodb": "^5.8.0 || ^6.0.0", - "mssql": "^9.1.1 || ^10.0.1 || ^11.0.1", + "mssql": "^9.1.1 || ^10.0.0 || ^11.0.0 || ^12.0.0", "mysql2": "^2.2.5 || ^3.0.1", "oracledb": "^6.3.0", "pg": "^8.5.1", "pg-native": "^3.0.0", "pg-query-stream": "^4.0.0", "redis": "^3.1.1 || ^4.0.0 || ^5.0.14", - "reflect-metadata": "^0.1.14 || ^0.2.0", "sql.js": "^1.4.0", "sqlite3": "^5.0.3", "ts-node": "^10.7.0", @@ -7542,6 +6699,47 @@ } } }, + "node_modules/typeorm/node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/typeorm/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/typeorm/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/typeorm/node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -7575,10 +6773,17 @@ "ieee754": "^1.2.1" } }, + "node_modules/typeorm/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, "node_modules/typeorm/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", @@ -7631,15 +6836,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/typeorm/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/typeorm/node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", @@ -7656,6 +6852,38 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/typeorm/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typeorm/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/typeorm/node_modules/uuid": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", @@ -7669,6 +6897,23 @@ "uuid": "dist/esm/bin/uuid" } }, + "node_modules/typeorm/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/typescript": { "version": "5.5.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", @@ -7682,6 +6927,15 @@ "node": ">=14.17" } }, + "node_modules/uint8array-tools": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.8.tgz", + "integrity": "sha512-xS6+s8e0Xbx++5/0L+yyexukU7pz//Yg6IHg3BKhXotg1JcYtgxVcUctQ0HxLByiJzpAkNFawz1Nz5Xadzo82g==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -7689,28 +6943,6 @@ "dev": true, "license": "MIT" }, - "node_modules/unique-filename": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", - "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", - "license": "ISC", - "optional": true, - "peer": true, - "dependencies": { - "unique-slug": "^2.0.0" - } - }, - "node_modules/unique-slug": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", - "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", - "license": "ISC", - "optional": true, - "peer": true, - "dependencies": { - "imurmurhash": "^0.1.4" - } - }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -7747,21 +6979,6 @@ "pct-encode": "~1.0.0" } }, - "node_modules/utf-8-validate": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", - "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">=6.14.2" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -7793,22 +7010,27 @@ "devOptional": true, "license": "MIT" }, - "node_modules/varuint-bitcoin": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/varuint-bitcoin/-/varuint-bitcoin-2.0.0.tgz", - "integrity": "sha512-6QZbU/rHO2ZQYpWFDALCDSRsXbAs1VOEmXAxtbtjLtKuMJ/FQ8YbhfxlaiKv5nklci0M6lZtlZyxo9Q+qNnyog==", + "node_modules/valibot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz", + "integrity": "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==", "license": "MIT", - "dependencies": { - "uint8array-tools": "^0.0.8" + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/varuint-bitcoin/node_modules/uint8array-tools": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.8.tgz", - "integrity": "sha512-xS6+s8e0Xbx++5/0L+yyexukU7pz//Yg6IHg3BKhXotg1JcYtgxVcUctQ0HxLByiJzpAkNFawz1Nz5Xadzo82g==", + "node_modules/varuint-bitcoin": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/varuint-bitcoin/-/varuint-bitcoin-1.1.2.tgz", + "integrity": "sha512-4EVb+w4rx+YfVM32HQX42AbbT7/1f5zwAYhIujKXKk8NQK+JfRVl3pqT3hjNn/L+RstigmGGKVwHA/P0wgITZw==", "license": "MIT", - "engines": { - "node": ">=14.0.0" + "dependencies": { + "safe-buffer": "^5.1.1" } }, "node_modules/vary": { @@ -7872,9 +7094,9 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", @@ -7904,15 +7126,6 @@ "node": ">=20.11" } }, - "node_modules/wide-align": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", - "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "license": "ISC", - "dependencies": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - }, "node_modules/wif": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/wif/-/wif-2.0.6.tgz", @@ -8012,9 +7225,9 @@ } }, "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -8051,10 +7264,13 @@ } }, "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } }, "node_modules/yargs": { "version": "17.7.2", @@ -8104,15 +7320,40 @@ "is-glob": "^4.0.3" } }, + "node_modules/zip-a-folder/node_modules/balanced-match": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.2.tgz", + "integrity": "sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==", + "license": "MIT", + "dependencies": { + "jackspeak": "^4.2.3" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/zip-a-folder/node_modules/brace-expansion": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz", + "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/zip-a-folder/node_modules/glob": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", - "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", - "license": "ISC", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "BlueOak-1.0.0", "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", - "minimatch": "^10.0.3", + "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" @@ -8128,12 +7369,12 @@ } }, "node_modules/zip-a-folder/node_modules/minimatch": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", - "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", - "license": "ISC", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.0.tgz", + "integrity": "sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w==", + "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" + "brace-expansion": "^5.0.2" }, "engines": { "node": "20 || >=22" @@ -8142,15 +7383,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/zip-a-folder/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/zip-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", diff --git a/package.json b/package.json index 7db3acdb..54f95e5c 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "zip-a-folder": "^3.1.9" }, "devDependencies": { + "@types/better-sqlite3": "^7.6.13", "@types/chai": "^4.3.4", "@types/chai-string": "^1.4.5", "@types/cors": "^2.8.17", @@ -93,4 +94,4 @@ "typescript": "5.5.4" }, "overrides": {} -} \ No newline at end of file +} diff --git a/proto/autogenerated/client.md b/proto/autogenerated/client.md index 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/extensions/README.md b/src/extensions/README.md new file mode 100644 index 00000000..2f4e0b15 --- /dev/null +++ b/src/extensions/README.md @@ -0,0 +1,731 @@ +# Lightning.Pub Extension System + +A modular extension system that allows third-party functionality to be added to Lightning.Pub without modifying core code. + +## Table of Contents + +- [Overview](#overview) +- [Architecture](#architecture) +- [Creating an Extension](#creating-an-extension) +- [Extension Lifecycle](#extension-lifecycle) +- [ExtensionContext API](#extensioncontext-api) +- [Database Isolation](#database-isolation) +- [RPC Methods](#rpc-methods) +- [HTTP Routes](#http-routes) +- [Event Handling](#event-handling) +- [Configuration](#configuration) +- [Examples](#examples) + +--- + +## Overview + +The extension system provides: + +- **Modularity**: Extensions are self-contained modules with their own code and data +- **Isolation**: Each extension gets its own SQLite database +- **Integration**: Extensions can register RPC methods, handle events, and interact with Lightning.Pub's payment and Nostr systems +- **Lifecycle Management**: Automatic discovery, loading, and graceful shutdown + +### Built-in Extensions + +| Extension | Description | +|-----------|-------------| +| `marketplace` | NIP-15 Nostr marketplace for selling products via Lightning | +| `withdraw` | LNURL-withdraw (LUD-03) for vouchers, faucets, and gifts | + +--- + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Lightning.Pub │ +├─────────────────────────────────────────────────────────────────┤ +│ Extension Loader │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ Extension A │ │ Extension B │ │ Extension C │ ... │ +│ │ ┌───────┐ │ │ ┌───────┐ │ │ ┌───────┐ │ │ +│ │ │Context│ │ │ │Context│ │ │ │Context│ │ │ +│ │ └───────┘ │ │ └───────┘ │ │ └───────┘ │ │ +│ │ ┌───────┐ │ │ ┌───────┐ │ │ ┌───────┐ │ │ +│ │ │ DB │ │ │ │ DB │ │ │ │ DB │ │ │ +│ │ └───────┘ │ │ └───────┘ │ │ └───────┘ │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +├─────────────────────────────────────────────────────────────────┤ +│ Payment Manager │ Nostr Transport │ Application Manager │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### Key Components + +| Component | File | Description | +|-----------|------|-------------| +| `ExtensionLoader` | `loader.ts` | Discovers, loads, and manages extensions | +| `ExtensionContext` | `context.ts` | Bridge between extensions and Lightning.Pub | +| `ExtensionDatabase` | `database.ts` | Isolated SQLite database per extension | + +--- + +## Creating an Extension + +### Directory Structure + +``` +src/extensions/ +└── my-extension/ + ├── index.ts # Main entry point (required) + ├── types.ts # TypeScript interfaces + ├── migrations.ts # Database migrations + └── managers/ # Business logic + └── myManager.ts +``` + +### Minimal Extension + +```typescript +// src/extensions/my-extension/index.ts + +import { Extension, ExtensionInfo, ExtensionContext, ExtensionDatabase } from '../types.js' + +export default class MyExtension implements Extension { + readonly info: ExtensionInfo = { + id: 'my-extension', // Must match directory name + name: 'My Extension', + version: '1.0.0', + description: 'Does something useful', + author: 'Your Name', + minPubVersion: '1.0.0' // Minimum Lightning.Pub version + } + + async initialize(ctx: ExtensionContext, db: ExtensionDatabase): Promise { + // Run migrations + await db.execute(` + CREATE TABLE IF NOT EXISTS my_table ( + id TEXT PRIMARY KEY, + data TEXT + ) + `) + + // Register RPC methods + ctx.registerMethod('my-extension.doSomething', async (req, appId) => { + return { result: 'done' } + }) + + ctx.log('info', 'Extension initialized') + } + + async shutdown(): Promise { + // Cleanup resources + } +} +``` + +### Extension Interface + +```typescript +interface Extension { + // Required: Extension metadata + readonly info: ExtensionInfo + + // Required: Called once when extension is loaded + initialize(ctx: ExtensionContext, db: ExtensionDatabase): Promise + + // Optional: Called when Lightning.Pub shuts down + shutdown?(): Promise + + // Optional: Health check for monitoring + healthCheck?(): Promise +} + +interface ExtensionInfo { + id: string // Unique identifier (lowercase, no spaces) + name: string // Display name + version: string // Semver version + description: string // Short description + author: string // Author name + minPubVersion?: string // Minimum Lightning.Pub version + dependencies?: string[] // Other extension IDs required +} +``` + +--- + +## Extension Lifecycle + +``` +┌──────────────┐ +│ Discover │ Scan extensions directory for index.ts files +└──────┬───────┘ + │ + ▼ +┌──────────────┐ +│ Load │ Import module, instantiate class +└──────┬───────┘ + │ + ▼ +┌──────────────┐ +│ Initialize │ Create database, call initialize() +└──────┬───────┘ + │ + ▼ +┌──────────────┐ +│ Ready │ Extension is active, handling requests +└──────┬───────┘ + │ + ▼ (on shutdown) +┌──────────────┐ +│ Shutdown │ Call shutdown(), close database +└──────────────┘ +``` + +### States + +| State | Description | +|-------|-------------| +| `loading` | Extension is being loaded | +| `ready` | Extension is active and healthy | +| `error` | Initialization failed | +| `stopped` | Extension has been shut down | + +--- + +## ExtensionContext API + +The `ExtensionContext` is passed to your extension during initialization. It provides access to Lightning.Pub functionality. + +### Application Management + +```typescript +// Get information about an application +const app = await ctx.getApplication(applicationId) +// Returns: { id, name, nostr_public, balance_sats } | null +``` + +### Payment Operations + +```typescript +// Create a Lightning invoice +const invoice = await ctx.createInvoice(amountSats, { + memo: 'Payment for service', + expiry: 3600, // seconds + metadata: { order_id: '123' } // Returned in payment callback +}) +// Returns: { id, paymentRequest, paymentHash, expiry } + +// Pay a Lightning invoice +const result = await ctx.payInvoice(applicationId, bolt11Invoice, maxFeeSats) +// Returns: { paymentHash, feeSats } +``` + +### Nostr Operations + +```typescript +// Send encrypted DM (NIP-44) +const eventId = await ctx.sendEncryptedDM(applicationId, recipientPubkey, content) + +// Publish a Nostr event (signed by application's key) +const eventId = await ctx.publishNostrEvent({ + kind: 30017, + pubkey: appPubkey, + created_at: Math.floor(Date.now() / 1000), + tags: [['d', 'identifier']], + content: JSON.stringify(data) +}) +``` + +### RPC Method Registration + +```typescript +// Register a method that can be called via RPC +ctx.registerMethod('my-extension.methodName', async (request, applicationId, userPubkey?) => { + // request: The RPC request payload + // applicationId: The calling application's ID + // userPubkey: The user's Nostr pubkey (if authenticated) + + return { result: 'success' } +}) +``` + +### Event Subscriptions + +```typescript +// Subscribe to payment received events +ctx.onPaymentReceived(async (payment) => { + // payment: { invoiceId, paymentHash, amountSats, metadata } + + if (payment.metadata?.extension === 'my-extension') { + // Handle payment for this extension + } +}) + +// Subscribe to incoming Nostr events +ctx.onNostrEvent(async (event, applicationId) => { + // event: { id, pubkey, kind, tags, content, created_at } + // applicationId: The application this event is for + + if (event.kind === 4) { // DM + // Handle incoming message + } +}) +``` + +### Logging + +```typescript +ctx.log('debug', 'Detailed debugging info') +ctx.log('info', 'Normal operation info') +ctx.log('warn', 'Warning message') +ctx.log('error', 'Error occurred', errorObject) +``` + +--- + +## Database Isolation + +Each extension gets its own SQLite database file at: +``` +{databaseDir}/{extension-id}.db +``` + +### Database Interface + +```typescript +interface ExtensionDatabase { + // Execute write queries (INSERT, UPDATE, DELETE, CREATE) + execute(sql: string, params?: any[]): Promise<{ changes?: number; lastId?: number }> + + // Execute read queries (SELECT) + query(sql: string, params?: any[]): Promise + + // Run multiple statements in a transaction + transaction(fn: () => Promise): Promise +} +``` + +### Migration Pattern + +```typescript +// migrations.ts + +export interface Migration { + version: number + name: string + up: (db: ExtensionDatabase) => Promise +} + +export const migrations: Migration[] = [ + { + version: 1, + name: 'create_initial_tables', + up: async (db) => { + await db.execute(` + CREATE TABLE items ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + created_at INTEGER NOT NULL + ) + `) + } + }, + { + version: 2, + name: 'add_status_column', + up: async (db) => { + await db.execute(`ALTER TABLE items ADD COLUMN status TEXT DEFAULT 'active'`) + } + } +] + +// Run migrations in initialize() +export async function runMigrations(db: ExtensionDatabase): Promise { + const result = await db.query<{ value: string }>( + `SELECT value FROM _extension_meta WHERE key = 'migration_version'` + ).catch(() => []) + + const currentVersion = result.length > 0 ? parseInt(result[0].value, 10) : 0 + + for (const migration of migrations) { + if (migration.version > currentVersion) { + console.log(`Running migration ${migration.version}: ${migration.name}`) + await migration.up(db) + await db.execute( + `INSERT INTO _extension_meta (key, value) VALUES ('migration_version', ?) + ON CONFLICT(key) DO UPDATE SET value = excluded.value`, + [String(migration.version)] + ) + } + } +} +``` + +--- + +## RPC Methods + +Extensions register RPC methods that can be called by clients. + +### Naming Convention + +Methods should be namespaced with the extension ID: +``` +{extension-id}.{methodName} +``` + +Examples: +- `marketplace.createStall` +- `withdraw.createLink` + +### Method Handler Signature + +```typescript +type RpcMethodHandler = ( + request: any, // The request payload + applicationId: string, // The calling application + userPubkey?: string // The authenticated user (if any) +) => Promise +``` + +### Example + +```typescript +ctx.registerMethod('my-extension.createItem', async (req, appId, userPubkey) => { + // Validate request + if (!req.name) { + throw new Error('Name is required') + } + + // Create item + const item = await this.manager.create(appId, req) + + // Return response + return { item } +}) +``` + +--- + +## HTTP Routes + +Some extensions need HTTP endpoints (e.g., LNURL protocol). Extensions can define routes that the main application mounts. + +### Defining Routes + +```typescript +interface HttpRoute { + method: 'GET' | 'POST' + path: string + handler: (req: HttpRequest) => Promise +} + +interface HttpRequest { + params: Record // URL path params + query: Record // Query string params + body?: any // POST body + headers: Record +} + +interface HttpResponse { + status: number + body: any + headers?: Record +} +``` + +### Example + +```typescript +class MyExtension implements Extension { + getHttpRoutes(): HttpRoute[] { + return [ + { + method: 'GET', + path: '/api/v1/my-extension/:id', + handler: async (req) => { + const item = await this.getItem(req.params.id) + return { + status: 200, + body: item, + headers: { 'Content-Type': 'application/json' } + } + } + } + ] + } +} +``` + +--- + +## Event Handling + +### Payment Callbacks + +When you create an invoice with metadata, you'll receive that metadata back in the payment callback: + +```typescript +// Creating invoice with metadata +const invoice = await ctx.createInvoice(1000, { + metadata: { + extension: 'my-extension', + order_id: 'order-123' + } +}) + +// Handling payment +ctx.onPaymentReceived(async (payment) => { + if (payment.metadata?.extension === 'my-extension') { + const orderId = payment.metadata.order_id + await this.handlePayment(orderId, payment) + } +}) +``` + +### Nostr Events + +Subscribe to Nostr events for your application: + +```typescript +ctx.onNostrEvent(async (event, applicationId) => { + // Filter by event kind + if (event.kind === 4) { // Encrypted DM + await this.handleDirectMessage(event, applicationId) + } +}) +``` + +--- + +## Configuration + +### Loader Configuration + +```typescript +interface ExtensionLoaderConfig { + extensionsDir: string // Directory containing extensions + databaseDir: string // Directory for extension databases + enabledExtensions?: string[] // Whitelist (if set, only these load) + disabledExtensions?: string[] // Blacklist +} +``` + +### Usage + +```typescript +import { createExtensionLoader } from './extensions' + +const loader = createExtensionLoader({ + extensionsDir: './src/extensions', + databaseDir: './data/extensions', + disabledExtensions: ['experimental-ext'] +}, mainHandler) + +await loader.loadAll() + +// Call extension methods +const result = await loader.callMethod( + 'marketplace.createStall', + { name: 'My Shop', currency: 'sat', shipping_zones: [] }, + applicationId, + userPubkey +) + +// Dispatch events +loader.dispatchPaymentReceived(paymentData) +loader.dispatchNostrEvent(event, applicationId) + +// Shutdown +await loader.shutdown() +``` + +--- + +## Examples + +### Example: Simple Counter Extension + +```typescript +// src/extensions/counter/index.ts + +import { Extension, ExtensionInfo, ExtensionContext, ExtensionDatabase } from '../types.js' + +export default class CounterExtension implements Extension { + readonly info: ExtensionInfo = { + id: 'counter', + name: 'Simple Counter', + version: '1.0.0', + description: 'A simple counter for each application', + author: 'Example' + } + + private db!: ExtensionDatabase + + async initialize(ctx: ExtensionContext, db: ExtensionDatabase): Promise { + this.db = db + + await db.execute(` + CREATE TABLE IF NOT EXISTS counters ( + application_id TEXT PRIMARY KEY, + count INTEGER NOT NULL DEFAULT 0 + ) + `) + + ctx.registerMethod('counter.increment', async (req, appId) => { + await db.execute( + `INSERT INTO counters (application_id, count) VALUES (?, 1) + ON CONFLICT(application_id) DO UPDATE SET count = count + 1`, + [appId] + ) + const result = await db.query<{ count: number }>( + 'SELECT count FROM counters WHERE application_id = ?', + [appId] + ) + return { count: result[0]?.count || 0 } + }) + + ctx.registerMethod('counter.get', async (req, appId) => { + const result = await db.query<{ count: number }>( + 'SELECT count FROM counters WHERE application_id = ?', + [appId] + ) + return { count: result[0]?.count || 0 } + }) + + ctx.registerMethod('counter.reset', async (req, appId) => { + await db.execute( + 'UPDATE counters SET count = 0 WHERE application_id = ?', + [appId] + ) + return { count: 0 } + }) + } +} +``` + +### Example: Payment-Triggered Extension + +```typescript +// src/extensions/donations/index.ts + +import { Extension, ExtensionContext, ExtensionDatabase } from '../types.js' + +export default class DonationsExtension implements Extension { + readonly info = { + id: 'donations', + name: 'Donations', + version: '1.0.0', + description: 'Accept donations with thank-you messages', + author: 'Example' + } + + private db!: ExtensionDatabase + private ctx!: ExtensionContext + + async initialize(ctx: ExtensionContext, db: ExtensionDatabase): Promise { + this.db = db + this.ctx = ctx + + await db.execute(` + CREATE TABLE IF NOT EXISTS donations ( + id TEXT PRIMARY KEY, + application_id TEXT NOT NULL, + amount_sats INTEGER NOT NULL, + donor_pubkey TEXT, + message TEXT, + created_at INTEGER NOT NULL + ) + `) + + // Create donation invoice + ctx.registerMethod('donations.createInvoice', async (req, appId) => { + const invoice = await ctx.createInvoice(req.amount_sats, { + memo: req.message || 'Donation', + metadata: { + extension: 'donations', + donor_pubkey: req.donor_pubkey, + message: req.message + } + }) + return { invoice: invoice.paymentRequest } + }) + + // Handle successful payments + ctx.onPaymentReceived(async (payment) => { + if (payment.metadata?.extension !== 'donations') return + + // Record donation + await db.execute( + `INSERT INTO donations (id, application_id, amount_sats, donor_pubkey, message, created_at) + VALUES (?, ?, ?, ?, ?, ?)`, + [ + payment.paymentHash, + payment.metadata.application_id, + payment.amountSats, + payment.metadata.donor_pubkey, + payment.metadata.message, + Math.floor(Date.now() / 1000) + ] + ) + + // Send thank-you DM if donor has pubkey + if (payment.metadata.donor_pubkey) { + await ctx.sendEncryptedDM( + payment.metadata.application_id, + payment.metadata.donor_pubkey, + `Thank you for your donation of ${payment.amountSats} sats!` + ) + } + }) + + // List donations + ctx.registerMethod('donations.list', async (req, appId) => { + const donations = await db.query( + `SELECT * FROM donations WHERE application_id = ? ORDER BY created_at DESC LIMIT ?`, + [appId, req.limit || 50] + ) + return { donations } + }) + } +} +``` + +--- + +## Best Practices + +1. **Namespace your methods**: Always prefix RPC methods with your extension ID +2. **Use migrations**: Never modify existing migration files; create new ones +3. **Handle errors gracefully**: Throw descriptive errors, don't return error objects +4. **Clean up in shutdown**: Close connections, cancel timers, etc. +5. **Log appropriately**: Use debug for verbose info, error for failures +6. **Validate inputs**: Check request parameters before processing +7. **Use transactions**: For multi-step database operations +8. **Document your API**: Include types and descriptions for RPC methods + +--- + +## Troubleshooting + +### Extension not loading + +1. Check that directory name matches `info.id` +2. Verify `index.ts` has a default export +3. Check for TypeScript/import errors in logs + +### Database errors + +1. Check migration syntax +2. Verify column types match queries +3. Look for migration version conflicts + +### RPC method not found + +1. Verify method is registered in `initialize()` +2. Check method name includes extension prefix +3. Ensure extension status is `ready` + +### Payment callbacks not firing + +1. Verify `metadata.extension` matches your extension ID +2. Check that `onPaymentReceived` is registered in `initialize()` +3. Confirm invoice was created through the extension diff --git a/src/extensions/context.ts b/src/extensions/context.ts new file mode 100644 index 00000000..f18891f5 --- /dev/null +++ b/src/extensions/context.ts @@ -0,0 +1,324 @@ +import { + ExtensionContext, + ExtensionDatabase, + ExtensionInfo, + ApplicationInfo, + CreateInvoiceOptions, + CreatedInvoice, + PaymentReceivedData, + NostrEvent, + UnsignedNostrEvent, + RpcMethodHandler, + LnurlPayInfo +} from './types.js' + +/** + * Main Handler interface (from Lightning.Pub) + * This is a minimal interface - the actual MainHandler has more methods + */ +export interface MainHandlerInterface { + // Application management + applicationManager: { + getById(id: string): Promise + PayAppUserInvoice(appId: string, req: { + amount: number + invoice: string + user_identifier: string + debit_npub?: string + }): Promise<{ + preimage: string + amount_paid: number + network_fee: number + service_fee: number + }> + } + + // Payment operations + paymentManager: { + createInvoice(params: { + applicationId: string + amountSats: number + memo?: string + expiry?: number + metadata?: Record + }): Promise<{ + id: string + paymentRequest: string + paymentHash: string + expiry: number + }> + + payInvoice(params: { + applicationId: string + paymentRequest: string + maxFeeSats?: number + userPubkey?: string + }): Promise<{ + paymentHash: string + feeSats: number + }> + + /** + * Get LNURL-pay info for a user by their Nostr pubkey + * This enables Lightning Address (LUD-16) and zap (NIP-57) support + */ + getLnurlPayInfoByPubkey(pubkeyHex: string, options?: { + metadata?: string + description?: string + }): Promise + } + + // Nostr operations + sendNostrEvent(event: any): Promise + sendEncryptedDM(applicationId: string, recipientPubkey: string, content: string): Promise +} + +/** + * Callback registries for extension events + */ +interface CallbackRegistries { + paymentReceived: Array<(payment: PaymentReceivedData) => Promise> + nostrEvent: Array<(event: NostrEvent, applicationId: string) => Promise> +} + +/** + * Registered RPC method + */ +interface RegisteredMethod { + extensionId: string + handler: RpcMethodHandler +} + +/** + * Extension Context Implementation + * + * Provides the interface for extensions to interact with Lightning.Pub. + * Each extension gets its own context instance. + */ +export class ExtensionContextImpl implements ExtensionContext { + private callbacks: CallbackRegistries = { + paymentReceived: [], + nostrEvent: [] + } + + constructor( + private extensionInfo: ExtensionInfo, + private database: ExtensionDatabase, + private mainHandler: MainHandlerInterface, + private methodRegistry: Map + ) {} + + /** + * Get information about an application + */ + async getApplication(applicationId: string): Promise { + try { + const app = await this.mainHandler.applicationManager.getById(applicationId) + if (!app) return null + + return { + id: app.id, + name: app.name, + nostr_public: app.nostr_public, + balance_sats: app.balance || 0 + } + } catch (e) { + this.log('error', `Failed to get application ${applicationId}:`, e) + return null + } + } + + /** + * Create a Lightning invoice + */ + async createInvoice(amountSats: number, options: CreateInvoiceOptions = {}): Promise { + // Note: In practice, this needs an applicationId. Extensions typically + // get this from the RPC request context. For now, we'll need to handle + // this in the actual implementation. + throw new Error('createInvoice requires applicationId from request context') + } + + /** + * Create invoice with explicit application ID + * This is the internal method used by extensions + */ + async createInvoiceForApp( + applicationId: string, + amountSats: number, + options: CreateInvoiceOptions = {} + ): Promise { + const result = await this.mainHandler.paymentManager.createInvoice({ + applicationId, + amountSats, + memo: options.memo, + expiry: options.expiry, + metadata: { + ...options.metadata, + extension: this.extensionInfo.id + } + }) + + return { + id: result.id, + paymentRequest: result.paymentRequest, + paymentHash: result.paymentHash, + expiry: result.expiry + } + } + + /** + * Pay a Lightning invoice + * If userPubkey is provided, pays from that user's balance instead of app.owner + */ + async payInvoice( + applicationId: string, + paymentRequest: string, + maxFeeSats?: number, + userPubkey?: string + ): Promise<{ paymentHash: string; feeSats: number }> { + return this.mainHandler.paymentManager.payInvoice({ + applicationId, + paymentRequest, + maxFeeSats, + userPubkey + }) + } + + /** + * Send an encrypted DM via Nostr + */ + async sendEncryptedDM( + applicationId: string, + recipientPubkey: string, + content: string + ): Promise { + return this.mainHandler.sendEncryptedDM(applicationId, recipientPubkey, content) + } + + /** + * Publish a Nostr event + */ + async publishNostrEvent(event: UnsignedNostrEvent): Promise { + return this.mainHandler.sendNostrEvent(event) + } + + /** + * Get LNURL-pay info for a user by pubkey + * Enables Lightning Address and zap support + */ + async getLnurlPayInfo(pubkeyHex: string, options?: { + metadata?: string + description?: string + }): Promise { + return this.mainHandler.paymentManager.getLnurlPayInfoByPubkey(pubkeyHex, options) + } + + /** + * Subscribe to payment received callbacks + */ + onPaymentReceived(callback: (payment: PaymentReceivedData) => Promise): void { + this.callbacks.paymentReceived.push(callback) + } + + /** + * Subscribe to incoming Nostr events + */ + onNostrEvent(callback: (event: NostrEvent, applicationId: string) => Promise): void { + this.callbacks.nostrEvent.push(callback) + } + + /** + * Register an RPC method + */ + registerMethod(name: string, handler: RpcMethodHandler): void { + const fullName = name.startsWith(`${this.extensionInfo.id}.`) + ? name + : `${this.extensionInfo.id}.${name}` + + if (this.methodRegistry.has(fullName)) { + throw new Error(`RPC method ${fullName} already registered`) + } + + this.methodRegistry.set(fullName, { + extensionId: this.extensionInfo.id, + handler + }) + + this.log('debug', `Registered RPC method: ${fullName}`) + } + + /** + * Get the extension's database + */ + getDatabase(): ExtensionDatabase { + return this.database + } + + /** + * Log a message + */ + log(level: 'debug' | 'info' | 'warn' | 'error', message: string, ...args: any[]): void { + const prefix = `[Extension:${this.extensionInfo.id}]` + switch (level) { + case 'debug': + console.debug(prefix, message, ...args) + break + case 'info': + console.info(prefix, message, ...args) + break + case 'warn': + console.warn(prefix, message, ...args) + break + case 'error': + console.error(prefix, message, ...args) + break + } + } + + // ===== Internal Methods (called by ExtensionLoader) ===== + + /** + * Dispatch payment received event to extension callbacks + */ + async dispatchPaymentReceived(payment: PaymentReceivedData): Promise { + for (const callback of this.callbacks.paymentReceived) { + try { + await callback(payment) + } catch (e) { + this.log('error', 'Error in payment callback:', e) + } + } + } + + /** + * Dispatch Nostr event to extension callbacks + */ + async dispatchNostrEvent(event: NostrEvent, applicationId: string): Promise { + for (const callback of this.callbacks.nostrEvent) { + try { + await callback(event, applicationId) + } catch (e) { + this.log('error', 'Error in Nostr event callback:', e) + } + } + } + + /** + * Get registered callbacks for external access + */ + getCallbacks(): CallbackRegistries { + return this.callbacks + } +} + +/** + * Create an extension context + */ +export function createExtensionContext( + extensionInfo: ExtensionInfo, + database: ExtensionDatabase, + mainHandler: MainHandlerInterface, + methodRegistry: Map +): ExtensionContextImpl { + return new ExtensionContextImpl(extensionInfo, database, mainHandler, methodRegistry) +} diff --git a/src/extensions/database.ts b/src/extensions/database.ts new file mode 100644 index 00000000..6f300c36 --- /dev/null +++ b/src/extensions/database.ts @@ -0,0 +1,148 @@ +import Database from 'better-sqlite3' +import path from 'path' +import fs from 'fs' +import { ExtensionDatabase } from './types.js' + +/** + * Extension Database Implementation + * + * Provides isolated SQLite database access for each extension. + * Uses better-sqlite3 for synchronous, high-performance access. + */ +export class ExtensionDatabaseImpl implements ExtensionDatabase { + private db: Database.Database + private extensionId: string + + constructor(extensionId: string, databaseDir: string) { + this.extensionId = extensionId + + // Ensure database directory exists + if (!fs.existsSync(databaseDir)) { + fs.mkdirSync(databaseDir, { recursive: true }) + } + + // Create database file for this extension + const dbPath = path.join(databaseDir, `${extensionId}.db`) + this.db = new Database(dbPath) + + // Enable WAL mode for better concurrency + this.db.pragma('journal_mode = WAL') + + // Enable foreign keys + this.db.pragma('foreign_keys = ON') + + // Create metadata table for tracking migrations + this.db.exec(` + CREATE TABLE IF NOT EXISTS _extension_meta ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + ) + `) + } + + /** + * Execute a write query (INSERT, UPDATE, DELETE, CREATE, etc.) + */ + async execute(sql: string, params: any[] = []): Promise<{ changes?: number; lastId?: number }> { + try { + const stmt = this.db.prepare(sql) + const result = stmt.run(...params) + + return { + changes: result.changes, + lastId: result.lastInsertRowid as number + } + } catch (e) { + console.error(`[Extension:${this.extensionId}] Database execute error:`, e) + throw e + } + } + + /** + * Execute a read query (SELECT) + */ + async query(sql: string, params: any[] = []): Promise { + try { + const stmt = this.db.prepare(sql) + return stmt.all(...params) as T[] + } catch (e) { + console.error(`[Extension:${this.extensionId}] Database query error:`, e) + throw e + } + } + + /** + * Execute multiple statements in a transaction + */ + async transaction(fn: () => Promise): Promise { + const runTransaction = this.db.transaction(() => { + // Note: better-sqlite3 transactions are synchronous + // We wrap the async function but it executes synchronously + return fn() + }) + + return runTransaction() as T + } + + /** + * Get a metadata value + */ + async getMeta(key: string): Promise { + const rows = await this.query<{ value: string }>( + 'SELECT value FROM _extension_meta WHERE key = ?', + [key] + ) + return rows.length > 0 ? rows[0].value : null + } + + /** + * Set a metadata value + */ + async setMeta(key: string, value: string): Promise { + await this.execute( + `INSERT INTO _extension_meta (key, value) VALUES (?, ?) + ON CONFLICT(key) DO UPDATE SET value = excluded.value`, + [key, value] + ) + } + + /** + * Get current migration version + */ + async getMigrationVersion(): Promise { + const version = await this.getMeta('migration_version') + return version ? parseInt(version, 10) : 0 + } + + /** + * Set migration version + */ + async setMigrationVersion(version: number): Promise { + await this.setMeta('migration_version', String(version)) + } + + /** + * Close the database connection + */ + close(): void { + this.db.close() + } + + /** + * Get the underlying database for advanced operations + * (Use with caution - bypasses isolation) + */ + getUnderlyingDb(): Database.Database { + return this.db + } +} + +/** + * Create an extension database instance + */ +export function createExtensionDatabase( + extensionId: string, + databaseDir: string +): ExtensionDatabaseImpl { + return new ExtensionDatabaseImpl(extensionId, databaseDir) +} diff --git a/src/extensions/index.ts b/src/extensions/index.ts new file mode 100644 index 00000000..208b7c86 --- /dev/null +++ b/src/extensions/index.ts @@ -0,0 +1,56 @@ +/** + * Lightning.Pub Extension System + * + * This module provides the extension infrastructure for Lightning.Pub. + * Extensions can add functionality like marketplaces, subscriptions, + * tipping, and more. + * + * Usage: + * + * ```typescript + * import { createExtensionLoader, ExtensionLoaderConfig } from './extensions' + * + * const config: ExtensionLoaderConfig = { + * extensionsDir: './extensions', + * databaseDir: './data/extensions' + * } + * + * const loader = createExtensionLoader(config, mainHandler) + * await loader.loadAll() + * + * // Call extension methods + * const result = await loader.callMethod( + * 'marketplace.createStall', + * { name: 'My Shop', currency: 'sat', shipping_zones: [...] }, + * applicationId + * ) + * ``` + */ + +// Export types +export { + Extension, + ExtensionInfo, + ExtensionContext, + ExtensionDatabase, + ExtensionModule, + ExtensionConstructor, + LoadedExtension, + ExtensionLoaderConfig, + ApplicationInfo, + CreateInvoiceOptions, + CreatedInvoice, + PaymentReceivedData, + NostrEvent, + UnsignedNostrEvent, + RpcMethodHandler +} from './types.js' + +// Export loader +export { ExtensionLoader, createExtensionLoader } from './loader.js' + +// Export database utilities +export { ExtensionDatabaseImpl, createExtensionDatabase } from './database.js' + +// Export context utilities +export { ExtensionContextImpl, createExtensionContext, MainHandlerInterface } from './context.js' diff --git a/src/extensions/loader.ts b/src/extensions/loader.ts new file mode 100644 index 00000000..9fc453c0 --- /dev/null +++ b/src/extensions/loader.ts @@ -0,0 +1,406 @@ +import path from 'path' +import fs from 'fs' +import { + Extension, + ExtensionInfo, + ExtensionModule, + LoadedExtension, + ExtensionLoaderConfig, + RpcMethodHandler, + PaymentReceivedData, + NostrEvent +} from './types.js' +import { ExtensionDatabaseImpl, createExtensionDatabase } from './database.js' +import { ExtensionContextImpl, createExtensionContext, MainHandlerInterface } from './context.js' + +/** + * Registered RPC method entry + */ +interface RegisteredMethod { + extensionId: string + handler: RpcMethodHandler +} + +/** + * Extension Loader + * + * Discovers, loads, and manages Lightning.Pub extensions. + * Provides lifecycle management and event dispatching. + */ +export class ExtensionLoader { + private config: ExtensionLoaderConfig + private mainHandler: MainHandlerInterface + private extensions: Map = new Map() + private contexts: Map = new Map() + private methodRegistry: Map = new Map() + private initialized = false + + constructor(config: ExtensionLoaderConfig, mainHandler: MainHandlerInterface) { + this.config = config + this.mainHandler = mainHandler + } + + /** + * Discover and load all extensions + */ + async loadAll(): Promise { + if (this.initialized) { + throw new Error('Extension loader already initialized') + } + + console.log('[Extensions] Loading extensions from:', this.config.extensionsDir) + + // Ensure directories exist + if (!fs.existsSync(this.config.extensionsDir)) { + console.log('[Extensions] Extensions directory does not exist, creating...') + fs.mkdirSync(this.config.extensionsDir, { recursive: true }) + this.initialized = true + return + } + + if (!fs.existsSync(this.config.databaseDir)) { + fs.mkdirSync(this.config.databaseDir, { recursive: true }) + } + + // Discover extensions + const extensionDirs = await this.discoverExtensions() + console.log(`[Extensions] Found ${extensionDirs.length} extension(s)`) + + // Load extensions in dependency order + const loadOrder = await this.resolveDependencies(extensionDirs) + + for (const extDir of loadOrder) { + try { + await this.loadExtension(extDir) + } catch (e) { + console.error(`[Extensions] Failed to load extension from ${extDir}:`, e) + } + } + + this.initialized = true + console.log(`[Extensions] Loaded ${this.extensions.size} extension(s)`) + } + + /** + * Discover extension directories + */ + private async discoverExtensions(): Promise { + const entries = fs.readdirSync(this.config.extensionsDir, { withFileTypes: true }) + const extensionDirs: string[] = [] + + for (const entry of entries) { + if (!entry.isDirectory()) continue + + const extDir = path.join(this.config.extensionsDir, entry.name) + const indexPath = path.join(extDir, 'index.ts') + const indexJsPath = path.join(extDir, 'index.js') + + // Check for index file + if (fs.existsSync(indexPath) || fs.existsSync(indexJsPath)) { + // Check enabled/disabled lists + if (this.config.disabledExtensions?.includes(entry.name)) { + console.log(`[Extensions] Skipping disabled extension: ${entry.name}`) + continue + } + + if (this.config.enabledExtensions && + !this.config.enabledExtensions.includes(entry.name)) { + console.log(`[Extensions] Skipping non-enabled extension: ${entry.name}`) + continue + } + + extensionDirs.push(extDir) + } + } + + return extensionDirs + } + + /** + * Resolve extension dependencies and return load order + */ + private async resolveDependencies(extensionDirs: string[]): Promise { + // For now, simple alphabetical order + // TODO: Implement proper dependency resolution with topological sort + return extensionDirs.sort() + } + + /** + * Load a single extension + */ + private async loadExtension(extensionDir: string): Promise { + const dirName = path.basename(extensionDir) + console.log(`[Extensions] Loading extension: ${dirName}`) + + // Determine index file path + let indexPath = path.join(extensionDir, 'index.js') + if (!fs.existsSync(indexPath)) { + indexPath = path.join(extensionDir, 'index.ts') + } + + // Dynamic import + const moduleUrl = `file://${indexPath}` + const module = await import(moduleUrl) as ExtensionModule + + if (!module.default) { + throw new Error(`Extension ${dirName} has no default export`) + } + + // Instantiate extension + const ExtensionClass = module.default + const instance = new ExtensionClass() as Extension + + if (!instance.info) { + throw new Error(`Extension ${dirName} has no info property`) + } + + const info = instance.info + + // Validate extension ID matches directory name + if (info.id !== dirName) { + console.warn( + `[Extensions] Extension ID '${info.id}' doesn't match directory '${dirName}'` + ) + } + + // Check for duplicate + if (this.extensions.has(info.id)) { + throw new Error(`Extension ${info.id} already loaded`) + } + + // Create isolated database + const database = createExtensionDatabase(info.id, this.config.databaseDir) + + // Create context + const context = createExtensionContext( + info, + database, + this.mainHandler, + this.methodRegistry + ) + + // Track as loading + const loaded: LoadedExtension = { + info, + instance, + database, + status: 'loading', + loadedAt: Date.now() + } + this.extensions.set(info.id, loaded) + this.contexts.set(info.id, context) + + try { + // Initialize extension + await instance.initialize(context, database) + + loaded.status = 'ready' + console.log(`[Extensions] Extension ${info.id} v${info.version} loaded successfully`) + } catch (e) { + loaded.status = 'error' + loaded.error = e as Error + console.error(`[Extensions] Extension ${info.id} initialization failed:`, e) + throw e + } + } + + /** + * Unload a specific extension + */ + async unloadExtension(extensionId: string): Promise { + const loaded = this.extensions.get(extensionId) + if (!loaded) { + throw new Error(`Extension ${extensionId} not found`) + } + + console.log(`[Extensions] Unloading extension: ${extensionId}`) + + try { + // Call shutdown if available + if (loaded.instance.shutdown) { + await loaded.instance.shutdown() + } + + loaded.status = 'stopped' + } catch (e) { + console.error(`[Extensions] Error during ${extensionId} shutdown:`, e) + } + + // Close database + if (loaded.database instanceof ExtensionDatabaseImpl) { + loaded.database.close() + } + + // Remove registered methods + for (const [name, method] of this.methodRegistry.entries()) { + if (method.extensionId === extensionId) { + this.methodRegistry.delete(name) + } + } + + // Remove from maps + this.extensions.delete(extensionId) + this.contexts.delete(extensionId) + } + + /** + * Shutdown all extensions + */ + async shutdown(): Promise { + console.log('[Extensions] Shutting down all extensions...') + + for (const extensionId of this.extensions.keys()) { + try { + await this.unloadExtension(extensionId) + } catch (e) { + console.error(`[Extensions] Error unloading ${extensionId}:`, e) + } + } + + console.log('[Extensions] All extensions shut down') + } + + /** + * Get a loaded extension + */ + getExtension(extensionId: string): LoadedExtension | undefined { + return this.extensions.get(extensionId) + } + + /** + * Get all loaded extensions + */ + getAllExtensions(): LoadedExtension[] { + return Array.from(this.extensions.values()) + } + + /** + * Check if an extension is loaded and ready + */ + isReady(extensionId: string): boolean { + const ext = this.extensions.get(extensionId) + return ext?.status === 'ready' + } + + /** + * Get all registered RPC methods + */ + getRegisteredMethods(): Map { + return this.methodRegistry + } + + /** + * Call an extension RPC method + */ + async callMethod( + methodName: string, + request: any, + applicationId: string, + userPubkey?: string + ): Promise { + const method = this.methodRegistry.get(methodName) + if (!method) { + throw new Error(`Unknown method: ${methodName}`) + } + + const ext = this.extensions.get(method.extensionId) + if (!ext || ext.status !== 'ready') { + throw new Error(`Extension ${method.extensionId} not ready`) + } + + return method.handler(request, applicationId, userPubkey) + } + + /** + * Check if a method exists + */ + hasMethod(methodName: string): boolean { + return this.methodRegistry.has(methodName) + } + + /** + * Dispatch payment received event to all extensions + */ + async dispatchPaymentReceived(payment: PaymentReceivedData): Promise { + for (const context of this.contexts.values()) { + try { + await context.dispatchPaymentReceived(payment) + } catch (e) { + console.error('[Extensions] Error dispatching payment:', e) + } + } + } + + /** + * Dispatch Nostr event to all extensions + */ + async dispatchNostrEvent(event: NostrEvent, applicationId: string): Promise { + for (const context of this.contexts.values()) { + try { + await context.dispatchNostrEvent(event, applicationId) + } catch (e) { + console.error('[Extensions] Error dispatching Nostr event:', e) + } + } + } + + /** + * Run health checks on all extensions + */ + async healthCheck(): Promise> { + const results = new Map() + + for (const [id, ext] of this.extensions.entries()) { + if (ext.status !== 'ready') { + results.set(id, false) + continue + } + + try { + if (ext.instance.healthCheck) { + results.set(id, await ext.instance.healthCheck()) + } else { + results.set(id, true) + } + } catch (e) { + results.set(id, false) + } + } + + return results + } + + /** + * Get extension status summary + */ + getStatus(): { + total: number + ready: number + error: number + extensions: Array<{ id: string; name: string; version: string; status: string }> + } { + const extensions = this.getAllExtensions().map(ext => ({ + id: ext.info.id, + name: ext.info.name, + version: ext.info.version, + status: ext.status + })) + + return { + total: extensions.length, + ready: extensions.filter(e => e.status === 'ready').length, + error: extensions.filter(e => e.status === 'error').length, + extensions + } + } +} + +/** + * Create an extension loader instance + */ +export function createExtensionLoader( + config: ExtensionLoaderConfig, + mainHandler: MainHandlerInterface +): ExtensionLoader { + return new ExtensionLoader(config, mainHandler) +} diff --git a/src/extensions/mainHandlerAdapter.ts b/src/extensions/mainHandlerAdapter.ts new file mode 100644 index 00000000..fec73c3f --- /dev/null +++ b/src/extensions/mainHandlerAdapter.ts @@ -0,0 +1,155 @@ +/** + * MainHandler Adapter for Extension System + * + * Wraps the Lightning.Pub mainHandler to provide the MainHandlerInterface + * required by the extension system. + */ + +import { MainHandlerInterface } from './context.js' +import { LnurlPayInfo } from './types.js' +import type Main from '../services/main/index.js' + +/** + * Create an adapter that wraps mainHandler for extension use + */ +export function createMainHandlerAdapter(mainHandler: Main): MainHandlerInterface { + return { + applicationManager: { + async getById(id: string) { + // The applicationManager stores apps internally + // We need to access it through the storage layer + try { + const app = await mainHandler.storage.applicationStorage.GetApplication(id) + if (!app) return null + + return { + id: app.app_id, + name: app.name, + nostr_public: app.nostr_public_key || '', + balance: app.owner?.balance_sats || 0 + } + } catch (e) { + // GetApplication throws if not found + return null + } + }, + + async PayAppUserInvoice(appId, req) { + return mainHandler.applicationManager.PayAppUserInvoice(appId, req) + } + }, + + paymentManager: { + async createInvoice(params: { + applicationId: string + amountSats: number + memo?: string + expiry?: number + metadata?: Record + }) { + // Get the app to find the user ID + const app = await mainHandler.storage.applicationStorage.GetApplication(params.applicationId) + if (!app) { + throw new Error(`Application not found: ${params.applicationId}`) + } + + // Create invoice using the app owner's user ID + const result = await mainHandler.paymentManager.NewInvoice( + app.owner.user_id, + { + amountSats: params.amountSats, + memo: params.memo || '' + }, + { + expiry: params.expiry || 3600 + } + ) + + return { + id: result.invoice.split(':')[0] || result.invoice, // Extract ID if present + paymentRequest: result.invoice, + paymentHash: '', // Not directly available from NewInvoice response + expiry: Date.now() + (params.expiry || 3600) * 1000 + } + }, + + async payInvoice(params: { + applicationId: string + paymentRequest: string + maxFeeSats?: number + userPubkey?: string + }) { + // Get the app to find the user ID and app reference + const app = await mainHandler.storage.applicationStorage.GetApplication(params.applicationId) + if (!app) { + throw new Error(`Application not found: ${params.applicationId}`) + } + + if (params.userPubkey) { + // Resolve the Nostr user's ApplicationUser to get their identifier + const appUser = await mainHandler.storage.applicationStorage.GetOrCreateNostrAppUser(app, params.userPubkey) + console.log(`[MainHandlerAdapter] Paying via PayAppUserInvoice from Nostr user ${params.userPubkey.slice(0, 8)}... (identifier: ${appUser.identifier})`) + + // Use applicationManager.PayAppUserInvoice so notifyAppUserPayment fires + // This sends LiveUserOperation events via Nostr for real-time balance updates + const result = await mainHandler.applicationManager.PayAppUserInvoice( + params.applicationId, + { + invoice: params.paymentRequest, + amount: 0, // Use invoice amount + user_identifier: appUser.identifier + } + ) + + return { + paymentHash: result.preimage || '', + feeSats: result.network_fee || 0 + } + } + + // Fallback: pay from app owner's balance (no Nostr user context) + const result = await mainHandler.paymentManager.PayInvoice( + app.owner.user_id, + { + invoice: params.paymentRequest, + amount: 0 + }, + app, + {} + ) + + return { + paymentHash: result.preimage || '', + feeSats: result.network_fee || 0 + } + }, + + async getLnurlPayInfoByPubkey(pubkeyHex: string, options?: { + metadata?: string + description?: string + }): Promise { + // This would need implementation based on how Lightning.Pub handles LNURL-pay + // For now, throw not implemented + throw new Error('getLnurlPayInfoByPubkey not yet implemented') + } + }, + + async sendNostrEvent(event: any): Promise { + // The mainHandler doesn't directly expose nostrSend + // This would need to be implemented through the nostrMiddleware + // For now, return null (not implemented) + console.warn('[MainHandlerAdapter] sendNostrEvent not fully implemented') + return null + }, + + async sendEncryptedDM( + applicationId: string, + recipientPubkey: string, + content: string + ): Promise { + // This would need implementation using NIP-44 encryption + // For now, throw not implemented + throw new Error('sendEncryptedDM not yet implemented') + } + } +} diff --git a/src/extensions/types.ts b/src/extensions/types.ts new file mode 100644 index 00000000..e67c1e4f --- /dev/null +++ b/src/extensions/types.ts @@ -0,0 +1,255 @@ +/** + * Extension System Core Types + * + * These types define the contract between Lightning.Pub and extensions. + */ + +/** + * Extension metadata + */ +export interface ExtensionInfo { + id: string // Unique identifier (lowercase, no spaces) + name: string // Display name + version: string // Semver version + description: string // Short description + author: string // Author name or organization + minPubVersion?: string // Minimum Lightning.Pub version required + dependencies?: string[] // Other extension IDs this depends on +} + +/** + * Extension database interface + * Provides isolated database access for each extension + */ +export interface ExtensionDatabase { + /** + * Execute a write query (INSERT, UPDATE, DELETE, CREATE, etc.) + */ + execute(sql: string, params?: any[]): Promise<{ changes?: number; lastId?: number }> + + /** + * Execute a read query (SELECT) + */ + query(sql: string, params?: any[]): Promise + + /** + * Execute multiple statements in a transaction + */ + transaction(fn: () => Promise): Promise +} + +/** + * Application info provided to extensions + */ +export interface ApplicationInfo { + id: string + name: string + nostr_public: string // Application's Nostr pubkey (hex) + balance_sats: number +} + +/** + * Invoice creation options + */ +export interface CreateInvoiceOptions { + memo?: string + expiry?: number // Seconds until expiry + metadata?: Record // Custom metadata for callbacks +} + +/** + * Created invoice result + */ +export interface CreatedInvoice { + id: string // Internal invoice ID + paymentRequest: string // BOLT11 invoice string + paymentHash: string // Payment hash (hex) + expiry: number // Expiry timestamp +} + +/** + * Payment received callback data + */ +export interface PaymentReceivedData { + invoiceId: string + paymentHash: string + amountSats: number + metadata?: Record +} + +/** + * LNURL-pay info response (LUD-06/LUD-16) + * Used for Lightning Address and zap support + */ +export interface LnurlPayInfo { + tag: 'payRequest' + callback: string // URL to call with amount + minSendable: number // Minimum msats + maxSendable: number // Maximum msats + metadata: string // JSON-encoded metadata array + allowsNostr?: boolean // Whether zaps are supported + nostrPubkey?: string // Pubkey for zap receipts (hex) +} + +/** + * Nostr event structure (minimal) + */ +export interface NostrEvent { + id: string + pubkey: string + created_at: number + kind: number + tags: string[][] + content: string + sig?: string +} + +/** + * Unsigned Nostr event for publishing + */ +export interface UnsignedNostrEvent { + kind: number + pubkey: string + created_at: number + tags: string[][] + content: string +} + +/** + * RPC method handler function + */ +export type RpcMethodHandler = ( + request: any, + applicationId: string, + userPubkey?: string +) => Promise + +/** + * Extension context - interface provided to extensions for interacting with Lightning.Pub + */ +export interface ExtensionContext { + /** + * Get information about an application + */ + getApplication(applicationId: string): Promise + + /** + * Create a Lightning invoice + */ + createInvoice(amountSats: number, options?: CreateInvoiceOptions): Promise + + /** + * Pay a Lightning invoice (requires sufficient balance) + * If userPubkey is provided, pays from that user's balance instead of app.owner + */ + payInvoice(applicationId: string, paymentRequest: string, maxFeeSats?: number, userPubkey?: string): Promise<{ + paymentHash: string + feeSats: number + }> + + /** + * Send an encrypted DM via Nostr (NIP-44) + */ + sendEncryptedDM(applicationId: string, recipientPubkey: string, content: string): Promise + + /** + * Publish a Nostr event (signed by application's key) + */ + publishNostrEvent(event: UnsignedNostrEvent): Promise + + /** + * Get LNURL-pay info for a user (by pubkey) + * Used to enable Lightning Address support (LUD-16) and zaps (NIP-57) + */ + getLnurlPayInfo(pubkeyHex: string, options?: { + metadata?: string // Custom metadata JSON + description?: string // Human-readable description + }): Promise + + /** + * Subscribe to payment received callbacks + */ + onPaymentReceived(callback: (payment: PaymentReceivedData) => Promise): void + + /** + * Subscribe to incoming Nostr events for the application + */ + onNostrEvent(callback: (event: NostrEvent, applicationId: string) => Promise): void + + /** + * Register an RPC method + */ + registerMethod(name: string, handler: RpcMethodHandler): void + + /** + * Get the extension's isolated database + */ + getDatabase(): ExtensionDatabase + + /** + * Log a message (prefixed with extension ID) + */ + log(level: 'debug' | 'info' | 'warn' | 'error', message: string, ...args: any[]): void +} + +/** + * Extension interface - what extensions must implement + */ +export interface Extension { + /** + * Extension metadata + */ + readonly info: ExtensionInfo + + /** + * Initialize the extension + * Called once when the extension is loaded + */ + initialize(ctx: ExtensionContext, db: ExtensionDatabase): Promise + + /** + * Shutdown the extension + * Called when Lightning.Pub is shutting down + */ + shutdown?(): Promise + + /** + * Health check + * Return true if extension is healthy + */ + healthCheck?(): Promise +} + +/** + * Extension constructor type + */ +export type ExtensionConstructor = new () => Extension + +/** + * Extension module default export + */ +export interface ExtensionModule { + default: ExtensionConstructor +} + +/** + * Loaded extension state + */ +export interface LoadedExtension { + info: ExtensionInfo + instance: Extension + database: ExtensionDatabase + status: 'loading' | 'ready' | 'error' | 'stopped' + error?: Error + loadedAt: number +} + +/** + * Extension loader configuration + */ +export interface ExtensionLoaderConfig { + extensionsDir: string // Directory containing extensions + databaseDir: string // Directory for extension databases + enabledExtensions?: string[] // If set, only load these extensions + disabledExtensions?: string[] // Extensions to skip +} diff --git a/src/extensions/withdraw/index.ts b/src/extensions/withdraw/index.ts new file mode 100644 index 00000000..1a38930b --- /dev/null +++ b/src/extensions/withdraw/index.ts @@ -0,0 +1,383 @@ +/** + * LNURL-withdraw Extension for Lightning.Pub + * + * Implements LUD-03 (LNURL-withdraw) for creating withdraw links + * that allow anyone to pull funds from a Lightning wallet. + * + * Use cases: + * - Quick vouchers (batch single-use codes) + * - Faucets + * - Gift cards / prepaid cards + * - Tips / donations + */ + +import { + Extension, + ExtensionInfo, + ExtensionContext, + ExtensionDatabase, + CreateWithdrawLinkRequest, + UpdateWithdrawLinkRequest, + HttpRoute, + HttpRequest, + HttpResponse +} from './types.js' +import { runMigrations } from './migrations.js' +import { WithdrawManager } from './managers/withdrawManager.js' + +/** + * LNURL-withdraw Extension + */ +export default class WithdrawExtension implements Extension { + readonly info: ExtensionInfo = { + id: 'withdraw', + name: 'LNURL Withdraw', + version: '1.0.0', + description: 'Create withdraw links for vouchers, faucets, and gifts (LUD-03)', + author: 'Lightning.Pub', + minPubVersion: '1.0.0' + } + + private manager!: WithdrawManager + private baseUrl: string = '' + + /** + * Initialize the extension + */ + async initialize(ctx: ExtensionContext, db: ExtensionDatabase): Promise { + // Run migrations + await runMigrations(db) + + // Initialize manager + this.manager = new WithdrawManager(db, ctx) + + // Register RPC methods + this.registerRpcMethods(ctx) + + // Register HTTP routes for LNURL protocol + this.registerHttpRoutes(ctx) + + ctx.log('info', 'Extension initialized') + } + + /** + * Shutdown the extension + */ + async shutdown(): Promise { + // Cleanup if needed + } + + /** + * Set the base URL for LNURL generation + * This should be called by the main application after loading + */ + setBaseUrl(url: string): void { + this.baseUrl = url + this.manager.setBaseUrl(url) + } + + /** + * Get HTTP routes for this extension + * These need to be mounted by the main HTTP server + */ + getHttpRoutes(): HttpRoute[] { + return [ + // Create withdraw link (HTTP API for ATM/external integrations) + { + method: 'POST', + path: '/api/v1/withdraw/create', + handler: this.handleCreateWithdrawLink.bind(this) + }, + // LNURL callback (user submits invoice) - MUST be before :unique_hash routes + { + method: 'GET', + path: '/api/v1/lnurl/cb/:unique_hash', + handler: this.handleLnurlCallback.bind(this) + }, + // Initial LNURL request (unique link with use hash) + { + method: 'GET', + path: '/api/v1/lnurl/:unique_hash/:id_unique_hash', + handler: this.handleLnurlUniqueRequest.bind(this) + }, + // Initial LNURL request (simple link) - MUST be last (catches all) + { + method: 'GET', + path: '/api/v1/lnurl/:unique_hash', + handler: this.handleLnurlRequest.bind(this) + } + ] + } + + /** + * Register RPC methods with the extension context + */ + private registerRpcMethods(ctx: ExtensionContext): void { + // Create withdraw link + ctx.registerMethod('withdraw.createLink', async (req, appId, userPubkey) => { + const link = await this.manager.create(appId, req as CreateWithdrawLinkRequest, userPubkey) + const stats = await this.manager.getWithdrawalStats(link.id) + return { + link, + total_withdrawn_sats: stats.total_sats, + withdrawals_count: stats.count + } + }) + + // Create quick vouchers + ctx.registerMethod('withdraw.createVouchers', async (req, appId) => { + const vouchers = await this.manager.createVouchers( + appId, + req.title, + req.amount, + req.count, + req.description + ) + return { + vouchers, + total_amount_sats: req.amount * req.count + } + }) + + // Get withdraw link + ctx.registerMethod('withdraw.getLink', async (req, appId) => { + const link = await this.manager.get(req.id, appId) + if (!link) throw new Error('Withdraw link not found') + const stats = await this.manager.getWithdrawalStats(link.id) + return { + link, + total_withdrawn_sats: stats.total_sats, + withdrawals_count: stats.count + } + }) + + // List withdraw links + ctx.registerMethod('withdraw.listLinks', async (req, appId) => { + const links = await this.manager.list( + appId, + req.include_spent || false, + req.limit, + req.offset + ) + return { links } + }) + + // Update withdraw link + ctx.registerMethod('withdraw.updateLink', async (req, appId) => { + const link = await this.manager.update(req.id, appId, req as UpdateWithdrawLinkRequest) + if (!link) throw new Error('Withdraw link not found') + const stats = await this.manager.getWithdrawalStats(link.id) + return { + link, + total_withdrawn_sats: stats.total_sats, + withdrawals_count: stats.count + } + }) + + // Delete withdraw link + ctx.registerMethod('withdraw.deleteLink', async (req, appId) => { + const success = await this.manager.delete(req.id, appId) + if (!success) throw new Error('Withdraw link not found') + return { success } + }) + + // List withdrawals + ctx.registerMethod('withdraw.listWithdrawals', async (req, appId) => { + const withdrawals = await this.manager.listWithdrawals( + appId, + req.link_id, + req.limit, + req.offset + ) + return { withdrawals } + }) + + // Get withdrawal stats + ctx.registerMethod('withdraw.getStats', async (req, appId) => { + // Get all links to calculate total stats + const links = await this.manager.list(appId, true) + + let totalLinks = links.length + let activeLinks = 0 + let spentLinks = 0 + let totalWithdrawn = 0 + let totalWithdrawals = 0 + + for (const link of links) { + if (link.used >= link.uses) { + spentLinks++ + } else { + activeLinks++ + } + const stats = await this.manager.getWithdrawalStats(link.id) + totalWithdrawn += stats.total_sats + totalWithdrawals += stats.count + } + + return { + total_links: totalLinks, + active_links: activeLinks, + spent_links: spentLinks, + total_withdrawn_sats: totalWithdrawn, + total_withdrawals: totalWithdrawals + } + }) + } + + /** + * Register HTTP routes (called by extension context) + */ + private registerHttpRoutes(ctx: ExtensionContext): void { + // HTTP routes are exposed via getHttpRoutes() + // The main application is responsible for mounting them + ctx.log('debug', 'HTTP routes registered for LNURL protocol') + } + + // ========================================================================= + // HTTP Route Handlers + // ========================================================================= + + /** + * Handle create withdraw link request (HTTP API) + * POST /api/v1/withdraw/create + * + * Body: { + * title: string + * min_withdrawable: number (sats) + * max_withdrawable: number (sats) + * uses?: number (defaults to 1) + * wait_time?: number (seconds between uses, defaults to 0) + * } + * + * Auth: Bearer token in Authorization header (app_) + * + * Returns: { + * link: { lnurl, unique_hash, id, ... } + * } + */ + private async handleCreateWithdrawLink(req: HttpRequest): Promise { + try { + const { title, min_withdrawable, max_withdrawable, uses, wait_time } = req.body + + // Extract app_id from Authorization header (Bearer app_) + const authHeader = req.headers?.authorization || req.headers?.Authorization || '' + let app_id = 'default' + if (authHeader.startsWith('Bearer app_')) { + app_id = authHeader.replace('Bearer app_', '') + } + + if (!title || !min_withdrawable) { + return { + status: 400, + body: { status: 'ERROR', reason: 'Missing required fields: title, min_withdrawable' }, + headers: { 'Content-Type': 'application/json' } + } + } + + const link = await this.manager.create(app_id, { + title, + min_withdrawable, + max_withdrawable: max_withdrawable || min_withdrawable, + uses: uses || 1, + wait_time: wait_time || 0, + is_unique: false // Simple single-use links for ATM + }) + + // Return in format expected by ATM client + return { + status: 200, + body: { + status: 'OK', + link: { + lnurl: link.lnurl, + unique_hash: link.unique_hash, + id: link.id, + title: link.title, + min_withdrawable: link.min_withdrawable, + max_withdrawable: link.max_withdrawable, + uses: link.uses, + used: link.used + } + }, + headers: { 'Content-Type': 'application/json' } + } + } catch (error: any) { + return { + status: 500, + body: { status: 'ERROR', reason: error.message }, + headers: { 'Content-Type': 'application/json' } + } + } + } + + /** + * Handle initial LNURL request (simple link) + * GET /api/v1/lnurl/:unique_hash + */ + private async handleLnurlRequest(req: HttpRequest): Promise { + const { unique_hash } = req.params + + const result = await this.manager.handleLnurlRequest(unique_hash) + + return { + status: 200, + body: result, + headers: { + 'Content-Type': 'application/json' + } + } + } + + /** + * Handle initial LNURL request (unique link) + * GET /api/v1/lnurl/:unique_hash/:id_unique_hash + */ + private async handleLnurlUniqueRequest(req: HttpRequest): Promise { + const { unique_hash, id_unique_hash } = req.params + + const result = await this.manager.handleLnurlRequest(unique_hash, id_unique_hash) + + return { + status: 200, + body: result, + headers: { + 'Content-Type': 'application/json' + } + } + } + + /** + * Handle LNURL callback (user submits invoice) + * GET /api/v1/lnurl/cb/:unique_hash?k1=...&pr=...&id_unique_hash=... + */ + private async handleLnurlCallback(req: HttpRequest): Promise { + const { unique_hash } = req.params + const { k1, pr, id_unique_hash } = req.query + + if (!k1 || !pr) { + return { + status: 200, + body: { status: 'ERROR', reason: 'Missing k1 or pr parameter' }, + headers: { 'Content-Type': 'application/json' } + } + } + + const result = await this.manager.handleLnurlCallback(unique_hash, { + k1, + pr, + id_unique_hash + }) + + return { + status: 200, + body: result, + headers: { + 'Content-Type': 'application/json' + } + } + } +} + +// Export types for external use +export * from './types.js' +export { WithdrawManager } from './managers/withdrawManager.js' diff --git a/src/extensions/withdraw/managers/withdrawManager.ts b/src/extensions/withdraw/managers/withdrawManager.ts new file mode 100644 index 00000000..5f76008e --- /dev/null +++ b/src/extensions/withdraw/managers/withdrawManager.ts @@ -0,0 +1,717 @@ +/** + * Withdraw Link Manager + * + * Handles CRUD operations for withdraw links and processes withdrawals + */ + +import { + ExtensionContext, + ExtensionDatabase, + WithdrawLink, + Withdrawal, + CreateWithdrawLinkRequest, + UpdateWithdrawLinkRequest, + WithdrawLinkWithLnurl, + LnurlWithdrawResponse, + LnurlErrorResponse, + LnurlSuccessResponse, + LnurlCallbackParams +} from '../types.js' +import { + generateId, + generateK1, + generateUniqueHash, + generateUseHash, + verifyUseHash, + encodeLnurl, + buildLnurlUrl, + buildUniqueLnurlUrl, + buildCallbackUrl, + satsToMsats +} from '../utils/lnurl.js' + +/** + * Database row types + */ +interface WithdrawLinkRow { + id: string + application_id: string + title: string + description: string | null + min_withdrawable: number + max_withdrawable: number + uses: number + used: number + wait_time: number + unique_hash: string + k1: string + is_unique: number + uses_csv: string + open_time: number + creator_pubkey: string | null + webhook_url: string | null + webhook_headers: string | null + webhook_body: string | null + created_at: number + updated_at: number +} + +interface WithdrawalRow { + id: string + link_id: string + application_id: string + payment_hash: string + amount_sats: number + fee_sats: number + recipient_node: string | null + webhook_success: number | null + webhook_response: string | null + created_at: number +} + +/** + * Convert row to WithdrawLink + */ +function rowToLink(row: WithdrawLinkRow): WithdrawLink { + return { + id: row.id, + application_id: row.application_id, + title: row.title, + description: row.description || undefined, + min_withdrawable: row.min_withdrawable, + max_withdrawable: row.max_withdrawable, + uses: row.uses, + used: row.used, + wait_time: row.wait_time, + unique_hash: row.unique_hash, + k1: row.k1, + is_unique: row.is_unique === 1, + uses_csv: row.uses_csv, + open_time: row.open_time, + creator_pubkey: row.creator_pubkey || undefined, + webhook_url: row.webhook_url || undefined, + webhook_headers: row.webhook_headers || undefined, + webhook_body: row.webhook_body || undefined, + created_at: row.created_at, + updated_at: row.updated_at + } +} + +/** + * Convert row to Withdrawal + */ +function rowToWithdrawal(row: WithdrawalRow): Withdrawal { + return { + id: row.id, + link_id: row.link_id, + application_id: row.application_id, + payment_hash: row.payment_hash, + amount_sats: row.amount_sats, + fee_sats: row.fee_sats, + recipient_node: row.recipient_node || undefined, + webhook_success: row.webhook_success === null ? undefined : row.webhook_success === 1, + webhook_response: row.webhook_response || undefined, + created_at: row.created_at + } +} + +/** + * WithdrawManager - Handles withdraw link operations + */ +export class WithdrawManager { + private baseUrl: string = '' + + constructor( + private db: ExtensionDatabase, + private ctx: ExtensionContext + ) {} + + /** + * Set the base URL for LNURL generation + */ + setBaseUrl(url: string): void { + this.baseUrl = url.replace(/\/$/, '') + } + + /** + * Add LNURL to a withdraw link + */ + private addLnurl(link: WithdrawLink): WithdrawLinkWithLnurl { + const lnurlUrl = buildLnurlUrl(this.baseUrl, link.unique_hash) + return { + ...link, + lnurl: encodeLnurl(lnurlUrl), + lnurl_url: lnurlUrl + } + } + + // ========================================================================= + // CRUD Operations + // ========================================================================= + + /** + * Create a new withdraw link + */ + async create(applicationId: string, req: CreateWithdrawLinkRequest, creatorPubkey?: string): Promise { + // Validation + if (req.uses < 1 || req.uses > 250) { + throw new Error('Uses must be between 1 and 250') + } + if (req.min_withdrawable < 1) { + throw new Error('Min withdrawable must be at least 1 sat') + } + if (req.max_withdrawable < req.min_withdrawable) { + throw new Error('Max withdrawable must be >= min withdrawable') + } + if (req.wait_time < 0) { + throw new Error('Wait time cannot be negative') + } + + // Validate webhook JSON if provided + if (req.webhook_headers) { + try { + JSON.parse(req.webhook_headers) + } catch { + throw new Error('webhook_headers must be valid JSON') + } + } + if (req.webhook_body) { + try { + JSON.parse(req.webhook_body) + } catch { + throw new Error('webhook_body must be valid JSON') + } + } + + const now = Math.floor(Date.now() / 1000) + const id = generateId() + const usesCsv = Array.from({ length: req.uses }, (_, i) => String(i)).join(',') + + const link: WithdrawLink = { + id, + application_id: applicationId, + title: req.title.trim(), + description: req.description?.trim(), + min_withdrawable: req.min_withdrawable, + max_withdrawable: req.max_withdrawable, + uses: req.uses, + used: 0, + wait_time: req.wait_time, + unique_hash: generateUniqueHash(), + k1: generateK1(), + is_unique: req.is_unique || false, + uses_csv: usesCsv, + open_time: now, + creator_pubkey: creatorPubkey, + webhook_url: req.webhook_url, + webhook_headers: req.webhook_headers, + webhook_body: req.webhook_body, + created_at: now, + updated_at: now + } + + await this.db.execute( + `INSERT INTO withdraw_links ( + id, application_id, title, description, + min_withdrawable, max_withdrawable, uses, used, wait_time, + unique_hash, k1, is_unique, uses_csv, open_time, + creator_pubkey, + webhook_url, webhook_headers, webhook_body, + created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [ + link.id, link.application_id, link.title, link.description || null, + link.min_withdrawable, link.max_withdrawable, link.uses, link.used, link.wait_time, + link.unique_hash, link.k1, link.is_unique ? 1 : 0, link.uses_csv, link.open_time, + link.creator_pubkey || null, + link.webhook_url || null, link.webhook_headers || null, link.webhook_body || null, + link.created_at, link.updated_at + ] + ) + + return this.addLnurl(link) + } + + /** + * Create multiple vouchers (single-use withdraw links) + */ + async createVouchers( + applicationId: string, + title: string, + amount: number, + count: number, + description?: string + ): Promise { + if (count < 1 || count > 100) { + throw new Error('Count must be between 1 and 100') + } + if (amount < 1) { + throw new Error('Amount must be at least 1 sat') + } + + const vouchers: WithdrawLinkWithLnurl[] = [] + + for (let i = 0; i < count; i++) { + const voucher = await this.create(applicationId, { + title: `${title} #${i + 1}`, + description, + min_withdrawable: amount, + max_withdrawable: amount, + uses: 1, + wait_time: 0, + is_unique: false + }) + vouchers.push(voucher) + } + + return vouchers + } + + /** + * Get a withdraw link by ID + */ + async get(id: string, applicationId: string): Promise { + const rows = await this.db.query( + 'SELECT * FROM withdraw_links WHERE id = ? AND application_id = ?', + [id, applicationId] + ) + + if (rows.length === 0) return null + return this.addLnurl(rowToLink(rows[0])) + } + + /** + * Get a withdraw link by unique hash (for LNURL) + */ + async getByHash(uniqueHash: string): Promise { + const rows = await this.db.query( + 'SELECT * FROM withdraw_links WHERE unique_hash = ?', + [uniqueHash] + ) + + if (rows.length === 0) return null + return rowToLink(rows[0]) + } + + /** + * List withdraw links for an application + */ + async list( + applicationId: string, + includeSpent: boolean = false, + limit?: number, + offset?: number + ): Promise { + let sql = 'SELECT * FROM withdraw_links WHERE application_id = ?' + const params: any[] = [applicationId] + + if (!includeSpent) { + sql += ' AND used < uses' + } + + sql += ' ORDER BY created_at DESC' + + if (limit) { + sql += ' LIMIT ?' + params.push(limit) + if (offset) { + sql += ' OFFSET ?' + params.push(offset) + } + } + + const rows = await this.db.query(sql, params) + return rows.map(row => this.addLnurl(rowToLink(row))) + } + + /** + * Update a withdraw link + */ + async update( + id: string, + applicationId: string, + req: UpdateWithdrawLinkRequest + ): Promise { + const existing = await this.get(id, applicationId) + if (!existing) return null + + // Validation + if (req.uses !== undefined) { + if (req.uses < 1 || req.uses > 250) { + throw new Error('Uses must be between 1 and 250') + } + if (req.uses < existing.used) { + throw new Error('Cannot reduce uses below current used count') + } + } + + const minWith = req.min_withdrawable ?? existing.min_withdrawable + const maxWith = req.max_withdrawable ?? existing.max_withdrawable + + if (minWith < 1) { + throw new Error('Min withdrawable must be at least 1 sat') + } + if (maxWith < minWith) { + throw new Error('Max withdrawable must be >= min withdrawable') + } + + // Handle uses change + let usesCsv = existing.uses_csv + const newUses = req.uses ?? existing.uses + if (newUses !== existing.uses) { + const currentUses = usesCsv.split(',').filter(u => u !== '') + if (newUses > existing.uses) { + // Add more uses + const lastNum = currentUses.length > 0 ? parseInt(currentUses[currentUses.length - 1], 10) : -1 + for (let i = lastNum + 1; currentUses.length < (newUses - existing.used); i++) { + currentUses.push(String(i)) + } + } else { + // Remove uses (keep first N) + usesCsv = currentUses.slice(0, newUses - existing.used).join(',') + } + usesCsv = currentUses.join(',') + } + + const now = Math.floor(Date.now() / 1000) + + await this.db.execute( + `UPDATE withdraw_links SET + title = ?, description = ?, + min_withdrawable = ?, max_withdrawable = ?, + uses = ?, wait_time = ?, is_unique = ?, uses_csv = ?, + webhook_url = ?, webhook_headers = ?, webhook_body = ?, + updated_at = ? + WHERE id = ? AND application_id = ?`, + [ + req.title ?? existing.title, + req.description ?? existing.description ?? null, + minWith, maxWith, + newUses, + req.wait_time ?? existing.wait_time, + (req.is_unique ?? existing.is_unique) ? 1 : 0, + usesCsv, + req.webhook_url ?? existing.webhook_url ?? null, + req.webhook_headers ?? existing.webhook_headers ?? null, + req.webhook_body ?? existing.webhook_body ?? null, + now, + id, applicationId + ] + ) + + return this.get(id, applicationId) + } + + /** + * Delete a withdraw link + */ + async delete(id: string, applicationId: string): Promise { + const result = await this.db.execute( + 'DELETE FROM withdraw_links WHERE id = ? AND application_id = ?', + [id, applicationId] + ) + return (result.changes || 0) > 0 + } + + // ========================================================================= + // LNURL Protocol Handlers + // ========================================================================= + + /** + * Handle initial LNURL request (user scans QR) + * Returns withdraw parameters + */ + async handleLnurlRequest( + uniqueHash: string, + idUniqueHash?: string + ): Promise { + const link = await this.getByHash(uniqueHash) + + if (!link) { + return { status: 'ERROR', reason: 'Withdraw link does not exist.' } + } + + if (link.used >= link.uses) { + return { status: 'ERROR', reason: 'Withdraw link is spent.' } + } + + // For unique links, require id_unique_hash + if (link.is_unique && !idUniqueHash) { + return { status: 'ERROR', reason: 'This link requires a unique hash.' } + } + + // Verify unique hash if provided + if (idUniqueHash) { + const useNumber = verifyUseHash(link.id, link.unique_hash, link.uses_csv, idUniqueHash) + if (!useNumber) { + return { status: 'ERROR', reason: 'Invalid unique hash.' } + } + } + + const callbackUrl = buildCallbackUrl(this.baseUrl, link.unique_hash) + + return { + tag: 'withdrawRequest', + callback: idUniqueHash ? `${callbackUrl}?id_unique_hash=${idUniqueHash}` : callbackUrl, + k1: link.k1, + minWithdrawable: satsToMsats(link.min_withdrawable), + maxWithdrawable: satsToMsats(link.max_withdrawable), + defaultDescription: link.title + } + } + + /** + * Handle LNURL callback (user submits invoice) + * Pays the invoice and records the withdrawal + */ + async handleLnurlCallback( + uniqueHash: string, + params: LnurlCallbackParams + ): Promise { + const link = await this.getByHash(uniqueHash) + + if (!link) { + return { status: 'ERROR', reason: 'Withdraw link not found.' } + } + + if (link.used >= link.uses) { + return { status: 'ERROR', reason: 'Withdraw link is spent.' } + } + + if (link.k1 !== params.k1) { + return { status: 'ERROR', reason: 'Invalid k1.' } + } + + // Check wait time + const now = Math.floor(Date.now() / 1000) + if (now < link.open_time) { + const waitSecs = link.open_time - now + return { status: 'ERROR', reason: `Please wait ${waitSecs} seconds.` } + } + + // For unique links, verify and consume the use hash + if (params.id_unique_hash) { + const useNumber = verifyUseHash(link.id, link.unique_hash, link.uses_csv, params.id_unique_hash) + if (!useNumber) { + return { status: 'ERROR', reason: 'Invalid unique hash.' } + } + } else if (link.is_unique) { + return { status: 'ERROR', reason: 'Unique hash required.' } + } + + // Prevent double-spending with hash check + try { + await this.createHashCheck(params.id_unique_hash || uniqueHash, params.k1) + } catch { + return { status: 'ERROR', reason: 'Withdrawal already in progress.' } + } + + try { + // Pay the invoice from the creator's balance (if created via Nostr RPC) + const payment = await this.ctx.payInvoice( + link.application_id, + params.pr, + link.max_withdrawable, + link.creator_pubkey + ) + + // Record the withdrawal + await this.recordWithdrawal(link, payment.paymentHash, link.max_withdrawable, payment.feeSats) + + // Increment usage + await this.incrementUsage(link, params.id_unique_hash) + + // Clean up hash check + await this.deleteHashCheck(params.id_unique_hash || uniqueHash) + + // Dispatch webhook if configured + if (link.webhook_url) { + this.dispatchWebhook(link, payment.paymentHash, params.pr).catch(err => { + console.error('[Withdraw] Webhook error:', err) + }) + } + + return { status: 'OK' } + } catch (err: any) { + // Clean up hash check on failure + await this.deleteHashCheck(params.id_unique_hash || uniqueHash) + return { status: 'ERROR', reason: `Payment failed: ${err.message}` } + } + } + + // ========================================================================= + // Helper Methods + // ========================================================================= + + /** + * Increment link usage and update open_time + */ + private async incrementUsage(link: WithdrawLink, idUniqueHash?: string): Promise { + const now = Math.floor(Date.now() / 1000) + let usesCsv = link.uses_csv + + // Remove used hash from uses_csv if unique + if (idUniqueHash) { + const uses = usesCsv.split(',').filter(u => { + const hash = generateUseHash(link.id, link.unique_hash, u.trim()) + return hash !== idUniqueHash + }) + usesCsv = uses.join(',') + } + + await this.db.execute( + `UPDATE withdraw_links SET + used = used + 1, + open_time = ?, + uses_csv = ?, + updated_at = ? + WHERE id = ?`, + [now + link.wait_time, usesCsv, now, link.id] + ) + } + + /** + * Record a successful withdrawal + */ + private async recordWithdrawal( + link: WithdrawLink, + paymentHash: string, + amountSats: number, + feeSats: number + ): Promise { + const now = Math.floor(Date.now() / 1000) + + await this.db.execute( + `INSERT INTO withdrawals ( + id, link_id, application_id, + payment_hash, amount_sats, fee_sats, + created_at + ) VALUES (?, ?, ?, ?, ?, ?, ?)`, + [ + generateId(), + link.id, + link.application_id, + paymentHash, + amountSats, + feeSats, + now + ] + ) + } + + /** + * Create hash check to prevent double-spending + */ + private async createHashCheck(hash: string, k1: string): Promise { + const now = Math.floor(Date.now() / 1000) + await this.db.execute( + 'INSERT INTO hash_checks (hash, k1, created_at) VALUES (?, ?, ?)', + [hash, k1, now] + ) + } + + /** + * Delete hash check after completion + */ + private async deleteHashCheck(hash: string): Promise { + await this.db.execute('DELETE FROM hash_checks WHERE hash = ?', [hash]) + } + + /** + * List withdrawals + */ + async listWithdrawals( + applicationId: string, + linkId?: string, + limit?: number, + offset?: number + ): Promise { + let sql = 'SELECT * FROM withdrawals WHERE application_id = ?' + const params: any[] = [applicationId] + + if (linkId) { + sql += ' AND link_id = ?' + params.push(linkId) + } + + sql += ' ORDER BY created_at DESC' + + if (limit) { + sql += ' LIMIT ?' + params.push(limit) + if (offset) { + sql += ' OFFSET ?' + params.push(offset) + } + } + + const rows = await this.db.query(sql, params) + return rows.map(rowToWithdrawal) + } + + /** + * Get withdrawal stats for a link + */ + async getWithdrawalStats(linkId: string): Promise<{ total_sats: number; count: number }> { + const result = await this.db.query<{ total: number; count: number }>( + `SELECT COALESCE(SUM(amount_sats), 0) as total, COUNT(*) as count + FROM withdrawals WHERE link_id = ?`, + [linkId] + ) + return { + total_sats: result[0]?.total || 0, + count: result[0]?.count || 0 + } + } + + /** + * Dispatch webhook notification + */ + private async dispatchWebhook( + link: WithdrawLink, + paymentHash: string, + paymentRequest: string + ): Promise { + if (!link.webhook_url) return + + try { + const headers: Record = { + 'Content-Type': 'application/json' + } + + if (link.webhook_headers) { + Object.assign(headers, JSON.parse(link.webhook_headers)) + } + + const body = { + payment_hash: paymentHash, + payment_request: paymentRequest, + lnurlw: link.id, + body: link.webhook_body ? JSON.parse(link.webhook_body) : {} + } + + const response = await fetch(link.webhook_url, { + method: 'POST', + headers, + body: JSON.stringify(body) + }) + + // Update withdrawal record with webhook result + await this.db.execute( + `UPDATE withdrawals SET + webhook_success = ?, + webhook_response = ? + WHERE payment_hash = ?`, + [response.ok ? 1 : 0, await response.text(), paymentHash] + ) + } catch (err: any) { + await this.db.execute( + `UPDATE withdrawals SET + webhook_success = 0, + webhook_response = ? + WHERE payment_hash = ?`, + [err.message, paymentHash] + ) + } + } +} diff --git a/src/extensions/withdraw/migrations.ts b/src/extensions/withdraw/migrations.ts new file mode 100644 index 00000000..1625638a --- /dev/null +++ b/src/extensions/withdraw/migrations.ts @@ -0,0 +1,164 @@ +/** + * LNURL-withdraw Extension Database Migrations + */ + +import { ExtensionDatabase } from '../types.js' + +export interface Migration { + version: number + name: string + up: (db: ExtensionDatabase) => Promise + down?: (db: ExtensionDatabase) => Promise +} + +export const migrations: Migration[] = [ + { + version: 1, + name: 'create_withdraw_links_table', + up: async (db: ExtensionDatabase) => { + await db.execute(` + CREATE TABLE IF NOT EXISTS withdraw_links ( + id TEXT PRIMARY KEY, + application_id TEXT NOT NULL, + + -- Display + title TEXT NOT NULL, + description TEXT, + + -- Amounts (sats) + min_withdrawable INTEGER NOT NULL, + max_withdrawable INTEGER NOT NULL, + + -- Usage limits + uses INTEGER NOT NULL DEFAULT 1, + used INTEGER NOT NULL DEFAULT 0, + wait_time INTEGER NOT NULL DEFAULT 0, + + -- Security + unique_hash TEXT NOT NULL UNIQUE, + k1 TEXT NOT NULL, + is_unique INTEGER NOT NULL DEFAULT 0, + uses_csv TEXT NOT NULL DEFAULT '', + + -- Rate limiting + open_time INTEGER NOT NULL DEFAULT 0, + + -- Webhooks + webhook_url TEXT, + webhook_headers TEXT, + webhook_body TEXT, + + -- Timestamps + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + ) + `) + + // Index for looking up by unique_hash (LNURL) + await db.execute(` + CREATE INDEX IF NOT EXISTS idx_withdraw_links_unique_hash + ON withdraw_links(unique_hash) + `) + + // Index for listing by application + await db.execute(` + CREATE INDEX IF NOT EXISTS idx_withdraw_links_application + ON withdraw_links(application_id, created_at DESC) + `) + } + }, + { + version: 2, + name: 'create_withdrawals_table', + up: async (db: ExtensionDatabase) => { + await db.execute(` + CREATE TABLE IF NOT EXISTS withdrawals ( + id TEXT PRIMARY KEY, + link_id TEXT NOT NULL, + application_id TEXT NOT NULL, + + -- Payment details + payment_hash TEXT NOT NULL, + amount_sats INTEGER NOT NULL, + fee_sats INTEGER NOT NULL DEFAULT 0, + + -- Recipient + recipient_node TEXT, + + -- Webhook result + webhook_success INTEGER, + webhook_response TEXT, + + -- Timestamp + created_at INTEGER NOT NULL, + + FOREIGN KEY (link_id) REFERENCES withdraw_links(id) ON DELETE CASCADE + ) + `) + + // Index for listing withdrawals by link + await db.execute(` + CREATE INDEX IF NOT EXISTS idx_withdrawals_link + ON withdrawals(link_id, created_at DESC) + `) + + // Index for looking up by payment hash + await db.execute(` + CREATE INDEX IF NOT EXISTS idx_withdrawals_payment_hash + ON withdrawals(payment_hash) + `) + } + }, + { + version: 3, + name: 'create_hash_checks_table', + up: async (db: ExtensionDatabase) => { + // Temporary table to prevent double-spending during payment processing + await db.execute(` + CREATE TABLE IF NOT EXISTS hash_checks ( + hash TEXT PRIMARY KEY, + k1 TEXT NOT NULL, + created_at INTEGER NOT NULL + ) + `) + } + }, + { + version: 4, + name: 'add_creator_pubkey_column', + up: async (db: ExtensionDatabase) => { + // Store the Nostr pubkey of the user who created the withdraw link + // so that when the LNURL callback fires, we debit the correct user's balance + await db.execute(` + ALTER TABLE withdraw_links ADD COLUMN creator_pubkey TEXT + `) + } + } +] + +/** + * Run all pending migrations + */ +export async function runMigrations(db: ExtensionDatabase): Promise { + // Get current version + const versionResult = await db.query<{ value: string }>( + `SELECT value FROM _extension_meta WHERE key = 'migration_version'` + ).catch(() => []) + + const currentVersion = versionResult.length > 0 ? parseInt(versionResult[0].value, 10) : 0 + + // Run pending migrations + for (const migration of migrations) { + if (migration.version > currentVersion) { + console.log(`[Withdraw] Running migration ${migration.version}: ${migration.name}`) + await migration.up(db) + + // Update version + await db.execute( + `INSERT INTO _extension_meta (key, value) VALUES ('migration_version', ?) + ON CONFLICT(key) DO UPDATE SET value = excluded.value`, + [String(migration.version)] + ) + } + } +} diff --git a/src/extensions/withdraw/types.ts b/src/extensions/withdraw/types.ts new file mode 100644 index 00000000..88d25f33 --- /dev/null +++ b/src/extensions/withdraw/types.ts @@ -0,0 +1,264 @@ +/** + * LNURL-withdraw Extension Types + * Implements LUD-03 (LNURL-withdraw) for Lightning.Pub + */ + +// Re-export base extension types +export { + Extension, + ExtensionInfo, + ExtensionContext, + ExtensionDatabase, + ApplicationInfo, + RpcMethodHandler +} from '../types.js' + +// ============================================================================ +// Core Data Types +// ============================================================================ + +/** + * A withdraw link that can be used to pull funds + */ +export interface WithdrawLink { + id: string + application_id: string + + // Display + title: string + description?: string + + // Amounts (in sats) + min_withdrawable: number + max_withdrawable: number + + // Usage limits + uses: number // Total allowed uses + used: number // Times used so far + wait_time: number // Seconds between uses + + // Security + unique_hash: string // For LNURL URL + k1: string // Challenge for callback + is_unique: boolean // Generate unique code per use + uses_csv: string // Comma-separated list of available use IDs + + // Rate limiting + open_time: number // Unix timestamp when next use is allowed + + // Creator identity (for Nostr RPC-created links) + creator_pubkey?: string // Nostr pubkey of the user who created this link + + // Webhook notifications + webhook_url?: string + webhook_headers?: string // JSON string + webhook_body?: string // JSON string + + // Timestamps + created_at: number + updated_at: number +} + +/** + * Withdrawal record - tracks each successful withdrawal + */ +export interface Withdrawal { + id: string + link_id: string + application_id: string + + // Payment details + payment_hash: string + amount_sats: number + fee_sats: number + + // Recipient (if known) + recipient_node?: string + + // Webhook result + webhook_success?: boolean + webhook_response?: string + + // Timestamp + created_at: number +} + +/** + * Hash check - prevents double-spending during payment + */ +export interface HashCheck { + hash: string + k1: string + created_at: number +} + +// ============================================================================ +// LNURL Protocol Types (LUD-03) +// ============================================================================ + +/** + * LNURL-withdraw response (first call) + * Returned when user scans the QR code + */ +export interface LnurlWithdrawResponse { + tag: 'withdrawRequest' + callback: string // URL to call with invoice + k1: string // Challenge + minWithdrawable: number // Millisats + maxWithdrawable: number // Millisats + defaultDescription: string +} + +/** + * LNURL error response + */ +export interface LnurlErrorResponse { + status: 'ERROR' + reason: string +} + +/** + * LNURL success response + */ +export interface LnurlSuccessResponse { + status: 'OK' +} + +// ============================================================================ +// RPC Request/Response Types +// ============================================================================ + +/** + * Create a new withdraw link + */ +export interface CreateWithdrawLinkRequest { + title: string + description?: string + min_withdrawable: number // sats + max_withdrawable: number // sats + uses: number // 1-250 + wait_time: number // seconds between uses + is_unique?: boolean // generate unique code per use + webhook_url?: string + webhook_headers?: string // JSON + webhook_body?: string // JSON +} + +/** + * Update an existing withdraw link + */ +export interface UpdateWithdrawLinkRequest { + id: string + title?: string + description?: string + min_withdrawable?: number + max_withdrawable?: number + uses?: number + wait_time?: number + is_unique?: boolean + webhook_url?: string + webhook_headers?: string + webhook_body?: string +} + +/** + * Get withdraw link by ID + */ +export interface GetWithdrawLinkRequest { + id: string +} + +/** + * List withdraw links + */ +export interface ListWithdrawLinksRequest { + include_spent?: boolean // Include fully used links + limit?: number + offset?: number +} + +/** + * Delete withdraw link + */ +export interface DeleteWithdrawLinkRequest { + id: string +} + +/** + * Create quick vouchers (batch of single-use links) + */ +export interface CreateVouchersRequest { + title: string + amount: number // sats per voucher + count: number // number of vouchers (1-100) + description?: string +} + +/** + * Get withdraw link with LNURL + */ +export interface WithdrawLinkWithLnurl extends WithdrawLink { + lnurl: string // bech32 encoded LNURL + lnurl_url: string // raw callback URL +} + +/** + * List withdrawals for a link + */ +export interface ListWithdrawalsRequest { + link_id?: string + limit?: number + offset?: number +} + +/** + * Withdraw link response with stats + */ +export interface WithdrawLinkResponse { + link: WithdrawLinkWithLnurl + total_withdrawn_sats: number + withdrawals_count: number +} + +/** + * Vouchers response + */ +export interface VouchersResponse { + vouchers: WithdrawLinkWithLnurl[] + total_amount_sats: number +} + +// ============================================================================ +// HTTP Handler Types +// ============================================================================ + +/** + * LNURL callback parameters + */ +export interface LnurlCallbackParams { + k1: string // Challenge from initial response + pr: string // Payment request (BOLT11 invoice) + id_unique_hash?: string // For unique links +} + +/** + * HTTP route handler + */ +export interface HttpRoute { + method: 'GET' | 'POST' + path: string + handler: (req: HttpRequest) => Promise +} + +export interface HttpRequest { + params: Record + query: Record + body?: any + headers: Record +} + +export interface HttpResponse { + status: number + body: any + headers?: Record +} diff --git a/src/extensions/withdraw/utils/lnurl.ts b/src/extensions/withdraw/utils/lnurl.ts new file mode 100644 index 00000000..96926c52 --- /dev/null +++ b/src/extensions/withdraw/utils/lnurl.ts @@ -0,0 +1,131 @@ +/** + * LNURL Encoding Utilities + * + * LNURL is a bech32-encoded URL with hrp "lnurl" + * See: https://github.com/lnurl/luds + */ + +import { bech32 } from 'bech32' +import crypto from 'crypto' + +/** + * Encode a URL as LNURL (bech32) + */ +export function encodeLnurl(url: string): string { + const words = bech32.toWords(Buffer.from(url, 'utf8')) + return bech32.encode('lnurl', words, 2000) // 2000 char limit for URLs +} + +/** + * Decode an LNURL to a URL + */ +export function decodeLnurl(lnurl: string): string { + const { prefix, words } = bech32.decode(lnurl, 2000) + if (prefix !== 'lnurl') { + throw new Error('Invalid LNURL prefix') + } + return Buffer.from(bech32.fromWords(words)).toString('utf8') +} + +/** + * Generate a URL-safe random ID + */ +export function generateId(length: number = 22): string { + const bytes = crypto.randomBytes(Math.ceil(length * 3 / 4)) + return bytes.toString('base64url').slice(0, length) +} + +/** + * Generate a k1 challenge (32 bytes hex) + */ +export function generateK1(): string { + return crypto.randomBytes(32).toString('hex') +} + +/** + * Generate a unique hash for a link + */ +export function generateUniqueHash(): string { + return generateId(32) +} + +/** + * Generate a unique hash for a specific use of a link + * This creates a deterministic hash based on link ID, unique_hash, and use number + */ +export function generateUseHash(linkId: string, uniqueHash: string, useNumber: string): string { + const data = `${linkId}${uniqueHash}${useNumber}` + return crypto.createHash('sha256').update(data).digest('hex').slice(0, 32) +} + +/** + * Verify a use hash matches one of the available uses + */ +export function verifyUseHash( + linkId: string, + uniqueHash: string, + usesCsv: string, + providedHash: string +): string | null { + const uses = usesCsv.split(',').filter(u => u.trim() !== '') + + for (const useNumber of uses) { + const expectedHash = generateUseHash(linkId, uniqueHash, useNumber.trim()) + if (expectedHash === providedHash) { + return useNumber.trim() + } + } + + return null +} + +/** + * Build the LNURL callback URL for a withdraw link + */ +export function buildLnurlUrl(baseUrl: string, uniqueHash: string): string { + // Remove trailing slash from baseUrl + const base = baseUrl.replace(/\/$/, '') + return `${base}/api/v1/lnurl/${uniqueHash}` +} + +/** + * Build the LNURL callback URL for a unique withdraw link + */ +export function buildUniqueLnurlUrl( + baseUrl: string, + uniqueHash: string, + useHash: string +): string { + const base = baseUrl.replace(/\/$/, '') + return `${base}/api/v1/lnurl/${uniqueHash}/${useHash}` +} + +/** + * Build the callback URL for the second step (where user sends invoice) + */ +export function buildCallbackUrl(baseUrl: string, uniqueHash: string): string { + const base = baseUrl.replace(/\/$/, '') + return `${base}/api/v1/lnurl/cb/${uniqueHash}` +} + +/** + * Sats to millisats + */ +export function satsToMsats(sats: number): number { + return sats * 1000 +} + +/** + * Millisats to sats + */ +export function msatsToSats(msats: number): number { + return Math.floor(msats / 1000) +} + +/** + * Validate a BOLT11 invoice (basic check) + */ +export function isValidBolt11(invoice: string): boolean { + const lower = invoice.toLowerCase() + return lower.startsWith('lnbc') || lower.startsWith('lntb') || lower.startsWith('lnbcrt') +} diff --git a/src/index.ts b/src/index.ts index fbe6802c..5fbb21dc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,8 @@ import 'dotenv/config' +import express from 'express' +import cors from 'cors' +import path from 'path' +import { fileURLToPath } from 'url' import NewServer from '../proto/autogenerated/ts/express_server.js' import GetServerMethods from './services/serverMethods/index.js' import serverOptions from './auth.js'; @@ -8,9 +12,15 @@ import { initMainHandler, initSettings } from './services/main/init.js'; import { nip19 } from 'nostr-tools' import { LoadStorageSettingsFromEnv } from './services/storage/index.js'; import { AppInfo } from './services/nostr/nostrPool.js'; +import { createExtensionLoader, ExtensionLoader } from './extensions/loader.js' +import { createMainHandlerAdapter } from './extensions/mainHandlerAdapter.js' +import type { HttpRoute } from './extensions/withdraw/types.js' //@ts-ignore const { nprofileEncode } = nip19 +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + const start = async () => { const log = getLogger({}) @@ -25,6 +35,42 @@ const start = async () => { const { mainHandler, localProviderClient, wizard, adminManager } = keepOn const serverMethods = GetServerMethods(mainHandler) + + // Initialize extension system BEFORE nostrMiddleware so RPC methods are available + let extensionLoader: ExtensionLoader | null = null + const mainPort = settingsManager.getSettings().serviceSettings.servicePort + const extensionPort = mainPort + 1 + + // Extension routes run on a separate port (main port + 1) + // SERVICE_URL for extensions should point to this port for LNURL to work + // In production, use a reverse proxy to route /api/v1/lnurl/* to extension port + const extensionServiceUrl = process.env.EXTENSION_SERVICE_URL || `http://localhost:${extensionPort}` + + try { + log("initializing extension system") + const extensionsDir = path.join(__dirname, 'extensions') + const databaseDir = path.join(__dirname, '..', 'data', 'extensions') + + const mainHandlerAdapter = createMainHandlerAdapter(mainHandler) + extensionLoader = createExtensionLoader( + { extensionsDir, databaseDir }, + mainHandlerAdapter + ) + + await extensionLoader.loadAll() + log(`loaded ${extensionLoader.getAllExtensions().length} extension(s)`) + + // Set base URL for LNURL generation on withdraw extension + const withdrawExt = extensionLoader.getExtension('withdraw') + if (withdrawExt && withdrawExt.instance && 'setBaseUrl' in withdrawExt.instance) { + (withdrawExt.instance as any).setBaseUrl(extensionServiceUrl) + log(`withdraw extension base URL set to ${extensionServiceUrl}`) + } + } catch (e) { + log(`extension system initialization failed: ${e}`) + } + + // Initialize nostr middleware with extension loader for RPC routing log("initializing nostr middleware") const relays = settingsManager.getSettings().nostrRelaySettings.relays const maxEventContentLength = settingsManager.getSettings().nostrRelaySettings.maxEventContentLength @@ -45,7 +91,8 @@ const start = async () => { { relays, maxEventContentLength, apps }, - (e, p) => mainHandler.liquidityProvider.onEvent(e, p) + (e, p) => mainHandler.liquidityProvider.onEvent(e, p), + { extensionLoader: extensionLoader || undefined } ) exitHandler(() => { Stop(); mainHandler.Stop() }) log("starting server") @@ -58,8 +105,58 @@ const start = async () => { wizard.AddConnectInfo(appNprofile, relays) } adminManager.setAppNprofile(appNprofile) + + // Create Express app for extension HTTP routes + const extensionApp = express() + extensionApp.use(cors()) // Enable CORS for all origins (ATM apps, wallets, etc.) + extensionApp.use(express.json()) + + // Mount extension HTTP routes + if (extensionLoader) { + for (const ext of extensionLoader.getAllExtensions()) { + if (ext.status === 'ready' && 'getHttpRoutes' in ext.instance) { + const routes = (ext.instance as any).getHttpRoutes() as HttpRoute[] + for (const route of routes) { + log(`mounting extension route: ${route.method} ${route.path}`) + const handler = async (req: express.Request, res: express.Response) => { + try { + const httpReq = { + params: req.params, + query: req.query as Record, + body: req.body, + headers: req.headers as Record + } + const result = await route.handler(httpReq) + res.status(result.status) + if (result.headers) { + for (const [key, value] of Object.entries(result.headers)) { + res.setHeader(key, value) + } + } + res.json(result.body) + } catch (e: any) { + log(`extension route error: ${e.message}`) + res.status(500).json({ status: 'ERROR', reason: e.message }) + } + } + if (route.method === 'GET') { + extensionApp.get(route.path, handler) + } else if (route.method === 'POST') { + extensionApp.post(route.path, handler) + } + } + } + } + } + + // Start extension routes server + extensionApp.listen(extensionPort, () => { + log(`extension HTTP routes listening on port ${extensionPort}`) + }) + + // Start main proto server const Server = NewServer(serverMethods, serverOptions(mainHandler)) - Server.Listen(settingsManager.getSettings().serviceSettings.servicePort) + Server.Listen(mainPort) } start() diff --git a/src/nostrMiddleware.ts b/src/nostrMiddleware.ts index 034dbce8..a7131375 100644 --- a/src/nostrMiddleware.ts +++ b/src/nostrMiddleware.ts @@ -5,9 +5,15 @@ import * as Types from '../proto/autogenerated/ts/types.js' import NewNostrTransport, { NostrRequest } from '../proto/autogenerated/ts/nostr_transport.js'; import { ERROR, getLogger } from "./services/helpers/logger.js"; import { NdebitData, NofferData, NmanageRequest } from "@shocknet/clink-sdk"; +import type { ExtensionLoader } from "./extensions/loader.js" type ExportedCalls = { Stop: () => void, Send: NostrSend, Ping: () => Promise, Reset: (settings: NostrSettings) => void } type ClientEventCallback = (e: { requestId: string }, fromPub: string) => void -export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSettings: NostrSettings, onClientEvent: ClientEventCallback): ExportedCalls => { + +export type NostrMiddlewareOptions = { + extensionLoader?: ExtensionLoader +} + +export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSettings: NostrSettings, onClientEvent: ClientEventCallback, options?: NostrMiddlewareOptions): ExportedCalls => { const log = getLogger({}) const nostrTransport = NewNostrTransport(serverMethods, { NostrUserAuthGuard: async (appId, pub) => { @@ -95,6 +101,31 @@ export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSett log(ERROR, "authIdentifier does not match", j.authIdentifier || "--", event.pub) return } + + // Check if this is an extension RPC method + const extensionLoader = options?.extensionLoader + if (extensionLoader && j.rpcName && extensionLoader.hasMethod(j.rpcName)) { + // Route to extension + log(`[Nostr] Routing to extension method: ${j.rpcName}`) + extensionLoader.callMethod(j.rpcName, j.body || {}, event.appId, event.pub) + .then(result => { + const response = { status: 'OK', requestId: j.requestId, ...result } + nostr.Send( + { type: 'app', appId: event.appId }, + { type: 'content', pub: event.pub, content: JSON.stringify(response) } + ) + }) + .catch(err => { + log(ERROR, `Extension method ${j.rpcName} failed:`, err.message) + const response = { status: 'ERROR', requestId: j.requestId, reason: err.message } + nostr.Send( + { type: 'app', appId: event.appId }, + { type: 'content', pub: event.pub, content: JSON.stringify(response) } + ) + }) + return + } + nostrTransport({ ...j, appId: event.appId }, res => { nostr.Send({ type: 'app', appId: event.appId }, { type: 'content', pub: event.pub, content: JSON.stringify({ ...res, requestId: j.requestId }) }) }, event.startAtNano, event.startAtMs) @@ -105,7 +136,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/payInvoiceReq.ts b/src/services/lnd/payInvoiceReq.ts index 323448e0..29ab151e 100644 --- a/src/services/lnd/payInvoiceReq.ts +++ b/src/services/lnd/payInvoiceReq.ts @@ -9,7 +9,7 @@ export const PayInvoiceReq = (invoice: string, amount: number, feeLimit: number) maxParts: 3, timeoutSeconds: 50, - allowSelfPayment: false, + allowSelfPayment: true, amp: false, amtMsat: 0n, cltvLimit: 0, 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..a9c19638 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 }, } } @@ -239,6 +241,8 @@ export default class { const paid = await this.paymentManager.PayInvoice(appUser.user.user_id, req, app, { ack: pendingOp => { this.notifyAppUserPayment(appUser, pendingOp) } }) + // Refresh appUser balance from DB so notification has accurate latest_balance + appUser.user.balance_sats = paid.latest_balance this.notifyAppUserPayment(appUser, paid.operation) getLogger({ appName: app.name })(appUser.identifier, "invoice paid", paid.amount_paid, "sats") return paid 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/nip44v1.ts b/src/services/nostr/nip44v1.ts index 0f37f620..1eb18046 100644 --- a/src/services/nostr/nip44v1.ts +++ b/src/services/nostr/nip44v1.ts @@ -1,14 +1,14 @@ -import { base64 } from "@scure/base"; +import { base64, hex } from "@scure/base"; import { randomBytes } from "@noble/hashes/utils"; import { streamXOR as xchacha20 } from "@stablelib/xchacha20"; -import { secp256k1 } from "@noble/curves/secp256k1"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; import { sha256 } from "@noble/hashes/sha256"; export type EncryptedData = { ciphertext: Uint8Array; nonce: Uint8Array; } export const getSharedSecret = (privateKey: string, publicKey: string) => { - const key = secp256k1.getSharedSecret(privateKey, "02" + publicKey); + const key = secp256k1.getSharedSecret(hex.decode(privateKey), hex.decode("02" + publicKey)); return sha256(key.slice(1, 33)); } 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) => {