diff --git a/datasource.js b/datasource.js index 08082474..580eb008 100644 --- a/datasource.js +++ b/datasource.js @@ -43,6 +43,7 @@ import { UserAccess1759426050669 } from './build/src/services/storage/migrations import { AddBlindToUserOffer1760000000000 } from './build/src/services/storage/migrations/1760000000000-add_blind_to_user_offer.js' import { ApplicationAvatarUrl1761000001000 } from './build/src/services/storage/migrations/1761000001000-application_avatar_url.js' import { AdminSettings1761683639419 } from './build/src/services/storage/migrations/1761683639419-admin_settings.js' +import { TxSwap1762890527098 } from './build/src/services/storage/migrations/1762890527098-tx_swap.js' export default new DataSource({ type: "better-sqlite3", @@ -51,11 +52,11 @@ export default new DataSource({ migrations: [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189, CreateInviteTokenTable1721751414878, PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264, DebitToPub1727105758354, UserCbUrl1727112281043, UserOffer1733502626042, ManagementGrant1751307732346, InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611, - AppUserDevice1753285173175, UserAccess1759426050669, AddBlindToUserOffer1760000000000, ApplicationAvatarUrl1761000001000, AdminSettings1761683639419], + AppUserDevice1753285173175, UserAccess1759426050669, AddBlindToUserOffer1760000000000, ApplicationAvatarUrl1761000001000, AdminSettings1761683639419, TxSwap1762890527098], 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], // synchronize: true, }) -//npx typeorm migration:generate ./src/services/storage/migrations/tx_swap -d ./datasource.js \ No newline at end of file +//npx typeorm migration:generate ./src/services/storage/migrations/tx_swap_address -d ./datasource.js \ No newline at end of file diff --git a/proto/autogenerated/client.md b/proto/autogenerated/client.md index 7eac86ae..b402cfec 100644 --- a/proto/autogenerated/client.md +++ b/proto/autogenerated/client.md @@ -243,6 +243,11 @@ 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 + - auth type: __User__ + - This methods has an __empty__ __request__ body + - output: [SwapsList](#SwapsList) + - LndGetInfo - auth type: __Admin__ - input: [LndGetInfoRequest](#LndGetInfoRequest) @@ -814,6 +819,13 @@ 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 + - auth type: __User__ + - http method: __post__ + - http route: __/api/user/swap/list__ + - This methods has an __empty__ __request__ body + - output: [SwapsList](#SwapsList) + - LndGetInfo - auth type: __Admin__ - http method: __post__ @@ -1582,6 +1594,15 @@ 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_ + +### SwapsList + - __swaps__: ARRAY of: _[SwapOperation](#SwapOperation)_ + ### TransactionSwapQuote - __chain_fee_sats__: _number_ - __invoice_amount_sats__: _number_ diff --git a/proto/autogenerated/go/http_client.go b/proto/autogenerated/go/http_client.go index 5b0fa1f2..77e301b5 100644 --- a/proto/autogenerated/go/http_client.go +++ b/proto/autogenerated/go/http_client.go @@ -114,6 +114,7 @@ type Client struct { Health func() error LinkNPubThroughToken func(req LinkNPubThroughTokenRequest) error ListChannels func() (*LndChannels, error) + ListSwaps func() (*SwapsList, error) LndGetInfo func(req LndGetInfoRequest) (*LndGetInfoResponse, error) NewAddress func(req NewAddressRequest) (*NewAddressResponse, error) NewInvoice func(req NewInvoiceRequest) (*NewInvoiceResponse, error) @@ -1632,6 +1633,32 @@ func NewClient(params ClientParams) *Client { } return &res, nil }, + ListSwaps: func() (*SwapsList, error) { + auth, err := params.RetrieveUserAuth() + if err != nil { + return nil, err + } + finalRoute := "/api/user/swap/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 := SwapsList{} + err = json.Unmarshal(resBody, &res) + if err != nil { + return nil, err + } + return &res, nil + }, LndGetInfo: func(req LndGetInfoRequest) (*LndGetInfoResponse, error) { auth, err := params.RetrieveAdminAuth() if err != nil { diff --git a/proto/autogenerated/go/types.go b/proto/autogenerated/go/types.go index cb1389cb..9dc27c9e 100644 --- a/proto/autogenerated/go/types.go +++ b/proto/autogenerated/go/types.go @@ -655,6 +655,15 @@ 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 SwapsList struct { + Swaps []SwapOperation `json:"swaps"` +} type TransactionSwapQuote struct { Chain_fee_sats int64 `json:"chain_fee_sats"` Invoice_amount_sats int64 `json:"invoice_amount_sats"` diff --git a/proto/autogenerated/ts/express_server.ts b/proto/autogenerated/ts/express_server.ts index 3b08f9a4..ded9c78e 100644 --- a/proto/autogenerated/ts/express_server.ts +++ b/proto/autogenerated/ts/express_server.ts @@ -545,6 +545,16 @@ 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' ) + } else { + opStats.validate = opStats.guard + const res = await methods.ListSwaps({...operation, ctx}); responses.push({ status: 'OK', ...res }) + opStats.handle = process.hrtime.bigint() + callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) + } + break case 'NewAddress': if (!methods.NewAddress) { throw new Error('method NewAddress not found' ) @@ -1594,6 +1604,25 @@ 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} + 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') + 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 }) + 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.LndGetInfo) throw new Error('method: LndGetInfo is not implemented') app.post('/api/admin/lnd/getinfo', async (req, res) => { const info: Types.RequestInfo = { rpcName: 'LndGetInfo', batch: false, nostr: false, batchSize: 0} diff --git a/proto/autogenerated/ts/http_client.ts b/proto/autogenerated/ts/http_client.ts index c48d2ef7..2f011982 100644 --- a/proto/autogenerated/ts/http_client.ts +++ b/proto/autogenerated/ts/http_client.ts @@ -781,6 +781,20 @@ export default (params: ClientParams) => ({ } return { status: 'ERROR', reason: 'invalid response' } }, + ListSwaps: async (): Promise => { + const auth = await params.retrieveUserAuth() + if (auth === null) throw new Error('retrieveUserAuth() returned null') + let finalRoute = '/api/user/swap/list' + const { data } = await axios.post(params.baseUrl + finalRoute, {}, { headers: { 'authorization': auth } }) + if (data.status === 'ERROR' && typeof data.reason === 'string') return data + if (data.status === 'OK') { + const result = data + if(!params.checkResult) return { status: 'OK', ...result } + const error = Types.SwapsListValidate(result) + if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } + } + return { status: 'ERROR', reason: 'invalid response' } + }, LndGetInfo: async (request: Types.LndGetInfoRequest): Promise => { const auth = await params.retrieveAdminAuth() if (auth === null) throw new Error('retrieveAdminAuth() returned null') diff --git a/proto/autogenerated/ts/nostr_client.ts b/proto/autogenerated/ts/nostr_client.ts index 9bcbfe00..98719c5f 100644 --- a/proto/autogenerated/ts/nostr_client.ts +++ b/proto/autogenerated/ts/nostr_client.ts @@ -665,6 +665,20 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ } return { status: 'ERROR', reason: 'invalid response' } }, + ListSwaps: 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 }) + 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) + if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message } + } + return { status: 'ERROR', reason: 'invalid response' } + }, LndGetInfo: async (request: Types.LndGetInfoRequest): Promise => { const auth = await params.retrieveNostrAdminAuth() if (auth === null) throw new Error('retrieveNostrAdminAuth() returned null') diff --git a/proto/autogenerated/ts/nostr_transport.ts b/proto/autogenerated/ts/nostr_transport.ts index 3d4614fc..124c7443 100644 --- a/proto/autogenerated/ts/nostr_transport.ts +++ b/proto/autogenerated/ts/nostr_transport.ts @@ -427,6 +427,16 @@ 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') + } else { + opStats.validate = opStats.guard + const res = await methods.ListSwaps({...operation, ctx}); responses.push({ status: 'OK', ...res }) + opStats.handle = process.hrtime.bigint() + callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) + } + break case 'NewAddress': if (!methods.NewAddress) { throw new Error('method not defined: NewAddress') @@ -1109,6 +1119,19 @@ 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': + try { + if (!methods.ListSwaps) throw new Error('method: ListSwaps is not implemented') + const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier) + stats.guard = process.hrtime.bigint() + authCtx = authContext + stats.validate = stats.guard + const response = await methods.ListSwaps({rpcName:'ListSwaps', 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 'LndGetInfo': try { if (!methods.LndGetInfo) throw new Error('method: LndGetInfo is not implemented') diff --git a/proto/autogenerated/ts/types.ts b/proto/autogenerated/ts/types.ts index c0518754..a306e5ac 100644 --- a/proto/autogenerated/ts/types.ts +++ b/proto/autogenerated/ts/types.ts @@ -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 | GetTransactionSwapQuote_Input | GetUserInfo_Input | GetUserOffer_Input | GetUserOfferInvoices_Input | GetUserOffers_Input | GetUserOperations_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 | GetTransactionSwapQuote_Output | GetUserInfo_Output | GetUserOffer_Output | GetUserOfferInvoices_Output | GetUserOffers_Output | GetUserOperations_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 | GetTransactionSwapQuote_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 | GetTransactionSwapQuote_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 AuthContext = AdminContext | AppContext | GuestContext | GuestWithPubContext | MetricsContext | UserContext export type AddApp_Input = {rpcName:'AddApp', req: AddAppRequest} @@ -238,6 +238,9 @@ export type LinkNPubThroughToken_Output = ResultError | { status: 'OK' } 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 LndGetInfo_Input = {rpcName:'LndGetInfo', req: LndGetInfoRequest} export type LndGetInfo_Output = ResultError | ({ status: 'OK' } & LndGetInfoResponse) @@ -385,6 +388,7 @@ export type ServerMethods = { Health?: (req: Health_Input & {ctx: GuestContext }) => Promise LinkNPubThroughToken?: (req: LinkNPubThroughToken_Input & {ctx: GuestWithPubContext }) => Promise ListChannels?: (req: ListChannels_Input & {ctx: AdminContext }) => Promise + ListSwaps?: (req: ListSwaps_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 @@ -3845,6 +3849,66 @@ 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 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 SwapOperationValidate = (o?: SwapOperation, opts: SwapOperationOptions = {}, path: string = 'SwapOperation::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 ((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 (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 SwapsList = { + swaps: SwapOperation[] +} +export const SwapsListOptionalFields: [] = [] +export type SwapsListOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: [] + swaps_ItemOptions?: SwapOperationOptions + swaps_CustomCheck?: (v: SwapOperation[]) => boolean +} +export const SwapsListValidate = (o?: SwapsList, opts: SwapsListOptions = {}, path: string = 'SwapsList::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 = SwapOperationValidate(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 TransactionSwapQuote = { chain_fee_sats: number invoice_amount_sats: number diff --git a/proto/service/methods.proto b/proto/service/methods.proto index 0f4147f4..20f155a3 100644 --- a/proto/service/methods.proto +++ b/proto/service/methods.proto @@ -503,6 +503,13 @@ service LightningPub { option (nostr) = true; } + rpc ListSwaps(structs.Empty) returns (structs.SwapsList){ + option (auth_type) = "User"; + option (http_method) = "post"; + option (http_route) = "/api/user/swap/list"; + option (nostr) = true; + } + rpc NewInvoice(structs.NewInvoiceRequest) returns (structs.NewInvoiceResponse){ option (auth_type) = "User"; option (http_method) = "post"; diff --git a/proto/service/structs.proto b/proto/service/structs.proto index 400a47ef..40f83e7a 100644 --- a/proto/service/structs.proto +++ b/proto/service/structs.proto @@ -843,6 +843,18 @@ message TransactionSwapQuote { int64 chain_fee_sats = 5; int64 service_fee_sats = 7; } + +message SwapOperation { + string swap_operation_id = 1; + optional UserOperation operation_payment = 2; + optional string failure_reason = 3; + string address_paid = 4; +} + +message SwapsList { + repeated SwapOperation swaps = 1; +} + message CumulativeFees { int64 networkFeeFixed = 2; int64 serviceFeeBps = 3; diff --git a/src/services/main/paymentManager.ts b/src/services/main/paymentManager.ts index c07c8ef3..238f11bc 100644 --- a/src/services/main/paymentManager.ts +++ b/src/services/main/paymentManager.ts @@ -490,18 +490,18 @@ export default class { payment = await this.PayInvoice(ctx.user_id, { amount: 0, invoice: txSwap.invoice }, app, { swapOperationId: req.swap_operation_id }) if (!swapResult.ok) { this.log("invoice payment successful, but swap failed") - await this.storage.paymentStorage.FailTransactionSwap(req.swap_operation_id, swapResult.error) + await this.storage.paymentStorage.FailTransactionSwap(req.swap_operation_id, req.address, swapResult.error) throw new Error(swapResult.error) } this.log("swap completed successfully") - await this.storage.paymentStorage.FinalizeTransactionSwap(req.swap_operation_id, swapResult.txId) + await this.storage.paymentStorage.FinalizeTransactionSwap(req.swap_operation_id, req.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(req.swap_operation_id, err.message) + await this.storage.paymentStorage.FailTransactionSwap(req.swap_operation_id, req.address, err.message) } else { this.log("failed to pay swap invoice and swap failed", swapResult.error) - await this.storage.paymentStorage.FailTransactionSwap(req.swap_operation_id, swapResult.error) + await this.storage.paymentStorage.FailTransactionSwap(req.swap_operation_id, req.address, swapResult.error) } throw err } @@ -545,6 +545,23 @@ export default class { } } + async ListSwaps(ctx: Types.UserContext): Promise { + const swaps = await this.storage.paymentStorage.ListCompletedSwaps(ctx.app_user_id) + return { + swaps: swaps.map(s => { + const p = s.payment + const opId = `${Types.UserOperationType.OUTGOING_TX}-${p?.serial_id}` + const op = p ? this.newInvoicePaymentOperation({ amount: p.paid_amount, confirmed: p.paid_at_unix !== 0, invoice: p.invoice, opId, networkFee: p.routing_fees, serviceFee: p.service_fees }) : 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, + } + }) + } + } + balanceCheckUrl(k1: string): string { return `${this.settings.getSettings().serviceSettings.serviceUrl}/api/guest/lnurl_withdraw/info?k1=${k1}` } diff --git a/src/services/serverMethods/index.ts b/src/services/serverMethods/index.ts index 651383f0..4a175ee6 100644 --- a/src/services/serverMethods/index.ts +++ b/src/services/serverMethods/index.ts @@ -147,6 +147,9 @@ export default (mainHandler: Main): Types.ServerMethods => { if (err != null) throw new Error(err.message) return mainHandler.paymentManager.PayAddress(ctx, req) }, + ListSwaps: async ({ ctx }) => { + return mainHandler.paymentManager.ListSwaps(ctx) + }, GetTransactionSwapQuote: async ({ ctx, req }) => { return mainHandler.paymentManager.GetTransactionSwapQuote(ctx, req) }, diff --git a/src/services/storage/entity/TransactionSwap.ts b/src/services/storage/entity/TransactionSwap.ts index 6fb01c99..5e3c0e51 100644 --- a/src/services/storage/entity/TransactionSwap.ts +++ b/src/services/storage/entity/TransactionSwap.ts @@ -57,6 +57,9 @@ export class TransactionSwap { @Column({ default: "" }) tx_id: string + @Column({ default: "" }) + address_paid: string + @CreateDateColumn() created_at: Date diff --git a/src/services/storage/migrations/1764779178945-tx_swap_address.ts b/src/services/storage/migrations/1764779178945-tx_swap_address.ts new file mode 100644 index 00000000..b62f44bd --- /dev/null +++ b/src/services/storage/migrations/1764779178945-tx_swap_address.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class TxSwapAddress1764779178945 implements MigrationInterface { + name = 'TxSwapAddress1764779178945' + + 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 (''))`); + 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") 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" 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')))`); + 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") 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" 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 b749a4ce..56c1eefd 100644 --- a/src/services/storage/migrations/runner.ts +++ b/src/services/storage/migrations/runner.ts @@ -28,13 +28,14 @@ import { AddBlindToUserOffer1760000000000 } from './1760000000000-add_blind_to_u import { ApplicationAvatarUrl1761000001000 } from './1761000001000-application_avatar_url.js' import { AdminSettings1761683639419 } from './1761683639419-admin_settings.js' import { TxSwap1762890527098 } from './1762890527098-tx_swap.js' +import { TxSwapAddress1764779178945 } from './1764779178945-tx_swap_address.js' export const allMigrations = [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189, TrackedProvider1720814323679, CreateInviteTokenTable1721751414878, PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264, DebitToPub1727105758354, UserCbUrl1727112281043, UserOffer1733502626042, ManagementGrant1751307732346, ManagementGrantBanned1751989251513, InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611, AppUserDevice1753285173175, - UserAccess1759426050669, AddBlindToUserOffer1760000000000, ApplicationAvatarUrl1761000001000, AdminSettings1761683639419, TxSwap1762890527098] + UserAccess1759426050669, AddBlindToUserOffer1760000000000, ApplicationAvatarUrl1761000001000, AdminSettings1761683639419, TxSwap1762890527098, TxSwapAddress1764779178945] export const allMetricsMigrations = [LndMetrics1703170330183, ChannelRouting1709316653538, HtlcCount1724266887195, BalanceEvents1724860966825, RootOps1732566440447, RootOpsTime1745428134124, ChannelEvents1750777346411] /* export const TypeOrmMigrationRunner = async (log: PubLogger, storageManager: Storage, settings: DbSettings, arg: string | undefined): Promise => { diff --git a/src/services/storage/paymentStorage.ts b/src/services/storage/paymentStorage.ts index 6f35fbb7..aa7d5b1d 100644 --- a/src/services/storage/paymentStorage.ts +++ b/src/services/storage/paymentStorage.ts @@ -1,5 +1,5 @@ import crypto from 'crypto'; -import { And, Between, Equal, FindOperator, IsNull, LessThan, LessThanOrEqual, MoreThan, MoreThanOrEqual } from "typeorm" +import { And, Between, Equal, FindOperator, IsNull, LessThan, LessThanOrEqual, MoreThan, MoreThanOrEqual, Not } from "typeorm" import { User } from './entity/User.js'; import { UserTransactionPayment } from './entity/UserTransactionPayment.js'; import { EphemeralKeyType, UserEphemeralKey } from './entity/UserEphemeralKey.js'; @@ -470,17 +470,19 @@ export default class { return this.dbs.FindOne('TransactionSwap', { where: { swap_operation_id: swapOperationId, used: false, app_user_id: appUserId } }, txId) } - async FinalizeTransactionSwap(swapOperationId: string, txId: string) { + async FinalizeTransactionSwap(swapOperationId: string, address: string, txId: string) { return this.dbs.Update('TransactionSwap', { swap_operation_id: swapOperationId }, { used: true, tx_id: txId, + address_paid: address, }) } - async FailTransactionSwap(swapOperationId: string, failureReason: string) { + async FailTransactionSwap(swapOperationId: string, address: string, failureReason: string) { return this.dbs.Update('TransactionSwap', { swap_operation_id: swapOperationId }, { used: true, failure_reason: failureReason, + address_paid: address, }) } @@ -491,6 +493,18 @@ export default class { async DeleteExpiredTransactionSwaps(currentHeight: number, txId?: string) { return this.dbs.Delete('TransactionSwap', { timeout_block_height: LessThan(currentHeight) }, txId) } + + async ListCompletedSwaps(appUserId: string, 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() + payments.forEach(p => { + paymentsMap.set(p.swap_operation_id, p) + }) + return completed.map(c => ({ + swap: c, payment: paymentsMap.get(c.swap_operation_id) + })) + } } const orFail = async (resultPromise: Promise) => {