tx swaps polish

This commit is contained in:
boufni95 2026-02-24 18:36:03 +00:00
parent e411b7aa7f
commit f8fe946b40
No known key found for this signature in database
13 changed files with 174 additions and 85 deletions

View file

@ -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 { Initial1703170309875 } from './build/src/services/storage/migrations/1703170309875-initial.js'
import { LspOrder1718387847693 } from './build/src/services/storage/migrations/1718387847693-lsp_order.js' import { LspOrder1718387847693 } from './build/src/services/storage/migrations/1718387847693-lsp_order.js'
import { LndNodeInfo1720187506189 } from './build/src/services/storage/migrations/1720187506189-lnd_node_info.js'
import { LiquidityProvider1719335699480 } from './build/src/services/storage/migrations/1719335699480-liquidity_provider.js' import { LiquidityProvider1719335699480 } from './build/src/services/storage/migrations/1719335699480-liquidity_provider.js'
import { LndNodeInfo1720187506189 } from './build/src/services/storage/migrations/1720187506189-lnd_node_info.js'
import { TrackedProvider1720814323679 } from './build/src/services/storage/migrations/1720814323679-tracked_provider.js'
import { CreateInviteTokenTable1721751414878 } from './build/src/services/storage/migrations/1721751414878-create_invite_token_table.js' import { CreateInviteTokenTable1721751414878 } from './build/src/services/storage/migrations/1721751414878-create_invite_token_table.js'
import { PaymentIndex1721760297610 } from './build/src/services/storage/migrations/1721760297610-payment_index.js' import { PaymentIndex1721760297610 } from './build/src/services/storage/migrations/1721760297610-payment_index.js'
import { DebitAccess1726496225078 } from './build/src/services/storage/migrations/1726496225078-debit_access.js' import { DebitAccess1726496225078 } from './build/src/services/storage/migrations/1726496225078-debit_access.js'
@ -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 { UserCbUrl1727112281043 } from './build/src/services/storage/migrations/1727112281043-user_cb_url.js'
import { UserOffer1733502626042 } from './build/src/services/storage/migrations/1733502626042-user_offer.js' import { UserOffer1733502626042 } from './build/src/services/storage/migrations/1733502626042-user_offer.js'
import { ManagementGrant1751307732346 } from './build/src/services/storage/migrations/1751307732346-management_grant.js' import { ManagementGrant1751307732346 } from './build/src/services/storage/migrations/1751307732346-management_grant.js'
import { ManagementGrantBanned1751989251513 } from './build/src/services/storage/migrations/1751989251513-management_grant_banned.js'
import { InvoiceCallbackUrls1752425992291 } from './build/src/services/storage/migrations/1752425992291-invoice_callback_urls.js' import { InvoiceCallbackUrls1752425992291 } from './build/src/services/storage/migrations/1752425992291-invoice_callback_urls.js'
import { OldSomethingLeftover1753106599604 } from './build/src/services/storage/migrations/1753106599604-old_something_leftover.js' import { OldSomethingLeftover1753106599604 } from './build/src/services/storage/migrations/1753106599604-old_something_leftover.js'
import { UserReceivingInvoiceIdx1753109184611 } from './build/src/services/storage/migrations/1753109184611-user_receiving_invoice_idx.js' import { UserReceivingInvoiceIdx1753109184611 } from './build/src/services/storage/migrations/1753109184611-user_receiving_invoice_idx.js'
@ -67,16 +69,19 @@ import { SwapTimestamps1771347307798 } from './build/src/services/storage/migrat
export default new DataSource({ export default new DataSource({
type: "better-sqlite3", type: "better-sqlite3",
database: "db.sqlite", database: "db.sqlite",
// logging: true, // logging: true,
migrations: [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189, CreateInviteTokenTable1721751414878, migrations: [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189,
PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264, DebitToPub1727105758354, UserCbUrl1727112281043, TrackedProvider1720814323679, CreateInviteTokenTable1721751414878, PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264,
UserOffer1733502626042, ManagementGrant1751307732346, InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611, DebitToPub1727105758354, UserCbUrl1727112281043, UserOffer1733502626042, ManagementGrant1751307732346, ManagementGrantBanned1751989251513,
AppUserDevice1753285173175, UserAccess1759426050669, AddBlindToUserOffer1760000000000, ApplicationAvatarUrl1761000001000, AdminSettings1761683639419, TxSwap1762890527098, InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611, AppUserDevice1753285173175,
TxSwapAddress1764779178945, ClinkRequester1765497600000, TrackedProviderHeight1766504040000, SwapsServiceUrl1768413055036, InvoiceSwaps1769529793283, InvoiceSwapsFixes1769805357459, UserAccess1759426050669, AddBlindToUserOffer1760000000000, ApplicationAvatarUrl1761000001000, AdminSettings1761683639419, TxSwap1762890527098,
ApplicationUserTopicId1770038768784, SwapTimestamps1771347307798], TxSwapAddress1764779178945, ClinkRequester1765497600000, TrackedProviderHeight1766504040000, SwapsServiceUrl1768413055036,
InvoiceSwaps1769529793283, InvoiceSwapsFixes1769805357459, ApplicationUserTopicId1770038768784, SwapTimestamps1771347307798
],
entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment, 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], TrackedProvider, InviteToken, DebitAccess, UserOffer, ManagementGrant, AppUserDevice, UserAccess, AdminSettings, TransactionSwap, InvoiceSwap],
// synchronize: true, // synchronize: true,
}) })
//npx typeorm migration:generate ./src/services/storage/migrations/swap_timestamps -d ./datasource.js //npx typeorm migration:generate ./src/services/storage/migrations/tx_swap_timestamps -d ./datasource.js

View file

@ -1740,7 +1740,10 @@ The nostr server will send back a message response, and inside the body there wi
### TransactionSwapQuote ### TransactionSwapQuote
- __chain_fee_sats__: _number_ - __chain_fee_sats__: _number_
- __completed_at_unix__: _number_
- __expires_at_block_height__: _number_
- __invoice_amount_sats__: _number_ - __invoice_amount_sats__: _number_
- __paid_at_unix__: _number_
- __service_fee_sats__: _number_ - __service_fee_sats__: _number_
- __service_url__: _string_ - __service_url__: _string_
- __swap_fee_sats__: _number_ - __swap_fee_sats__: _number_
@ -1754,13 +1757,13 @@ The nostr server will send back a message response, and inside the body there wi
- __transaction_amount_sats__: _number_ - __transaction_amount_sats__: _number_
### TxSwapOperation ### TxSwapOperation
- __address_paid__: _string_ - __address_paid__: _string_ *this field is optional
- __failure_reason__: _string_ *this field is optional - __failure_reason__: _string_ *this field is optional
- __operation_payment__: _[UserOperation](#UserOperation)_ *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 ### TxSwapsList
- __quotes__: ARRAY of: _[TransactionSwapQuote](#TransactionSwapQuote)_
- __swaps__: ARRAY of: _[TxSwapOperation](#TxSwapOperation)_ - __swaps__: ARRAY of: _[TxSwapOperation](#TxSwapOperation)_
### UpdateChannelPolicyRequest ### UpdateChannelPolicyRequest

View file

@ -717,7 +717,10 @@ type SingleMetricReq struct {
} }
type TransactionSwapQuote struct { type TransactionSwapQuote struct {
Chain_fee_sats int64 `json:"chain_fee_sats"` Chain_fee_sats int64 `json:"chain_fee_sats"`
Completed_at_unix int64 `json:"completed_at_unix"`
Expires_at_block_height int64 `json:"expires_at_block_height"`
Invoice_amount_sats int64 `json:"invoice_amount_sats"` Invoice_amount_sats int64 `json:"invoice_amount_sats"`
Paid_at_unix int64 `json:"paid_at_unix"`
Service_fee_sats int64 `json:"service_fee_sats"` Service_fee_sats int64 `json:"service_fee_sats"`
Service_url string `json:"service_url"` Service_url string `json:"service_url"`
Swap_fee_sats int64 `json:"swap_fee_sats"` Swap_fee_sats int64 `json:"swap_fee_sats"`
@ -734,10 +737,10 @@ type TxSwapOperation struct {
Address_paid string `json:"address_paid"` Address_paid string `json:"address_paid"`
Failure_reason string `json:"failure_reason"` Failure_reason string `json:"failure_reason"`
Operation_payment *UserOperation `json:"operation_payment"` Operation_payment *UserOperation `json:"operation_payment"`
Swap_operation_id string `json:"swap_operation_id"` Quote *TransactionSwapQuote `json:"quote"`
Tx_id string `json:"tx_id"`
} }
type TxSwapsList struct { type TxSwapsList struct {
Quotes []TransactionSwapQuote `json:"quotes"`
Swaps []TxSwapOperation `json:"swaps"` Swaps []TxSwapOperation `json:"swaps"`
} }
type UpdateChannelPolicyRequest struct { type UpdateChannelPolicyRequest struct {

View file

@ -4232,7 +4232,10 @@ export const SingleMetricReqValidate = (o?: SingleMetricReq, opts: SingleMetricR
export type TransactionSwapQuote = { export type TransactionSwapQuote = {
chain_fee_sats: number chain_fee_sats: number
completed_at_unix: number
expires_at_block_height: number
invoice_amount_sats: number invoice_amount_sats: number
paid_at_unix: number
service_fee_sats: number service_fee_sats: number
service_url: string service_url: string
swap_fee_sats: number swap_fee_sats: number
@ -4243,7 +4246,10 @@ export const TransactionSwapQuoteOptionalFields: [] = []
export type TransactionSwapQuoteOptions = OptionsBaseMessage & { export type TransactionSwapQuoteOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: [] checkOptionalsAreSet?: []
chain_fee_sats_CustomCheck?: (v: number) => boolean 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 invoice_amount_sats_CustomCheck?: (v: number) => boolean
paid_at_unix_CustomCheck?: (v: number) => boolean
service_fee_sats_CustomCheck?: (v: number) => boolean service_fee_sats_CustomCheck?: (v: number) => boolean
service_url_CustomCheck?: (v: string) => boolean service_url_CustomCheck?: (v: string) => boolean
swap_fee_sats_CustomCheck?: (v: number) => 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 (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 (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 (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 (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 (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 (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 = { export type TxSwapOperation = {
address_paid: string address_paid?: string
failure_reason?: string failure_reason?: string
operation_payment?: UserOperation operation_payment?: UserOperation
swap_operation_id: string quote: TransactionSwapQuote
tx_id?: string
} }
export type TxSwapOperationOptionalField = 'failure_reason' | 'operation_payment' export type TxSwapOperationOptionalField = 'address_paid' | 'failure_reason' | 'operation_payment' | 'tx_id'
export const TxSwapOperationOptionalFields: TxSwapOperationOptionalField[] = ['failure_reason', 'operation_payment'] export const TxSwapOperationOptionalFields: TxSwapOperationOptionalField[] = ['address_paid', 'failure_reason', 'operation_payment', 'tx_id']
export type TxSwapOperationOptions = OptionsBaseMessage & { export type TxSwapOperationOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: TxSwapOperationOptionalField[] checkOptionalsAreSet?: TxSwapOperationOptionalField[]
address_paid_CustomCheck?: (v: string) => boolean address_paid_CustomCheck?: (v?: string) => boolean
failure_reason_CustomCheck?: (v?: string) => boolean failure_reason_CustomCheck?: (v?: string) => boolean
operation_payment_Options?: UserOperationOptions 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 => { 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 (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 !== '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 (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 ((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`) const quoteErr = TransactionSwapQuoteValidate(o.quote, opts.quote_Options, `${path}.quote`)
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 (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 return null
} }
export type TxSwapsList = { export type TxSwapsList = {
quotes: TransactionSwapQuote[]
swaps: TxSwapOperation[] swaps: TxSwapOperation[]
} }
export const TxSwapsListOptionalFields: [] = [] export const TxSwapsListOptionalFields: [] = []
export type TxSwapsListOptions = OptionsBaseMessage & { export type TxSwapsListOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: [] checkOptionalsAreSet?: []
quotes_ItemOptions?: TransactionSwapQuoteOptions
quotes_CustomCheck?: (v: TransactionSwapQuote[]) => boolean
swaps_ItemOptions?: TxSwapOperationOptions swaps_ItemOptions?: TxSwapOperationOptions
swaps_CustomCheck?: (v: TxSwapOperation[]) => boolean 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 (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 !== '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`) if (!Array.isArray(o.swaps)) return new Error(`${path}.swaps: is not an array`)
for (let index = 0; index < o.swaps.length; index++) { for (let index = 0; index < o.swaps.length; index++) {
const swapsErr = TxSwapOperationValidate(o.swaps[index], opts.swaps_ItemOptions, `${path}.swaps[${index}]`) const swapsErr = TxSwapOperationValidate(o.swaps[index], opts.swaps_ItemOptions, `${path}.swaps[${index}]`)

View file

@ -902,6 +902,10 @@ message TransactionSwapQuote {
int64 chain_fee_sats = 5; int64 chain_fee_sats = 5;
int64 service_fee_sats = 7; int64 service_fee_sats = 7;
string service_url = 8; string service_url = 8;
int64 expires_at_block_height = 9;
int64 paid_at_unix = 10;
int64 completed_at_unix = 11;
} }
message TransactionSwapQuoteList { message TransactionSwapQuoteList {
@ -914,15 +918,15 @@ message AdminTxSwapResponse {
} }
message TxSwapOperation { message TxSwapOperation {
string swap_operation_id = 1; TransactionSwapQuote quote = 1;
optional UserOperation operation_payment = 2; optional UserOperation operation_payment = 2;
optional string failure_reason = 3; optional string failure_reason = 3;
string address_paid = 4; optional string address_paid = 4;
optional string tx_id = 5;
} }
message TxSwapsList { message TxSwapsList {
repeated TxSwapOperation swaps = 1; repeated TxSwapOperation swaps = 1;
repeated TransactionSwapQuote quotes = 2;
} }
message CumulativeFees { message CumulativeFees {

View file

@ -9,6 +9,7 @@ import { UserInvoicePayment } from '../../storage/entity/UserInvoicePayment.js';
import { ReverseSwaps, TransactionSwapData } from './reverseSwaps.js'; import { ReverseSwaps, TransactionSwapData } from './reverseSwaps.js';
import { SubmarineSwaps, InvoiceSwapData } from './submarineSwaps.js'; import { SubmarineSwaps, InvoiceSwapData } from './submarineSwaps.js';
import { InvoiceSwap } from '../../storage/entity/InvoiceSwap.js'; import { InvoiceSwap } from '../../storage/entity/InvoiceSwap.js';
import { TransactionSwap } from '../../storage/entity/TransactionSwap.js';
export class Swaps { export class Swaps {
@ -271,32 +272,35 @@ export class Swaps {
} }
} }
ListTxSwaps = async (appUserId: string, payments: UserInvoicePayment[], newOp: (p: UserInvoicePayment) => Types.UserOperation | undefined, getServiceFee: (amt: number) => number): Promise<Types.TxSwapsList> => { private mapTransactionSwapQuote = (s: TransactionSwap, getServiceFee: (amt: number) => number): Types.TransactionSwapQuote => {
const completedSwaps = await this.storage.paymentStorage.ListCompletedTxSwaps(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) const serviceFee = getServiceFee(s.invoice_amount)
return { return {
swap_operation_id: s.swap_operation_id, swap_operation_id: s.swap_operation_id,
invoice_amount_sats: s.invoice_amount,
transaction_amount_sats: s.transaction_amount, transaction_amount_sats: s.transaction_amount,
invoice_amount_sats: s.invoice_amount,
chain_fee_sats: s.chain_fee_sats, chain_fee_sats: s.chain_fee_sats,
service_fee_sats: serviceFee, service_fee_sats: serviceFee,
swap_fee_sats: s.swap_fee_sats, swap_fee_sats: s.swap_fee_sats,
expires_at_block_height: s.timeout_block_height,
service_url: s.service_url, service_url: s.service_url,
paid_at_unix: s.paid_at_unix,
completed_at_unix: s.completed_at_unix,
} }
}) }
ListTxSwaps = async (appUserId: string, payments: UserInvoicePayment[], newOp: (p: UserInvoicePayment) => Types.UserOperation | undefined, getServiceFee: (amt: number) => number): Promise<Types.TxSwapsList> => {
const completedSwaps = await this.storage.paymentStorage.ListCompletedTxSwaps(appUserId, payments)
const pendingSwaps = await this.storage.paymentStorage.ListPendingTransactionSwaps(appUserId)
const quotes: Types.TxSwapOperation[] = pendingSwaps.map(s => ({ quote: this.mapTransactionSwapQuote(s, getServiceFee) }))
const swaps: Types.TxSwapOperation[] = completedSwaps.map(s => ({
quote: this.mapTransactionSwapQuote(s.swap, getServiceFee),
operation_payment: s.payment ? newOp(s.payment) : undefined,
address_paid: s.swap.address_paid,
tx_id: s.swap.tx_id,
failure_reason: s.swap.failure_reason,
}))
return {
swaps: swaps.concat(quotes),
} }
} }
GetTxSwapQuotes = async (appUserId: string, amt: number, getServiceFee: (decodedAmt: number) => number): Promise<Types.TransactionSwapQuote[]> => { GetTxSwapQuotes = async (appUserId: string, amt: number, getServiceFee: (decodedAmt: number) => number): Promise<Types.TransactionSwapQuote[]> => {
@ -364,6 +368,9 @@ export class Swaps {
chain_fee_sats: minerFee, chain_fee_sats: minerFee,
service_fee_sats: serviceFee, service_fee_sats: serviceFee,
service_url: swapper.getHttpUrl(), 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 swapResult = result
}) })
try { try {
await this.storage.paymentStorage.SetTransactionSwapPaid(swapOpId)
await payInvoice(txSwap.invoice, txSwap.invoice_amount) await payInvoice(txSwap.invoice, txSwap.invoice_amount)
if (!swapResult.ok) { if (!swapResult.ok) {
this.log("invoice payment successful, but swap failed") this.log("invoice payment successful, but swap failed")

View file

@ -625,7 +625,9 @@ export default class {
} }
async ListTxSwaps(ctx: Types.UserContext): Promise<Types.TxSwapsList> { async ListTxSwaps(ctx: Types.UserContext): Promise<Types.TxSwapsList> {
console.log("listing tx swaps", { appUserId: ctx.app_user_id })
const payments = await this.storage.paymentStorage.ListTxSwapPayments(ctx.app_user_id) const payments = await this.storage.paymentStorage.ListTxSwapPayments(ctx.app_user_id)
console.log("payments", payments.length)
const app = await this.storage.applicationStorage.GetApplication(ctx.app_id) const app = await this.storage.applicationStorage.GetApplication(ctx.app_id)
const isManagedUser = ctx.user_id !== app.owner.user_id const isManagedUser = ctx.user_id !== app.owner.user_id
return this.swaps.ListTxSwaps(ctx.app_user_id, payments, p => { return this.swaps.ListTxSwaps(ctx.app_user_id, payments, p => {

View file

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

View file

@ -104,9 +104,10 @@ export class StorageInterface extends EventEmitter {
return this.handleOp<T | null>(findOp) return this.handleOp<T | null>(findOp)
} }
Find<T>(entity: DBNames, q: QueryOptions<T>, txId?: string): Promise<T[]> { Find<T>(entity: DBNames, q: QueryOptions<T>, txId?: string, debug = false): Promise<T[]> {
if (debug) console.log("Find", { entity })
const opId = Math.random().toString() const opId = Math.random().toString()
const findOp: FindOperation<T> = { type: 'find', entity, opId, q, txId } const findOp: FindOperation<T> = { type: 'find', entity, opId, q, txId, debug }
return this.handleOp<T[]>(findOp) return this.handleOp<T[]>(findOp)
} }
@ -166,15 +167,16 @@ export class StorageInterface extends EventEmitter {
} }
private handleOp<T>(op: IStorageOperation): Promise<T> { private handleOp<T>(op: IStorageOperation): Promise<T> {
if (this.debug) console.log('handleOp', op) if (this.debug || op.debug) console.log('handleOp', op)
this.checkConnected() this.checkConnected()
return new Promise<T>((resolve, reject) => { return new Promise<T>((resolve, reject) => {
const responseHandler = (response: OperationResponse<T>) => { const responseHandler = (response: OperationResponse<T>) => {
if (this.debug) console.log('responseHandler', response) if (this.debug || op.debug) console.log('responseHandler', response)
if (!response.success) { if (!response.success) {
reject(new Error(response.error)); reject(new Error(response.error));
return return
} }
if (this.debug || op.debug) console.log("response", response, op)
if (response.type !== op.type) { if (response.type !== op.type) {
reject(new Error('Invalid storage response type')); reject(new Error('Invalid storage response type'));
return return
@ -186,12 +188,12 @@ export class StorageInterface extends EventEmitter {
}) })
} }
private serializeOperation(operation: IStorageOperation): IStorageOperation { private serializeOperation(operation: IStorageOperation, debug = false): IStorageOperation {
const serialized = { ...operation }; const serialized = { ...operation };
if ('q' in serialized) { if ('q' in serialized) {
(serialized as any).q = serializeRequest((serialized as any).q); (serialized as any).q = serializeRequest((serialized as any).q, debug);
} }
if (this.debug) { if (this.debug || debug) {
serialized.debug = true serialized.debug = true
} }
return serialized; return serialized;

View file

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

View file

@ -0,0 +1,20 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class TxSwapTimestamps1771878683383 implements MigrationInterface {
name = 'TxSwapTimestamps1771878683383'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "temporary_transaction_swap" ("swap_operation_id" varchar PRIMARY KEY NOT NULL, "app_user_id" varchar NOT NULL, "swap_quote_id" varchar NOT NULL, "swap_tree" varchar NOT NULL, "lockup_address" varchar NOT NULL, "refund_public_key" varchar NOT NULL, "timeout_block_height" integer NOT NULL, "invoice" varchar NOT NULL, "invoice_amount" integer NOT NULL, "transaction_amount" integer NOT NULL, "swap_fee_sats" integer NOT NULL, "chain_fee_sats" integer NOT NULL, "preimage" varchar NOT NULL, "ephemeral_public_key" varchar NOT NULL, "ephemeral_private_key" varchar NOT NULL, "used" boolean NOT NULL DEFAULT (0), "failure_reason" varchar NOT NULL DEFAULT (''), "tx_id" varchar NOT NULL DEFAULT (''), "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')), "address_paid" varchar NOT NULL DEFAULT (''), "service_url" varchar NOT NULL DEFAULT (''), "completed_at_unix" integer NOT NULL DEFAULT (0), "paid_at_unix" integer NOT NULL DEFAULT (0))`);
await queryRunner.query(`INSERT INTO "temporary_transaction_swap"("swap_operation_id", "app_user_id", "swap_quote_id", "swap_tree", "lockup_address", "refund_public_key", "timeout_block_height", "invoice", "invoice_amount", "transaction_amount", "swap_fee_sats", "chain_fee_sats", "preimage", "ephemeral_public_key", "ephemeral_private_key", "used", "failure_reason", "tx_id", "created_at", "updated_at", "address_paid", "service_url") SELECT "swap_operation_id", "app_user_id", "swap_quote_id", "swap_tree", "lockup_address", "refund_public_key", "timeout_block_height", "invoice", "invoice_amount", "transaction_amount", "swap_fee_sats", "chain_fee_sats", "preimage", "ephemeral_public_key", "ephemeral_private_key", "used", "failure_reason", "tx_id", "created_at", "updated_at", "address_paid", "service_url" FROM "transaction_swap"`);
await queryRunner.query(`DROP TABLE "transaction_swap"`);
await queryRunner.query(`ALTER TABLE "temporary_transaction_swap" RENAME TO "transaction_swap"`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "transaction_swap" RENAME TO "temporary_transaction_swap"`);
await queryRunner.query(`CREATE TABLE "transaction_swap" ("swap_operation_id" varchar PRIMARY KEY NOT NULL, "app_user_id" varchar NOT NULL, "swap_quote_id" varchar NOT NULL, "swap_tree" varchar NOT NULL, "lockup_address" varchar NOT NULL, "refund_public_key" varchar NOT NULL, "timeout_block_height" integer NOT NULL, "invoice" varchar NOT NULL, "invoice_amount" integer NOT NULL, "transaction_amount" integer NOT NULL, "swap_fee_sats" integer NOT NULL, "chain_fee_sats" integer NOT NULL, "preimage" varchar NOT NULL, "ephemeral_public_key" varchar NOT NULL, "ephemeral_private_key" varchar NOT NULL, "used" boolean NOT NULL DEFAULT (0), "failure_reason" varchar NOT NULL DEFAULT (''), "tx_id" varchar NOT NULL DEFAULT (''), "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')), "address_paid" varchar NOT NULL DEFAULT (''), "service_url" varchar NOT NULL DEFAULT (''))`);
await queryRunner.query(`INSERT INTO "transaction_swap"("swap_operation_id", "app_user_id", "swap_quote_id", "swap_tree", "lockup_address", "refund_public_key", "timeout_block_height", "invoice", "invoice_amount", "transaction_amount", "swap_fee_sats", "chain_fee_sats", "preimage", "ephemeral_public_key", "ephemeral_private_key", "used", "failure_reason", "tx_id", "created_at", "updated_at", "address_paid", "service_url") SELECT "swap_operation_id", "app_user_id", "swap_quote_id", "swap_tree", "lockup_address", "refund_public_key", "timeout_block_height", "invoice", "invoice_amount", "transaction_amount", "swap_fee_sats", "chain_fee_sats", "preimage", "ephemeral_public_key", "ephemeral_private_key", "used", "failure_reason", "tx_id", "created_at", "updated_at", "address_paid", "service_url" FROM "temporary_transaction_swap"`);
await queryRunner.query(`DROP TABLE "temporary_transaction_swap"`);
}
}

View file

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

View file

@ -473,19 +473,30 @@ export default class {
return this.dbs.FindOne<TransactionSwap>('TransactionSwap', { where: { swap_operation_id: swapOperationId, used: false, app_user_id: appUserId } }, txId) return this.dbs.FindOne<TransactionSwap>('TransactionSwap', { where: { swap_operation_id: swapOperationId, used: false, app_user_id: appUserId } }, txId)
} }
async SetTransactionSwapPaid(swapOperationId: string, txId?: string) {
const now = Math.floor(Date.now() / 1000)
return this.dbs.Update<TransactionSwap>('TransactionSwap', { swap_operation_id: swapOperationId }, {
paid_at_unix: now,
}, txId)
}
async FinalizeTransactionSwap(swapOperationId: string, address: string, chainTxId: string, txId?: string) { async FinalizeTransactionSwap(swapOperationId: string, address: string, chainTxId: string, txId?: string) {
const now = Math.floor(Date.now() / 1000)
return this.dbs.Update<TransactionSwap>('TransactionSwap', { swap_operation_id: swapOperationId }, { return this.dbs.Update<TransactionSwap>('TransactionSwap', { swap_operation_id: swapOperationId }, {
used: true, used: true,
tx_id: chainTxId, tx_id: chainTxId,
address_paid: address, address_paid: address,
completed_at_unix: now,
}, txId) }, txId)
} }
async FailTransactionSwap(swapOperationId: string, address: string, failureReason: string, txId?: string) { async FailTransactionSwap(swapOperationId: string, address: string, failureReason: string, txId?: string) {
const now = Math.floor(Date.now() / 1000)
return this.dbs.Update<TransactionSwap>('TransactionSwap', { swap_operation_id: swapOperationId }, { return this.dbs.Update<TransactionSwap>('TransactionSwap', { swap_operation_id: swapOperationId }, {
used: true, used: true,
failure_reason: failureReason, failure_reason: failureReason,
address_paid: address, address_paid: address,
completed_at_unix: now,
}, txId) }, txId)
} }