diff --git a/datasource.js b/datasource.js index 0ad041a1..4badca25 100644 --- a/datasource.js +++ b/datasource.js @@ -37,8 +37,9 @@ 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' @@ -47,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' @@ -67,16 +69,19 @@ import { SwapTimestamps1771347307798 } from './build/src/services/storage/migrat + 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, SwapsServiceUrl1768413055036, InvoiceSwaps1769529793283, InvoiceSwapsFixes1769805357459, - ApplicationUserTopicId1770038768784, SwapTimestamps1771347307798], + 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, @@ -84,4 +89,4 @@ export default new DataSource({ TrackedProvider, InviteToken, DebitAccess, UserOffer, ManagementGrant, AppUserDevice, UserAccess, AdminSettings, TransactionSwap, InvoiceSwap], // synchronize: true, }) -//npx typeorm migration:generate ./src/services/storage/migrations/swap_timestamps -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/proto/autogenerated/client.md b/proto/autogenerated/client.md index d5b9baab..106804e1 100644 --- a/proto/autogenerated/client.md +++ b/proto/autogenerated/client.md @@ -1740,7 +1740,10 @@ The nostr server will send back a message response, and inside the body there wi ### 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_ @@ -1754,13 +1757,13 @@ The nostr server will send back a message response, and inside the body there wi - __transaction_amount_sats__: _number_ ### TxSwapOperation - - __address_paid__: _string_ + - __address_paid__: _string_ *this field is optional - __failure_reason__: _string_ *this field is optional - __operation_payment__: _[UserOperation](#UserOperation)_ *this field is optional - - __swap_operation_id__: _string_ + - __quote__: _[TransactionSwapQuote](#TransactionSwapQuote)_ + - __tx_id__: _string_ *this field is optional ### TxSwapsList - - __quotes__: ARRAY of: _[TransactionSwapQuote](#TransactionSwapQuote)_ - __swaps__: ARRAY of: _[TxSwapOperation](#TxSwapOperation)_ ### UpdateChannelPolicyRequest diff --git a/proto/autogenerated/go/types.go b/proto/autogenerated/go/types.go index d0efb918..76c662cf 100644 --- a/proto/autogenerated/go/types.go +++ b/proto/autogenerated/go/types.go @@ -717,7 +717,10 @@ type SingleMetricReq struct { } 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"` @@ -731,14 +734,14 @@ 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"` - Swap_operation_id string `json:"swap_operation_id"` + 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 { - Quotes []TransactionSwapQuote `json:"quotes"` - Swaps []TxSwapOperation `json:"swaps"` + Swaps []TxSwapOperation `json:"swaps"` } type UpdateChannelPolicyRequest struct { Policy *ChannelPolicy `json:"policy"` diff --git a/proto/autogenerated/ts/types.ts b/proto/autogenerated/ts/types.ts index f62fea43..64ee28f9 100644 --- a/proto/autogenerated/ts/types.ts +++ b/proto/autogenerated/ts/types.ts @@ -4232,7 +4232,10 @@ export const SingleMetricReqValidate = (o?: SingleMetricReq, opts: SingleMetricR 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 @@ -4243,7 +4246,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 @@ -4257,9 +4263,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`) @@ -4320,25 +4335,27 @@ export const TransactionSwapRequestValidate = (o?: TransactionSwapRequest, opts: } export type TxSwapOperation = { - address_paid: string + address_paid?: string failure_reason?: string operation_payment?: UserOperation - swap_operation_id: string + quote: TransactionSwapQuote + tx_id?: string } -export type TxSwapOperationOptionalField = 'failure_reason' | 'operation_payment' -export const TxSwapOperationOptionalFields: TxSwapOperationOptionalField[] = ['failure_reason', 'operation_payment'] +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 + address_paid_CustomCheck?: (v?: string) => boolean failure_reason_CustomCheck?: (v?: string) => boolean operation_payment_Options?: UserOperationOptions - swap_operation_id_CustomCheck?: (v: string) => boolean + 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 (typeof o.address_paid !== 'string') return new Error(`${path}.address_paid: is not a string`) + 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`) @@ -4350,21 +4367,22 @@ export const TxSwapOperationValidate = (o?: TxSwapOperation, opts: TxSwapOperati } - 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`) + 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 = { - quotes: TransactionSwapQuote[] swaps: TxSwapOperation[] } export const TxSwapsListOptionalFields: [] = [] export type TxSwapsListOptions = OptionsBaseMessage & { checkOptionalsAreSet?: [] - quotes_ItemOptions?: TransactionSwapQuoteOptions - quotes_CustomCheck?: (v: TransactionSwapQuote[]) => boolean swaps_ItemOptions?: TxSwapOperationOptions swaps_CustomCheck?: (v: TxSwapOperation[]) => boolean } @@ -4372,13 +4390,6 @@ export const TxSwapsListValidate = (o?: TxSwapsList, opts: TxSwapsListOptions = 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 (!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}]`) diff --git a/proto/service/structs.proto b/proto/service/structs.proto index fb1f0867..694a6442 100644 --- a/proto/service/structs.proto +++ b/proto/service/structs.proto @@ -902,6 +902,10 @@ 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 { @@ -914,15 +918,15 @@ message AdminTxSwapResponse { } message TxSwapOperation { - string swap_operation_id = 1; + 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 TxSwapsList { repeated TxSwapOperation swaps = 1; - repeated TransactionSwapQuote quotes = 2; } message CumulativeFees { diff --git a/src/services/lnd/swaps/swaps.ts b/src/services/lnd/swaps/swaps.ts index c5abad05..dc1590b4 100644 --- a/src/services/lnd/swaps/swaps.ts +++ b/src/services/lnd/swaps/swaps.ts @@ -9,6 +9,7 @@ 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 { @@ -271,32 +272,35 @@ export class Swaps { } } + 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: 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, - } - }) + swaps: swaps.concat(quotes), } } GetTxSwapQuotes = async (appUserId: string, amt: number, getServiceFee: (decodedAmt: number) => number): Promise => { @@ -364,6 +368,9 @@ export class Swaps { 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, } } @@ -411,6 +418,7 @@ export class Swaps { 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") diff --git a/src/services/main/paymentManager.ts b/src/services/main/paymentManager.ts index 38c7658b..07021be5 100644 --- a/src/services/main/paymentManager.ts +++ b/src/services/main/paymentManager.ts @@ -595,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 @@ -625,7 +631,9 @@ export default class { } 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.ListTxSwaps(ctx.app_user_id, payments, p => { diff --git a/src/services/serverMethods/index.ts b/src/services/serverMethods/index.ts index 21668449..6783e03d 100644 --- a/src/services/serverMethods/index.ts +++ b/src/services/serverMethods/index.ts @@ -184,7 +184,7 @@ 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) 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 85b757f5..976ec20d 100644 --- a/src/services/storage/db/storageInterface.ts +++ b/src/services/storage/db/storageInterface.ts @@ -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,15 +167,16 @@ 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 @@ -186,12 +188,12 @@ export class StorageInterface extends EventEmitter { }) } - 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; 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/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 1b5537d6..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,11 +25,20 @@ 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' @@ -48,7 +50,8 @@ export const allMigrations = [Initial1703170309875, LspOrder1718387847693, Liqui InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611, AppUserDevice1753285173175, UserAccess1759426050669, AddBlindToUserOffer1760000000000, ApplicationAvatarUrl1761000001000, AdminSettings1761683639419, TxSwap1762890527098, TxSwapAddress1764779178945, ClinkRequester1765497600000, TrackedProviderHeight1766504040000, SwapsServiceUrl1768413055036, - InvoiceSwaps1769529793283, InvoiceSwapsFixes1769805357459, ApplicationUserTopicId1770038768784, SwapTimestamps1771347307798] + InvoiceSwaps1769529793283, InvoiceSwapsFixes1769805357459, ApplicationUserTopicId1770038768784, SwapTimestamps1771347307798, + TxSwapTimestamps1771878683383] export const allMetricsMigrations = [LndMetrics1703170330183, ChannelRouting1709316653538, HtlcCount1724266887195, BalanceEvents1724860966825, diff --git a/src/services/storage/paymentStorage.ts b/src/services/storage/paymentStorage.ts index 785f9909..fd6e6b30 100644 --- a/src/services/storage/paymentStorage.ts +++ b/src/services/storage/paymentStorage.ts @@ -473,19 +473,30 @@ export default class { return this.dbs.FindOne('TransactionSwap', { where: { swap_operation_id: swapOperationId, used: false, app_user_id: appUserId } }, txId) } + async SetTransactionSwapPaid(swapOperationId: string, txId?: string) { + const now = Math.floor(Date.now() / 1000) + return this.dbs.Update('TransactionSwap', { swap_operation_id: swapOperationId }, { + paid_at_unix: now, + }, txId) + } + async FinalizeTransactionSwap(swapOperationId: string, address: string, chainTxId: string, txId?: string) { + const now = Math.floor(Date.now() / 1000) 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) }