swaps wired

This commit is contained in:
boufni95 2025-11-11 19:51:07 +00:00
parent ec2d48664a
commit f20d40e44f
21 changed files with 679 additions and 137 deletions

View file

@ -21,6 +21,7 @@ import { ManagementGrant } from "./build/src/services/storage/entity/ManagementG
import { AppUserDevice } from "./build/src/services/storage/entity/AppUserDevice.js" import { AppUserDevice } from "./build/src/services/storage/entity/AppUserDevice.js"
import { UserAccess } from "./build/src/services/storage/entity/UserAccess.js" import { UserAccess } from "./build/src/services/storage/entity/UserAccess.js"
import { AdminSettings } from "./build/src/services/storage/entity/AdminSettings.js" import { AdminSettings } from "./build/src/services/storage/entity/AdminSettings.js"
import { TransactionSwap } from "./build/src/services/storage/entity/TransactionSwap.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'
@ -41,6 +42,7 @@ import { AppUserDevice1753285173175 } from './build/src/services/storage/migrati
import { UserAccess1759426050669 } from './build/src/services/storage/migrations/1759426050669-user_access.js' import { UserAccess1759426050669 } from './build/src/services/storage/migrations/1759426050669-user_access.js'
import { AddBlindToUserOffer1760000000000 } from './build/src/services/storage/migrations/1760000000000-add_blind_to_user_offer.js' 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 { ApplicationAvatarUrl1761000001000 } from './build/src/services/storage/migrations/1761000001000-application_avatar_url.js'
import { AdminSettings1761683639419 } from './build/src/services/storage/migrations/1761683639419-admin_settings.js'
export default new DataSource({ export default new DataSource({
type: "better-sqlite3", type: "better-sqlite3",
@ -49,10 +51,11 @@ export default new DataSource({
migrations: [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189, CreateInviteTokenTable1721751414878, migrations: [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189, CreateInviteTokenTable1721751414878,
PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264, DebitToPub1727105758354, UserCbUrl1727112281043, PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264, DebitToPub1727105758354, UserCbUrl1727112281043,
UserOffer1733502626042, ManagementGrant1751307732346, InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611, UserOffer1733502626042, ManagementGrant1751307732346, InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611,
AppUserDevice1753285173175, UserAccess1759426050669, AddBlindToUserOffer1760000000000, ApplicationAvatarUrl1761000001000], AppUserDevice1753285173175, UserAccess1759426050669, AddBlindToUserOffer1760000000000, ApplicationAvatarUrl1761000001000, AdminSettings1761683639419],
entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment, entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment,
UserBasicAuth, UserEphemeralKey, Product, UserToUserPayment, Application, ApplicationUser, UserToUserPayment, LspOrder, LndNodeInfo, UserBasicAuth, UserEphemeralKey, Product, UserToUserPayment, Application, ApplicationUser, UserToUserPayment, LspOrder, LndNodeInfo,
TrackedProvider, InviteToken, DebitAccess, UserOffer, ManagementGrant, AppUserDevice, UserAccess, AdminSettings], TrackedProvider, InviteToken, DebitAccess, UserOffer, ManagementGrant, AppUserDevice, UserAccess, AdminSettings, TransactionSwap],
// synchronize: true, // synchronize: true,
}) })
//npx typeorm migration:generate ./src/services/storage/migrations/admin_settings -d ./datasource.js //npx typeorm migration:generate ./src/services/storage/migrations/tx_swap -d ./datasource.js

View file

@ -198,6 +198,11 @@ The nostr server will send back a message response, and inside the body there wi
- input: [SingleMetricReq](#SingleMetricReq) - input: [SingleMetricReq](#SingleMetricReq)
- output: [UsageMetricTlv](#UsageMetricTlv) - output: [UsageMetricTlv](#UsageMetricTlv)
- GetTransactionSwapQuote
- auth type: __User__
- input: [TransactionSwapRequest](#TransactionSwapRequest)
- output: [TransactionSwapQuote](#TransactionSwapQuote)
- GetUsageMetrics - GetUsageMetrics
- auth type: __Metrics__ - auth type: __Metrics__
- input: [LatestUsageMetricReq](#LatestUsageMetricReq) - input: [LatestUsageMetricReq](#LatestUsageMetricReq)
@ -708,6 +713,13 @@ The nostr server will send back a message response, and inside the body there wi
- input: [SingleMetricReq](#SingleMetricReq) - input: [SingleMetricReq](#SingleMetricReq)
- output: [UsageMetricTlv](#UsageMetricTlv) - output: [UsageMetricTlv](#UsageMetricTlv)
- GetTransactionSwapQuote
- auth type: __User__
- http method: __post__
- http route: __/api/user/swap/quote__
- input: [TransactionSwapRequest](#TransactionSwapRequest)
- output: [TransactionSwapQuote](#TransactionSwapQuote)
- GetUsageMetrics - GetUsageMetrics
- auth type: __Metrics__ - auth type: __Metrics__
- http method: __post__ - http method: __post__
@ -1450,6 +1462,7 @@ The nostr server will send back a message response, and inside the body there wi
- __address__: _string_ - __address__: _string_
- __amoutSats__: _number_ - __amoutSats__: _number_
- __satsPerVByte__: _number_ - __satsPerVByte__: _number_
- __swap_operation_id__: _string_ *this field is optional
### PayAddressResponse ### PayAddressResponse
- __network_fee__: _number_ - __network_fee__: _number_
@ -1554,6 +1567,16 @@ The nostr server will send back a message response, and inside the body there wi
- __page__: _number_ - __page__: _number_
- __request_id__: _number_ *this field is optional - __request_id__: _number_ *this field is optional
### TransactionSwapQuote
- __chain_fee_sats__: _number_
- __invoice_amount_sats__: _number_
- __swap_fee_sats__: _number_
- __swap_operation_id__: _string_
- __transaction_amount_sats__: _number_
### TransactionSwapRequest
- __transaction_amount_sats__: _number_
### UpdateChannelPolicyRequest ### UpdateChannelPolicyRequest
- __policy__: _[ChannelPolicy](#ChannelPolicy)_ - __policy__: _[ChannelPolicy](#ChannelPolicy)_
- __update__: _[UpdateChannelPolicyRequest_update](#UpdateChannelPolicyRequest_update)_ - __update__: _[UpdateChannelPolicyRequest_update](#UpdateChannelPolicyRequest_update)_

View file

@ -101,6 +101,7 @@ type Client struct {
GetSeed func() (*LndSeed, error) GetSeed func() (*LndSeed, error)
GetSingleBundleMetrics func(req SingleMetricReq) (*BundleData, error) GetSingleBundleMetrics func(req SingleMetricReq) (*BundleData, error)
GetSingleUsageMetrics func(req SingleMetricReq) (*UsageMetricTlv, error) GetSingleUsageMetrics func(req SingleMetricReq) (*UsageMetricTlv, error)
GetTransactionSwapQuote func(req TransactionSwapRequest) (*TransactionSwapQuote, error)
GetUsageMetrics func(req LatestUsageMetricReq) (*UsageMetrics, error) GetUsageMetrics func(req LatestUsageMetricReq) (*UsageMetrics, error)
GetUserInfo func() (*UserInfo, error) GetUserInfo func() (*UserInfo, error)
GetUserOffer func(req OfferId) (*OfferConfig, error) GetUserOffer func(req OfferId) (*OfferConfig, error)
@ -1285,6 +1286,35 @@ func NewClient(params ClientParams) *Client {
} }
return &res, nil return &res, nil
}, },
GetTransactionSwapQuote: func(req TransactionSwapRequest) (*TransactionSwapQuote, error) {
auth, err := params.RetrieveUserAuth()
if err != nil {
return nil, err
}
finalRoute := "/api/user/swap/quote"
body, err := json.Marshal(req)
if err != nil {
return nil, err
}
resBody, err := doPostRequest(params.BaseURL+finalRoute, body, auth)
if err != nil {
return nil, err
}
result := ResultError{}
err = json.Unmarshal(resBody, &result)
if err != nil {
return nil, err
}
if result.Status == "ERROR" {
return nil, fmt.Errorf(result.Reason)
}
res := TransactionSwapQuote{}
err = json.Unmarshal(resBody, &res)
if err != nil {
return nil, err
}
return &res, nil
},
GetUsageMetrics: func(req LatestUsageMetricReq) (*UsageMetrics, error) { GetUsageMetrics: func(req LatestUsageMetricReq) (*UsageMetrics, error) {
auth, err := params.RetrieveMetricsAuth() auth, err := params.RetrieveMetricsAuth()
if err != nil { if err != nil {

View file

@ -535,6 +535,7 @@ type PayAddressRequest struct {
Address string `json:"address"` Address string `json:"address"`
Amoutsats int64 `json:"amoutSats"` Amoutsats int64 `json:"amoutSats"`
Satspervbyte int64 `json:"satsPerVByte"` Satspervbyte int64 `json:"satsPerVByte"`
Swap_operation_id string `json:"swap_operation_id"`
} }
type PayAddressResponse struct { type PayAddressResponse struct {
Network_fee int64 `json:"network_fee"` Network_fee int64 `json:"network_fee"`
@ -639,6 +640,16 @@ type SingleMetricReq struct {
Page int64 `json:"page"` Page int64 `json:"page"`
Request_id int64 `json:"request_id"` Request_id int64 `json:"request_id"`
} }
type TransactionSwapQuote struct {
Chain_fee_sats int64 `json:"chain_fee_sats"`
Invoice_amount_sats int64 `json:"invoice_amount_sats"`
Swap_fee_sats int64 `json:"swap_fee_sats"`
Swap_operation_id string `json:"swap_operation_id"`
Transaction_amount_sats int64 `json:"transaction_amount_sats"`
}
type TransactionSwapRequest struct {
Transaction_amount_sats int64 `json:"transaction_amount_sats"`
}
type UpdateChannelPolicyRequest struct { type UpdateChannelPolicyRequest struct {
Policy *ChannelPolicy `json:"policy"` Policy *ChannelPolicy `json:"policy"`
Update *UpdateChannelPolicyRequest_update `json:"update"` Update *UpdateChannelPolicyRequest_update `json:"update"`

View file

@ -477,6 +477,18 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => {
callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) callsMetrics.push({ ...opInfo, ...opStats, ...ctx })
} }
break break
case 'GetTransactionSwapQuote':
if (!methods.GetTransactionSwapQuote) {
throw new Error('method GetTransactionSwapQuote not found' )
} else {
const error = Types.TransactionSwapRequestValidate(operation.req)
opStats.validate = process.hrtime.bigint()
if (error !== null) throw error
const res = await methods.GetTransactionSwapQuote({...operation, ctx}); responses.push({ status: 'OK', ...res })
opStats.handle = process.hrtime.bigint()
callsMetrics.push({ ...opInfo, ...opStats, ...ctx })
}
break
case 'GetUserInfo': case 'GetUserInfo':
if (!methods.GetUserInfo) { if (!methods.GetUserInfo) {
throw new Error('method GetUserInfo not found' ) throw new Error('method GetUserInfo not found' )
@ -1317,6 +1329,28 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => {
opts.metricsCallback([{ ...info, ...stats, ...authContext }]) 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 } } 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.GetTransactionSwapQuote) throw new Error('method: GetTransactionSwapQuote is not implemented')
app.post('/api/user/swap/quote', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'GetTransactionSwapQuote', 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.GetTransactionSwapQuote) throw new Error('method: GetTransactionSwapQuote is not implemented')
const authContext = await opts.UserAuthGuard(req.headers['authorization'])
authCtx = authContext
stats.guard = process.hrtime.bigint()
const request = req.body
const error = Types.TransactionSwapRequestValidate(request)
stats.validate = process.hrtime.bigint()
if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authContext }, opts.metricsCallback)
const query = req.query
const params = req.params
const response = await methods.GetTransactionSwapQuote({rpcName:'GetTransactionSwapQuote', ctx:authContext , req: request})
stats.handle = process.hrtime.bigint()
res.json({status: 'OK', ...response})
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
})
if (!opts.allowNotImplementedMethods && !methods.GetUsageMetrics) throw new Error('method: GetUsageMetrics is not implemented') if (!opts.allowNotImplementedMethods && !methods.GetUsageMetrics) throw new Error('method: GetUsageMetrics is not implemented')
app.post('/api/reports/usage', async (req, res) => { app.post('/api/reports/usage', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'GetUsageMetrics', batch: false, nostr: false, batchSize: 0} const info: Types.RequestInfo = { rpcName: 'GetUsageMetrics', batch: false, nostr: false, batchSize: 0}

View file

@ -603,6 +603,20 @@ export default (params: ClientParams) => ({
} }
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
}, },
GetTransactionSwapQuote: async (request: Types.TransactionSwapRequest): Promise<ResultError | ({ status: 'OK' }& Types.TransactionSwapQuote)> => {
const auth = await params.retrieveUserAuth()
if (auth === null) throw new Error('retrieveUserAuth() returned null')
let finalRoute = '/api/user/swap/quote'
const { data } = await axios.post(params.baseUrl + finalRoute, request, { headers: { 'authorization': auth } })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') {
const result = data
if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.TransactionSwapQuoteValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
GetUsageMetrics: async (request: Types.LatestUsageMetricReq): Promise<ResultError | ({ status: 'OK' }& Types.UsageMetrics)> => { GetUsageMetrics: async (request: Types.LatestUsageMetricReq): Promise<ResultError | ({ status: 'OK' }& Types.UsageMetrics)> => {
const auth = await params.retrieveMetricsAuth() const auth = await params.retrieveMetricsAuth()
if (auth === null) throw new Error('retrieveMetricsAuth() returned null') if (auth === null) throw new Error('retrieveMetricsAuth() returned null')

View file

@ -536,6 +536,21 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ
} }
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
}, },
GetTransactionSwapQuote: async (request: Types.TransactionSwapRequest): Promise<ResultError | ({ status: 'OK' }& Types.TransactionSwapQuote)> => {
const auth = await params.retrieveNostrUserAuth()
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
const nostrRequest: NostrRequest = {}
nostrRequest.body = request
const data = await send(params.pubDestination, {rpcName:'GetTransactionSwapQuote',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.TransactionSwapQuoteValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
GetUsageMetrics: async (request: Types.LatestUsageMetricReq): Promise<ResultError | ({ status: 'OK' }& Types.UsageMetrics)> => { GetUsageMetrics: async (request: Types.LatestUsageMetricReq): Promise<ResultError | ({ status: 'OK' }& Types.UsageMetrics)> => {
const auth = await params.retrieveNostrMetricsAuth() const auth = await params.retrieveNostrMetricsAuth()
if (auth === null) throw new Error('retrieveNostrMetricsAuth() returned null') if (auth === null) throw new Error('retrieveNostrMetricsAuth() returned null')

View file

@ -359,6 +359,18 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => {
callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) callsMetrics.push({ ...opInfo, ...opStats, ...ctx })
} }
break break
case 'GetTransactionSwapQuote':
if (!methods.GetTransactionSwapQuote) {
throw new Error('method not defined: GetTransactionSwapQuote')
} else {
const error = Types.TransactionSwapRequestValidate(operation.req)
opStats.validate = process.hrtime.bigint()
if (error !== null) throw error
const res = await methods.GetTransactionSwapQuote({...operation, ctx}); responses.push({ status: 'OK', ...res })
opStats.handle = process.hrtime.bigint()
callsMetrics.push({ ...opInfo, ...opStats, ...ctx })
}
break
case 'GetUserInfo': case 'GetUserInfo':
if (!methods.GetUserInfo) { if (!methods.GetUserInfo) {
throw new Error('method not defined: GetUserInfo') throw new Error('method not defined: GetUserInfo')
@ -962,6 +974,22 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => {
opts.metricsCallback([{ ...info, ...stats, ...authContext }]) 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 } }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 break
case 'GetTransactionSwapQuote':
try {
if (!methods.GetTransactionSwapQuote) throw new Error('method: GetTransactionSwapQuote is not implemented')
const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier)
stats.guard = process.hrtime.bigint()
authCtx = authContext
const request = req.body
const error = Types.TransactionSwapRequestValidate(request)
stats.validate = process.hrtime.bigint()
if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback)
const response = await methods.GetTransactionSwapQuote({rpcName:'GetTransactionSwapQuote', ctx:authContext , req: request})
stats.handle = process.hrtime.bigint()
res({status: 'OK', ...response})
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
}catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
break
case 'GetUsageMetrics': case 'GetUsageMetrics':
try { try {
if (!methods.GetUsageMetrics) throw new Error('method: GetUsageMetrics is not implemented') if (!methods.GetUsageMetrics) throw new Error('method: GetUsageMetrics is not implemented')

View file

@ -35,8 +35,8 @@ export type UserContext = {
app_user_id: string app_user_id: string
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 | 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 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 | 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 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 AuthContext = AdminContext | AppContext | GuestContext | GuestWithPubContext | MetricsContext | UserContext export type AuthContext = AdminContext | AppContext | GuestContext | GuestWithPubContext | MetricsContext | UserContext
export type AddApp_Input = {rpcName:'AddApp', req: AddAppRequest} export type AddApp_Input = {rpcName:'AddApp', req: AddAppRequest}
@ -186,6 +186,9 @@ export type GetSingleBundleMetrics_Output = ResultError | ({ status: 'OK' } & Bu
export type GetSingleUsageMetrics_Input = {rpcName:'GetSingleUsageMetrics', req: SingleMetricReq} export type GetSingleUsageMetrics_Input = {rpcName:'GetSingleUsageMetrics', req: SingleMetricReq}
export type GetSingleUsageMetrics_Output = ResultError | ({ status: 'OK' } & UsageMetricTlv) export type GetSingleUsageMetrics_Output = ResultError | ({ status: 'OK' } & UsageMetricTlv)
export type GetTransactionSwapQuote_Input = {rpcName:'GetTransactionSwapQuote', req: TransactionSwapRequest}
export type GetTransactionSwapQuote_Output = ResultError | ({ status: 'OK' } & TransactionSwapQuote)
export type GetUsageMetrics_Input = {rpcName:'GetUsageMetrics', req: LatestUsageMetricReq} export type GetUsageMetrics_Input = {rpcName:'GetUsageMetrics', req: LatestUsageMetricReq}
export type GetUsageMetrics_Output = ResultError | ({ status: 'OK' } & UsageMetrics) export type GetUsageMetrics_Output = ResultError | ({ status: 'OK' } & UsageMetrics)
@ -369,6 +372,7 @@ export type ServerMethods = {
GetSeed?: (req: GetSeed_Input & {ctx: AdminContext }) => Promise<LndSeed> GetSeed?: (req: GetSeed_Input & {ctx: AdminContext }) => Promise<LndSeed>
GetSingleBundleMetrics?: (req: GetSingleBundleMetrics_Input & {ctx: MetricsContext }) => Promise<BundleData> GetSingleBundleMetrics?: (req: GetSingleBundleMetrics_Input & {ctx: MetricsContext }) => Promise<BundleData>
GetSingleUsageMetrics?: (req: GetSingleUsageMetrics_Input & {ctx: MetricsContext }) => Promise<UsageMetricTlv> GetSingleUsageMetrics?: (req: GetSingleUsageMetrics_Input & {ctx: MetricsContext }) => Promise<UsageMetricTlv>
GetTransactionSwapQuote?: (req: GetTransactionSwapQuote_Input & {ctx: UserContext }) => Promise<TransactionSwapQuote>
GetUsageMetrics?: (req: GetUsageMetrics_Input & {ctx: MetricsContext }) => Promise<UsageMetrics> GetUsageMetrics?: (req: GetUsageMetrics_Input & {ctx: MetricsContext }) => Promise<UsageMetrics>
GetUserInfo?: (req: GetUserInfo_Input & {ctx: UserContext }) => Promise<UserInfo> GetUserInfo?: (req: GetUserInfo_Input & {ctx: UserContext }) => Promise<UserInfo>
GetUserOffer?: (req: GetUserOffer_Input & {ctx: UserContext }) => Promise<OfferConfig> GetUserOffer?: (req: GetUserOffer_Input & {ctx: UserContext }) => Promise<OfferConfig>
@ -3132,13 +3136,16 @@ export type PayAddressRequest = {
address: string address: string
amoutSats: number amoutSats: number
satsPerVByte: number satsPerVByte: number
swap_operation_id?: string
} }
export const PayAddressRequestOptionalFields: [] = [] export type PayAddressRequestOptionalField = 'swap_operation_id'
export const PayAddressRequestOptionalFields: PayAddressRequestOptionalField[] = ['swap_operation_id']
export type PayAddressRequestOptions = OptionsBaseMessage & { export type PayAddressRequestOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: [] checkOptionalsAreSet?: PayAddressRequestOptionalField[]
address_CustomCheck?: (v: string) => boolean address_CustomCheck?: (v: string) => boolean
amoutSats_CustomCheck?: (v: number) => boolean amoutSats_CustomCheck?: (v: number) => boolean
satsPerVByte_CustomCheck?: (v: number) => boolean satsPerVByte_CustomCheck?: (v: number) => boolean
swap_operation_id_CustomCheck?: (v?: string) => boolean
} }
export const PayAddressRequestValidate = (o?: PayAddressRequest, opts: PayAddressRequestOptions = {}, path: string = 'PayAddressRequest::root.'): Error | null => { export const PayAddressRequestValidate = (o?: PayAddressRequest, opts: PayAddressRequestOptions = {}, path: string = 'PayAddressRequest::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')
@ -3153,6 +3160,9 @@ export const PayAddressRequestValidate = (o?: PayAddressRequest, opts: PayAddres
if (typeof o.satsPerVByte !== 'number') return new Error(`${path}.satsPerVByte: is not a number`) if (typeof o.satsPerVByte !== 'number') return new Error(`${path}.satsPerVByte: is not a number`)
if (opts.satsPerVByte_CustomCheck && !opts.satsPerVByte_CustomCheck(o.satsPerVByte)) return new Error(`${path}.satsPerVByte: custom check failed`) if (opts.satsPerVByte_CustomCheck && !opts.satsPerVByte_CustomCheck(o.satsPerVByte)) return new Error(`${path}.satsPerVByte: custom check failed`)
if ((o.swap_operation_id || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('swap_operation_id')) && 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 return null
} }
@ -3744,6 +3754,62 @@ export const SingleMetricReqValidate = (o?: SingleMetricReq, opts: SingleMetricR
return null return null
} }
export type TransactionSwapQuote = {
chain_fee_sats: number
invoice_amount_sats: number
swap_fee_sats: number
swap_operation_id: string
transaction_amount_sats: number
}
export const TransactionSwapQuoteOptionalFields: [] = []
export type TransactionSwapQuoteOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
chain_fee_sats_CustomCheck?: (v: number) => boolean
invoice_amount_sats_CustomCheck?: (v: number) => boolean
swap_fee_sats_CustomCheck?: (v: number) => boolean
swap_operation_id_CustomCheck?: (v: string) => boolean
transaction_amount_sats_CustomCheck?: (v: number) => boolean
}
export const TransactionSwapQuoteValidate = (o?: TransactionSwapQuote, opts: TransactionSwapQuoteOptions = {}, path: string = 'TransactionSwapQuote::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.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.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.swap_fee_sats !== 'number') return new Error(`${path}.swap_fee_sats: is not a number`)
if (opts.swap_fee_sats_CustomCheck && !opts.swap_fee_sats_CustomCheck(o.swap_fee_sats)) return new Error(`${path}.swap_fee_sats: custom check failed`)
if (typeof o.swap_operation_id !== 'string') return new Error(`${path}.swap_operation_id: is not a string`)
if (opts.swap_operation_id_CustomCheck && !opts.swap_operation_id_CustomCheck(o.swap_operation_id)) return new Error(`${path}.swap_operation_id: custom check failed`)
if (typeof o.transaction_amount_sats !== 'number') return new Error(`${path}.transaction_amount_sats: is not a number`)
if (opts.transaction_amount_sats_CustomCheck && !opts.transaction_amount_sats_CustomCheck(o.transaction_amount_sats)) return new Error(`${path}.transaction_amount_sats: custom check failed`)
return null
}
export type TransactionSwapRequest = {
transaction_amount_sats: number
}
export const TransactionSwapRequestOptionalFields: [] = []
export type TransactionSwapRequestOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
transaction_amount_sats_CustomCheck?: (v: number) => boolean
}
export const TransactionSwapRequestValidate = (o?: TransactionSwapRequest, opts: TransactionSwapRequestOptions = {}, path: string = 'TransactionSwapRequest::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.transaction_amount_sats !== 'number') return new Error(`${path}.transaction_amount_sats: is not a number`)
if (opts.transaction_amount_sats_CustomCheck && !opts.transaction_amount_sats_CustomCheck(o.transaction_amount_sats)) return new Error(`${path}.transaction_amount_sats: custom check failed`)
return null
}
export type UpdateChannelPolicyRequest = { export type UpdateChannelPolicyRequest = {
policy: ChannelPolicy policy: ChannelPolicy
update: UpdateChannelPolicyRequest_update update: UpdateChannelPolicyRequest_update

View file

@ -496,6 +496,13 @@ service LightningPub {
option (nostr) = true; option (nostr) = true;
} }
rpc GetTransactionSwapQuote(structs.TransactionSwapRequest) returns (structs.TransactionSwapQuote){
option (auth_type) = "User";
option (http_method) = "post";
option (http_route) = "/api/user/swap/quote";
option (nostr) = true;
}
rpc NewInvoice(structs.NewInvoiceRequest) returns (structs.NewInvoiceResponse){ rpc NewInvoice(structs.NewInvoiceRequest) returns (structs.NewInvoiceResponse){
option (auth_type) = "User"; option (auth_type) = "User";
option (http_method) = "post"; option (http_method) = "post";

View file

@ -432,6 +432,7 @@ message PayAddressRequest{
string address = 1; string address = 1;
int64 amoutSats = 2; int64 amoutSats = 2;
int64 satsPerVByte = 3; int64 satsPerVByte = 3;
optional string swap_operation_id = 4;
} }
message PayAddressResponse{ message PayAddressResponse{
@ -824,3 +825,15 @@ message MessagingToken {
string device_id = 1; string device_id = 1;
string firebase_messaging_token = 2; string firebase_messaging_token = 2;
} }
message TransactionSwapRequest {
int64 transaction_amount_sats = 2;
}
message TransactionSwapQuote {
string swap_operation_id = 1;
int64 swap_fee_sats = 2;
int64 invoice_amount_sats = 3;
int64 transaction_amount_sats = 4;
int64 chain_fee_sats = 5;
}

View file

@ -47,7 +47,6 @@ const start = async () => {
adminManager.setAppNprofile(appNprofile) adminManager.setAppNprofile(appNprofile)
const Server = NewServer(serverMethods, serverOptions(mainHandler)) const Server = NewServer(serverMethods, serverOptions(mainHandler))
Server.Listen(mainHandler.settings.getSettings().serviceSettings.servicePort) Server.Listen(mainHandler.settings.getSettings().serviceSettings.servicePort)
await TMP_swapTest_TMP(mainHandler) // TMP -- remove this
} }
start() start()
@ -66,15 +65,3 @@ const exitHandler = async (kill: () => void) => {
process.exit(99); process.exit(99);
}); });
} }
const TMP_swapTest_TMP = async (mainHandler: Main) => {
await new Promise(resolve => setTimeout(resolve, 10000))
/* const i = await mainHandler.lnd.NewInvoice(25000, 'test', 3600, { useProvider: true, from: 'user' })
console.log('Invoice created with provider destination', i.providerDst)
const decoded = await mainHandler.lnd.DecodeInvoice(i.payRequest)
await mainHandler.paymentManager.swaps.SwapInvoice(i.payRequest, decoded.paymentHash) */
const a = await mainHandler.lnd.NewAddress(Types.AddressType.WITNESS_PUBKEY_HASH, { useProvider: false, from: 'user' })
console.log('Address created', a.address)
await mainHandler.paymentManager.swaps.SwapTransaction(a.address, 25000, networks.bitcoin)
}

View file

@ -4,7 +4,8 @@ import { crypto, initEccLib, Transaction, address, Network } from 'bitcoinjs-lib
// import bolt11 from 'bolt11'; // import bolt11 from 'bolt11';
import { import {
Musig, SwapTreeSerializer, TaprootUtils, detectSwap, Musig, SwapTreeSerializer, TaprootUtils, detectSwap,
constructClaimTransaction, targetFee, OutputType constructClaimTransaction, targetFee, OutputType,
Networks,
} from 'boltz-core'; } from 'boltz-core';
import { randomBytes, createHash } from 'crypto'; import { randomBytes, createHash } from 'crypto';
import { ECPairFactory, ECPairInterface } from 'ecpair'; import { ECPairFactory, ECPairInterface } from 'ecpair';
@ -12,22 +13,57 @@ import * as ecc from 'tiny-secp256k1';
import ws from 'ws'; import ws from 'ws';
import { getLogger, PubLogger, ERROR } from '../helpers/logger.js'; import { getLogger, PubLogger, ERROR } from '../helpers/logger.js';
import SettingsManager from '../main/settingsManager.js'; import SettingsManager from '../main/settingsManager.js';
import * as Types from '../../../proto/autogenerated/ts/types.js';
type InvoiceSwapResponse = { id: string, claimPublicKey: string, swapTree: string } type InvoiceSwapResponse = { id: string, claimPublicKey: string, swapTree: string }
type InvoiceSwapInfo = { paymentHash: string, keys: ECPairInterface } type InvoiceSwapInfo = { paymentHash: string, keys: ECPairInterface }
type InvoiceSwapData = { createdResponse: InvoiceSwapResponse, info: InvoiceSwapInfo } type InvoiceSwapData = { createdResponse: InvoiceSwapResponse, info: InvoiceSwapInfo }
type TransactionSwapResponse = { id: string, refundPublicKey: string, swapTree: string } type TransactionSwapFees = {
type TransactionSwapInfo = { destinationAddress: string, network: Network, preimage: Buffer, keys: ECPairInterface, txHex: string } percentage: number,
type TransactionSwapData = { createdResponse: TransactionSwapResponse, info: TransactionSwapInfo } minerFees: {
claim: number,
lockup: number,
}
}
type TransactionSwapFeesRes = {
BTC?: {
BTC?: {
fees: TransactionSwapFees
}
}
}
type TransactionSwapResponse = {
id: string, refundPublicKey: string, swapTree: string,
timeoutBlockHeight: number, lockupAddress: string, invoice: string,
onchainAmount?: number
}
type TransactionSwapInfo = { destinationAddress: string, preimage: Buffer, keys: ECPairInterface, chainFee: number }
export type TransactionSwapData = { createdResponse: TransactionSwapResponse, info: TransactionSwapInfo }
const network = Networks.bitcoinMainnet
export class Swaps { export class Swaps {
reverseSwaps: ReverseSwaps
submarineSwaps: SubmarineSwaps
constructor(settings: SettingsManager) {
this.reverseSwaps = new ReverseSwaps(settings)
this.submarineSwaps = new SubmarineSwaps(settings)
}
GetKeys = (privateKey: string) => {
const keys = ECPairFactory(ecc).fromPrivateKey(Buffer.from(privateKey, 'hex'))
return keys
}
}
export class SubmarineSwaps {
settings: SettingsManager settings: SettingsManager
log: PubLogger log: PubLogger
constructor(settings: SettingsManager) { constructor(settings: SettingsManager) {
this.settings = settings this.settings = settings
this.log = getLogger({ component: 'SwapsService' }) this.log = getLogger({ component: 'SwapsService' })
initEccLib(ecc)
} }
SwapInvoice = async (invoice: string, paymentHash: string) => { SwapInvoice = async (invoice: string, paymentHash: string) => {
@ -40,11 +76,11 @@ export class Swaps {
const req = { invoice, to: 'BTC', from: 'BTC', refundPublicKey } const req = { invoice, to: 'BTC', from: 'BTC', refundPublicKey }
const url = `${this.settings.getSettings().swapsSettings.boltzHttpUrl}/v2/swap/submarine` const url = `${this.settings.getSettings().swapsSettings.boltzHttpUrl}/v2/swap/submarine`
this.log('Sending invoice swap request to', url); this.log('Sending invoice swap request to', url);
const createdResponse = await loggedPost<InvoiceSwapResponse>(this.log, url, req) const createdResponseRes = await loggedPost<InvoiceSwapResponse>(this.log, url, req)
if (!createdResponse) { if (!createdResponseRes.ok) {
return; return createdResponseRes
} }
const createdResponse = createdResponseRes.data
this.log('Created invoice swap'); this.log('Created invoice swap');
this.log(createdResponse); this.log(createdResponse);
@ -98,11 +134,11 @@ export class Swaps {
const { boltzHttpUrl } = this.settings.getSettings().swapsSettings const { boltzHttpUrl } = this.settings.getSettings().swapsSettings
// Get the information request to create a partial signature // Get the information request to create a partial signature
const url = `${boltzHttpUrl}/v2/swap/submarine/${createdResponse.id}/claim` const url = `${boltzHttpUrl}/v2/swap/submarine/${createdResponse.id}/claim`
const claimTxDetails = await loggedGet<{ preimage: string, transactionHash: string, pubNonce: string }>(this.log, url) const claimTxDetailsRes = await loggedGet<{ preimage: string, transactionHash: string, pubNonce: string }>(this.log, url)
if (!claimTxDetails) { if (!claimTxDetailsRes.ok) {
return; return claimTxDetailsRes
} }
const claimTxDetails = claimTxDetailsRes.data
// Verify that Boltz actually paid the invoice by comparing the preimage hash // Verify that Boltz actually paid the invoice by comparing the preimage hash
// of the invoice to the SHA256 hash of the preimage from the response // of the invoice to the SHA256 hash of the preimage from the response
const claimTxPreimageHash = createHash('sha256').update(Buffer.from(claimTxDetails.preimage, 'hex')).digest() const claimTxPreimageHash = createHash('sha256').update(Buffer.from(claimTxDetails.preimage, 'hex')).digest()
@ -141,53 +177,111 @@ export class Swaps {
pubNonce: Buffer.from(musig.getPublicNonce()).toString('hex'), pubNonce: Buffer.from(musig.getPublicNonce()).toString('hex'),
partialSignature: Buffer.from(musig.signPartial()).toString('hex'), partialSignature: Buffer.from(musig.signPartial()).toString('hex'),
} }
const claimResponse = await loggedPost<{ pubNonce: string, partialSignature: string }>(this.log, claimUrl, claimReq) const claimResponseRes = await loggedPost<{ pubNonce: string, partialSignature: string }>(this.log, claimUrl, claimReq)
if (!claimResponse) { if (!claimResponseRes.ok) {
return; return claimResponseRes
} }
const claimResponse = claimResponseRes.data
this.log('Claim response', claimResponse) this.log('Claim response', claimResponse)
} }
}
export class ReverseSwaps {
settings: SettingsManager
log: PubLogger
constructor(settings: SettingsManager) {
this.settings = settings
this.log = getLogger({ component: 'SwapsService' })
initEccLib(ecc)
}
SwapTransaction = async (destinationAddress: string, invoiceAmount: number, network: Network) => { calculateFees = (fees: TransactionSwapFees, receiveAmount: number) => {
const pct = fees.percentage / 100
const minerFee = fees.minerFees.claim + fees.minerFees.lockup
const preFee = receiveAmount + minerFee
const fee = Math.ceil(preFee * pct)
const total = preFee + fee
return { total, fee, minerFee }
}
GetFees = async (): Promise<{ ok: true, fees: TransactionSwapFees, } | { ok: false, error: string }> => {
const url = `${this.settings.getSettings().swapsSettings.boltzHttpUrl}/v2/swap/reverse`
const feesRes = await loggedGet<TransactionSwapFeesRes>(this.log, url)
if (!feesRes.ok) {
return { ok: false, error: feesRes.error }
}
if (!feesRes.data.BTC?.BTC?.fees) {
return { ok: false, error: 'No fees found for BTC to BTC swap' }
}
return { ok: true, fees: feesRes.data.BTC.BTC.fees }
}
SwapTransaction = async (txAmount: number): Promise<{ ok: true, createdResponse: TransactionSwapResponse, preimage: string, pubkey: string, privKey: string } | { ok: false, error: string }> => {
if (!this.settings.getSettings().swapsSettings.enableSwaps) { if (!this.settings.getSettings().swapsSettings.enableSwaps) {
this.log(ERROR, 'Swaps are not enabled'); this.log(ERROR, 'Swaps are not enabled');
return; return { ok: false, error: 'Swaps are not enabled' }
} }
const preimage = randomBytes(32); const preimage = randomBytes(32);
const keys = ECPairFactory(ecc).makeRandom() const keys = ECPairFactory(ecc).makeRandom()
if (!keys.privateKey) {
return { ok: false, error: 'Failed to generate keys' }
}
const url = `${this.settings.getSettings().swapsSettings.boltzHttpUrl}/v2/swap/reverse` const url = `${this.settings.getSettings().swapsSettings.boltzHttpUrl}/v2/swap/reverse`
const req = { const req: any = {
onchainAmount: invoiceAmount, onchainAmount: txAmount,
// invoiceAmount,
to: 'BTC', to: 'BTC',
from: 'BTC', from: 'BTC',
claimPublicKey: Buffer.from(keys.publicKey).toString('hex'), claimPublicKey: Buffer.from(keys.publicKey).toString('hex'),
preimageHash: createHash('sha256').update(preimage).digest('hex'), preimageHash: createHash('sha256').update(preimage).digest('hex'),
} }
const createdResponse = await loggedPost<TransactionSwapResponse>(this.log, url, req) /* if (amount.type === Types.TransactionSwapRequest_amount_type.INVOICE_AMOUNT_SATS) {
if (!createdResponse) { req.invoiceAmount = amount.invoice_amount_sats
return; } else if (amount.type === Types.TransactionSwapRequest_amount_type.TRANSACTION_AMOUNT_SATS) {
req.onchainAmount = amount.transaction_amount_sats
} */
const createdResponseRes = await loggedPost<TransactionSwapResponse>(this.log, url, req)
if (!createdResponseRes.ok) {
return createdResponseRes
} }
const createdResponse = createdResponseRes.data
this.log('Created transaction swap'); this.log('Created transaction swap');
this.log(createdResponse); this.log(createdResponse);
return {
ok: true, createdResponse,
preimage: Buffer.from(preimage).toString('hex'),
pubkey: Buffer.from(keys.publicKey).toString('hex'),
privKey: Buffer.from(keys.privateKey).toString('hex')
}
}
SubscribeToTransactionSwap = async (data: TransactionSwapData) => {
const webSocket = new ws(`${this.settings.getSettings().swapsSettings.boltzWebSocketUrl}/v2/ws`) const webSocket = new ws(`${this.settings.getSettings().swapsSettings.boltzWebSocketUrl}/v2/ws`)
const subReq = { op: 'subscribe', channel: 'swap.update', args: [createdResponse.id] } const subReq = { op: 'subscribe', channel: 'swap.update', args: [data.createdResponse.id] }
webSocket.on('open', () => { webSocket.on('open', () => {
webSocket.send(JSON.stringify(subReq)) webSocket.send(JSON.stringify(subReq))
}) })
return new Promise<string>((resolve, reject) => {
const done = (txId: string) => {
webSocket.close()
resolve(txId)
}
webSocket.on('message', async (rawMsg) => { webSocket.on('message', async (rawMsg) => {
try { try {
await this.handleSwapTransactionMessage(rawMsg, { createdResponse, info: { destinationAddress, network, preimage, keys, txHex: '' } }, () => webSocket.close()) await this.handleSwapTransactionMessage(rawMsg, data, done)
} catch (err: any) { } catch (err: any) {
this.log(ERROR, 'Error handling transaction WebSocket message', err.message) this.log(ERROR, 'Error handling transaction WebSocket message', err.message)
webSocket.close() webSocket.close()
reject(err)
return return
} }
}) })
})
} }
handleSwapTransactionMessage = async (rawMsg: ws.RawData, data: TransactionSwapData, closeWebSocket: () => void) => { handleSwapTransactionMessage = async (rawMsg: ws.RawData, data: TransactionSwapData, done: (txId: string) => void) => {
const msg = JSON.parse(rawMsg.toString('utf-8')); const msg = JSON.parse(rawMsg.toString('utf-8'));
if (msg.event !== 'update') { if (msg.event !== 'update') {
return; return;
@ -195,7 +289,7 @@ export class Swaps {
this.log('Got WebSocket update'); this.log('Got WebSocket update');
this.log(msg); this.log(msg);
let txId = ""
switch (msg.args[0].status) { switch (msg.args[0].status) {
// "swap.created" means Boltz is waiting for the invoice to be paid // "swap.created" means Boltz is waiting for the invoice to be paid
case 'swap.created': case 'swap.created':
@ -204,20 +298,23 @@ export class Swaps {
// "transaction.mempool" means that Boltz sent an onchain transaction // "transaction.mempool" means that Boltz sent an onchain transaction
case 'transaction.mempool': case 'transaction.mempool':
data.info.txHex = msg.args[0].transaction.hex const txIdRes = await this.handleTransactionMempool(data, msg.args[0].transaction.hex)
await this.handleTransactionMempool(data) if (!txIdRes.ok) {
throw new Error(txIdRes.error)
}
txId = txIdRes.txId
return return
case 'invoice.settled': case 'invoice.settled':
this.log('Transaction swap successful'); this.log('Transaction swap successful');
closeWebSocket() done(txId)
return; return;
} }
} }
handleTransactionMempool = async (data: TransactionSwapData) => { handleTransactionMempool = async (data: TransactionSwapData, txHex: string): Promise<{ ok: true, txId: string } | { ok: false, error: string }> => {
this.log('Creating claim transaction'); this.log('Creating claim transaction');
const { createdResponse, info } = data const { createdResponse, info } = data
const { destinationAddress, network, keys, preimage, txHex } = info const { destinationAddress, keys, preimage, chainFee } = info
const boltzPublicKey = Buffer.from( const boltzPublicKey = Buffer.from(
createdResponse.refundPublicKey, createdResponse.refundPublicKey,
'hex', 'hex',
@ -238,12 +335,11 @@ export class Swaps {
const swapOutput = detectSwap(tweakedKey, lockupTx); const swapOutput = detectSwap(tweakedKey, lockupTx);
if (swapOutput === undefined) { if (swapOutput === undefined) {
this.log(ERROR, 'No swap output found in lockup transaction'); this.log(ERROR, 'No swap output found in lockup transaction');
return; return { ok: false, error: 'No swap output found in lockup transaction' }
} }
// Create a claim transaction to be signed cooperatively via a key path spend // Create a claim transaction to be signed cooperatively via a key path spend
const claimTx = targetFee(2, (fee) => const claimTx = constructClaimTransaction(
constructClaimTransaction(
[ [
{ {
...swapOutput, ...swapOutput,
@ -255,9 +351,8 @@ export class Swaps {
}, },
], ],
address.toOutputScript(destinationAddress, network), address.toOutputScript(destinationAddress, network),
fee, chainFee,
), )
);
const { boltzHttpUrl } = this.settings.getSettings().swapsSettings const { boltzHttpUrl } = this.settings.getSettings().swapsSettings
// Get the partial signature from Boltz // Get the partial signature from Boltz
const claimUrl = `${boltzHttpUrl}/v2/swap/reverse/${createdResponse.id}/claim` const claimUrl = `${boltzHttpUrl}/v2/swap/reverse/${createdResponse.id}/claim`
@ -267,10 +362,11 @@ export class Swaps {
preimage: preimage.toString('hex'), preimage: preimage.toString('hex'),
pubNonce: Buffer.from(musig.getPublicNonce()).toString('hex'), pubNonce: Buffer.from(musig.getPublicNonce()).toString('hex'),
} }
const boltzSig = await loggedPost<{ pubNonce: string, partialSignature: string }>(this.log, claimUrl, claimReq) const boltzSigRes = await loggedPost<{ pubNonce: string, partialSignature: string }>(this.log, claimUrl, claimReq)
if (!boltzSig) { if (!boltzSigRes.ok) {
return; return boltzSigRes
} }
const boltzSig = boltzSigRes.data
// Aggregate the nonces // Aggregate the nonces
musig.aggregateNonces([ musig.aggregateNonces([
@ -304,38 +400,41 @@ export class Swaps {
const broadcastReq = { const broadcastReq = {
hex: claimTx.toHex(), hex: claimTx.toHex(),
} }
const broadcastResponse = await loggedPost(this.log, broadcastUrl, broadcastReq)
if (!broadcastResponse) { const broadcastResponse = await loggedPost<any>(this.log, broadcastUrl, broadcastReq)
return; if (!broadcastResponse.ok) {
return broadcastResponse
} }
this.log('Transaction broadcasted', broadcastResponse) this.log('Transaction broadcasted', broadcastResponse.data)
const txId = claimTx.getId()
return { ok: true, txId }
} }
} }
const loggedPost = async <T>(log: PubLogger, url: string, req: any): Promise<T | null> => { const loggedPost = async <T>(log: PubLogger, url: string, req: any): Promise<{ ok: true, data: T } | { ok: false, error: string }> => {
try { try {
const { data } = await axios.post(url, req) const { data } = await axios.post(url, req)
return data as T return { ok: true, data: data as T }
} catch (err: any) { } catch (err: any) {
if (err.response?.data) { if (err.response?.data) {
log(ERROR, 'Error sending request', err.response.data) log(ERROR, 'Error sending request', err.response.data)
return null return { ok: false, error: err.response.data }
} }
log(ERROR, 'Error sending request', err.message) log(ERROR, 'Error sending request', err.message)
return null return { ok: false, error: err.message }
} }
} }
const loggedGet = async <T>(log: PubLogger, url: string): Promise<T | null> => { const loggedGet = async <T>(log: PubLogger, url: string): Promise<{ ok: true, data: T } | { ok: false, error: string }> => {
try { try {
const { data } = await axios.get(url) const { data } = await axios.get(url)
return data as T return { ok: true, data: data as T }
} catch (err: any) { } catch (err: any) {
if (err.response?.data) { if (err.response?.data) {
log(ERROR, 'Error getting request', err.response.data) log(ERROR, 'Error getting request', err.response.data)
return null return { ok: false, error: err.response.data }
} }
log(ERROR, 'Error getting request', err.message) log(ERROR, 'Error getting request', err.message)
return null return { ok: false, error: err.message }
} }
} }

View file

@ -458,5 +458,3 @@ export default class {
this.nostrReset(s) this.nostrReset(s)
} }
} }

View file

@ -17,7 +17,7 @@ import { LiquidityManager } from './liquidityManager.js'
import { Utils } from '../helpers/utilsWrapper.js' import { Utils } from '../helpers/utilsWrapper.js'
import { UserInvoicePayment } from '../storage/entity/UserInvoicePayment.js' import { UserInvoicePayment } from '../storage/entity/UserInvoicePayment.js'
import SettingsManager from './settingsManager.js' import SettingsManager from './settingsManager.js'
import { Swaps } from '../lnd/swaps.js' import { Swaps, TransactionSwapData } from '../lnd/swaps.js'
interface UserOperationInfo { interface UserOperationInfo {
serial_id: number serial_id: number
paid_amount: number paid_amount: number
@ -251,7 +251,7 @@ export default class {
} }
} }
async PayInvoice(userId: string, req: Types.PayInvoiceRequest, linkedApplication: Application): Promise<Types.PayInvoiceResponse> { async PayInvoice(userId: string, req: Types.PayInvoiceRequest, linkedApplication: Application, swapOperationId?: string): Promise<Types.PayInvoiceResponse> {
await this.watchDog.PaymentRequested() await this.watchDog.PaymentRequested()
const maybeBanned = await this.storage.userStorage.GetUser(userId) const maybeBanned = await this.storage.userStorage.GetUser(userId)
if (maybeBanned.locked) { if (maybeBanned.locked) {
@ -279,7 +279,7 @@ export default class {
if (internalInvoice) { if (internalInvoice) {
paymentInfo = await this.PayInternalInvoice(userId, internalInvoice, { payAmount, serviceFee }, linkedApplication, req.debit_npub) paymentInfo = await this.PayInternalInvoice(userId, internalInvoice, { payAmount, serviceFee }, linkedApplication, req.debit_npub)
} else { } else {
paymentInfo = await this.PayExternalInvoice(userId, req.invoice, { payAmount, serviceFee, amountForLnd: req.amount }, linkedApplication, req.debit_npub) paymentInfo = await this.PayExternalInvoice(userId, req.invoice, { payAmount, serviceFee, amountForLnd: req.amount }, linkedApplication, { debitNpub: req.debit_npub, swapOperationId })
} }
if (isAppUserPayment && serviceFee > 0) { if (isAppUserPayment && serviceFee > 0) {
await this.storage.userStorage.IncrementUserBalance(linkedApplication.owner.user_id, serviceFee, "fees") await this.storage.userStorage.IncrementUserBalance(linkedApplication.owner.user_id, serviceFee, "fees")
@ -295,7 +295,8 @@ export default class {
} }
} }
async PayExternalInvoice(userId: string, invoice: string, amounts: { payAmount: number, serviceFee: number, amountForLnd: number }, linkedApplication: Application, debitNpub?: string) { async PayExternalInvoice(userId: string, invoice: string, amounts: { payAmount: number, serviceFee: number, amountForLnd: number }, linkedApplication: Application, optionals: { debitNpub?: string, swapOperationId?: string } = {}) {
if (this.settings.getSettings().serviceSettings.disableExternalPayments) { if (this.settings.getSettings().serviceSettings.disableExternalPayments) {
throw new Error("something went wrong sending payment, please try again later") throw new Error("something went wrong sending payment, please try again later")
} }
@ -315,7 +316,7 @@ export default class {
const provider = use === 'provider' ? this.lnd.liquidProvider.GetProviderDestination() : undefined const provider = use === 'provider' ? this.lnd.liquidProvider.GetProviderDestination() : undefined
const pendingPayment = await this.storage.StartTransaction(async tx => { const pendingPayment = await this.storage.StartTransaction(async tx => {
await this.storage.userStorage.DecrementUserBalance(userId, totalAmountToDecrement + routingFeeLimit, invoice, tx) await this.storage.userStorage.DecrementUserBalance(userId, totalAmountToDecrement + routingFeeLimit, invoice, tx)
return await this.storage.paymentStorage.AddPendingExternalPayment(userId, invoice, { payAmount, serviceFee, networkFee: routingFeeLimit }, linkedApplication, provider, tx, debitNpub) return await this.storage.paymentStorage.AddPendingExternalPayment(userId, invoice, { payAmount, serviceFee, networkFee: routingFeeLimit }, linkedApplication, provider, tx, optionals)
}, "payment started") }, "payment started")
this.log("ready to pay") this.log("ready to pay")
try { try {
@ -358,51 +359,132 @@ export default class {
} }
async GetTransactionSwapQuote(ctx: Types.UserContext, req: Types.TransactionSwapRequest): Promise<Types.TransactionSwapQuote> {
const feesRes = await this.swaps.reverseSwaps.GetFees()
if (!feesRes.ok) {
throw new Error(feesRes.error)
}
const { claim, lockup } = feesRes.fees.minerFees
const minerFee = claim + lockup
const chainTotal = req.transaction_amount_sats + minerFee
const res = await this.swaps.reverseSwaps.SwapTransaction(chainTotal)
if (!res.ok) {
throw new Error(res.error)
}
const decoded = await this.lnd.DecodeInvoice(res.createdResponse.invoice)
const swapFee = decoded.numSatoshis - chainTotal
const newSwap = await this.storage.paymentStorage.AddTransactionSwap({
app_user_id: ctx.app_user_id,
swap_quote_id: res.createdResponse.id,
swap_tree: JSON.stringify(res.createdResponse.swapTree),
lockup_address: res.createdResponse.lockupAddress,
refund_public_key: res.createdResponse.refundPublicKey,
timeout_block_height: res.createdResponse.timeoutBlockHeight,
invoice: res.createdResponse.invoice,
invoice_amount: decoded.numSatoshis,
transaction_amount: chainTotal,
swap_fee_sats: swapFee,
chain_fee_sats: minerFee,
preimage: res.preimage,
ephemeral_private_key: res.privKey,
ephemeral_public_key: res.pubkey,
})
return {
swap_operation_id: newSwap.swap_operation_id,
swap_fee_sats: swapFee,
invoice_amount_sats: decoded.numSatoshis,
transaction_amount_sats: req.transaction_amount_sats,
chain_fee_sats: minerFee,
}
}
async PayAddress(ctx: Types.UserContext, req: Types.PayAddressRequest): Promise<Types.PayAddressResponse> { async PayAddress(ctx: Types.UserContext, req: Types.PayAddressRequest): Promise<Types.PayAddressResponse> {
throw new Error("address payment currently disabled, use Lightning instead")
await this.watchDog.PaymentRequested() await this.watchDog.PaymentRequested()
this.log("paying address", req.address, "for user", ctx.user_id, "with amount", req.amoutSats) this.log("paying address", req.address, "for user", ctx.user_id, "with amount", req.amoutSats)
const maybeBanned = await this.storage.userStorage.GetUser(ctx.user_id) const maybeBanned = await this.storage.userStorage.GetUser(ctx.user_id)
if (maybeBanned.locked) { if (maybeBanned.locked) {
throw new Error("user is banned, cannot send chain tx") throw new Error("user is banned, cannot send chain tx")
} }
const internalAddress = await this.storage.paymentStorage.GetAddressOwner(req.address)
if (internalAddress) {
return this.PayInternalAddress(ctx, req)
}
return this.PayAddressWithSwap(ctx, req)
}
async PayAddressWithSwap(ctx: Types.UserContext, req: Types.PayAddressRequest): Promise<Types.PayAddressResponse> {
this.log("paying external address")
if (!req.swap_operation_id) {
throw new Error("request a swap quote before payng an external address")
}
const app = await this.storage.applicationStorage.GetApplication(ctx.app_id)
const txSwap = await this.storage.paymentStorage.GetTransactionSwap(req.swap_operation_id)
if (!txSwap) {
throw new Error("swap quote not found")
}
const keys = this.swaps.GetKeys(txSwap.ephemeral_private_key)
const data: TransactionSwapData = {
createdResponse: {
id: txSwap.swap_quote_id,
invoice: txSwap.invoice,
lockupAddress: txSwap.lockup_address,
refundPublicKey: txSwap.refund_public_key,
swapTree: txSwap.swap_tree,
timeoutBlockHeight: txSwap.timeout_block_height,
onchainAmount: txSwap.transaction_amount,
},
info: {
destinationAddress: req.address,
keys,
chainFee: txSwap.chain_fee_sats,
preimage: Buffer.from(txSwap.preimage, 'hex'),
}
}
const swapPromise = this.swaps.reverseSwaps.SubscribeToTransactionSwap(data)
const payment = await this.PayInvoice(ctx.user_id, { amount: 0, invoice: txSwap.invoice }, app, req.swap_operation_id)
let txId = ""
try {
txId = await swapPromise
await this.storage.paymentStorage.FinalizeTransactionSwap(req.swap_operation_id, txId)
} catch (err: any) {
await this.storage.paymentStorage.FailTransactionSwap(req.swap_operation_id, err.message)
throw err
}
const networkFeesTotal = txSwap.chain_fee_sats + txSwap.swap_fee_sats + payment.network_fee
return {
txId: txId,
network_fee: networkFeesTotal,
service_fee: payment.service_fee,
operation_id: payment.operation_id,
}
}
async PayInternalAddress(ctx: Types.UserContext, req: Types.PayAddressRequest): Promise<Types.PayAddressResponse> {
this.log("paying internal address")
if (req.swap_operation_id) {
await this.storage.paymentStorage.DeleteTransactionSwap(req.swap_operation_id)
}
const { blockHeight } = await this.lnd.GetInfo() const { blockHeight } = await this.lnd.GetInfo()
const app = await this.storage.applicationStorage.GetApplication(ctx.app_id) const app = await this.storage.applicationStorage.GetApplication(ctx.app_id)
const serviceFee = this.getServiceFee(Types.UserOperationType.OUTGOING_TX, req.amoutSats, false) const serviceFee = this.getServiceFee(Types.UserOperationType.OUTGOING_TX, req.amoutSats, false)
const isAppUserPayment = ctx.user_id !== app.owner.user_id const isAppUserPayment = ctx.user_id !== app.owner.user_id
const internalAddress = await this.storage.paymentStorage.GetAddressOwner(req.address)
let txId = "" const txId = crypto.randomBytes(32).toString("hex")
let chainFees = 0
if (!internalAddress) {
this.log("paying external address")
const estimate = await this.lnd.EstimateChainFees(req.address, req.amoutSats, 1)
const vBytes = Math.ceil(Number(estimate.feeSat / estimate.satPerVbyte))
chainFees = vBytes * req.satsPerVByte
const total = req.amoutSats + chainFees
// WARNING, before re-enabling this, make sure to add the tx_hash to the DecrementUserBalance "reason"!!
this.storage.userStorage.DecrementUserBalance(ctx.user_id, total + serviceFee, req.address)
try {
const payment = await this.lnd.PayAddress(req.address, req.amoutSats, req.satsPerVByte, "", { useProvider: false, from: 'user' })
txId = payment.txid
} catch (err) {
// WARNING, before re-enabling this, make sure to add the tx_hash to the IncrementUserBalance "reason"!!
await this.storage.userStorage.IncrementUserBalance(ctx.user_id, total + serviceFee, req.address)
throw err
}
} else {
this.log("paying internal address")
txId = crypto.randomBytes(32).toString("hex")
const addressData = `${req.address}:${txId}` const addressData = `${req.address}:${txId}`
await this.storage.userStorage.DecrementUserBalance(ctx.user_id, req.amoutSats + serviceFee, addressData) await this.storage.userStorage.DecrementUserBalance(ctx.user_id, req.amoutSats + serviceFee, addressData)
this.addressPaidCb({ hash: txId, index: 0 }, req.address, req.amoutSats, 'internal') this.addressPaidCb({ hash: txId, index: 0 }, req.address, req.amoutSats, 'internal')
}
if (isAppUserPayment && serviceFee > 0) { if (isAppUserPayment && serviceFee > 0) {
await this.storage.userStorage.IncrementUserBalance(app.owner.user_id, serviceFee, 'fees') await this.storage.userStorage.IncrementUserBalance(app.owner.user_id, serviceFee, 'fees')
} }
const chainFees = 0
const newTx = await this.storage.paymentStorage.AddUserTransactionPayment(ctx.user_id, req.address, txId, 0, req.amoutSats, chainFees, serviceFee, !!internalAddress, blockHeight, app) const internalAddress = true
const newTx = await this.storage.paymentStorage.AddUserTransactionPayment(ctx.user_id, req.address, txId, 0, req.amoutSats, chainFees, serviceFee, internalAddress, blockHeight, app)
const user = await this.storage.userStorage.GetUser(ctx.user_id) const user = await this.storage.userStorage.GetUser(ctx.user_id)
const txData = `${newTx.address}:${newTx.tx_hash}` const txData = `${newTx.address}:${newTx.tx_hash}`
this.storage.eventsLog.LogEvent({ type: 'address_payment', userId: ctx.user_id, appId: app.app_id, appUserId: "", balance: user.balance_sats, data: txData, amount: req.amoutSats }) this.storage.eventsLog.LogEvent({ type: 'address_payment', userId: ctx.user_id, appId: app.app_id, appUserId: "", balance: user.balance_sats, data: txData, amount: req.amoutSats })

View file

@ -142,11 +142,14 @@ export default (mainHandler: Main): Types.ServerMethods => {
const err = Types.PayAddressRequestValidate(req, { const err = Types.PayAddressRequestValidate(req, {
address_CustomCheck: addr => addr !== '', address_CustomCheck: addr => addr !== '',
amoutSats_CustomCheck: amt => amt > 0, amoutSats_CustomCheck: amt => amt > 0,
satsPerVByte_CustomCheck: spb => spb > 0 // satsPerVByte_CustomCheck: spb => spb > 0
}) })
if (err != null) throw new Error(err.message) if (err != null) throw new Error(err.message)
return mainHandler.paymentManager.PayAddress(ctx, req) return mainHandler.paymentManager.PayAddress(ctx, req)
}, },
GetTransactionSwapQuote: async ({ ctx, req }) => {
return mainHandler.paymentManager.GetTransactionSwapQuote(ctx, req)
},
NewInvoice: ({ ctx, req }) => mainHandler.appUserManager.NewInvoice(ctx, req), NewInvoice: ({ ctx, req }) => mainHandler.appUserManager.NewInvoice(ctx, req),
DecodeInvoice: async ({ ctx, req }) => { DecodeInvoice: async ({ ctx, req }) => {
return mainHandler.paymentManager.DecodeInvoice(req) return mainHandler.paymentManager.DecodeInvoice(req)

View file

@ -29,6 +29,7 @@ import { AppUserDevice } from "../entity/AppUserDevice.js"
import * as fs from 'fs' import * as fs from 'fs'
import { UserAccess } from "../entity/UserAccess.js" import { UserAccess } from "../entity/UserAccess.js"
import { AdminSettings } from "../entity/AdminSettings.js" import { AdminSettings } from "../entity/AdminSettings.js"
import { TransactionSwap } from "../entity/TransactionSwap.js"
export type DbSettings = { export type DbSettings = {
@ -74,7 +75,8 @@ export const MainDbEntities = {
'ManagementGrant': ManagementGrant, 'ManagementGrant': ManagementGrant,
'AppUserDevice': AppUserDevice, 'AppUserDevice': AppUserDevice,
'UserAccess': UserAccess, 'UserAccess': UserAccess,
'AdminSettings': AdminSettings 'AdminSettings': AdminSettings,
'TransactionSwap': TransactionSwap
} }
export type MainDbNames = keyof typeof MainDbEntities export type MainDbNames = keyof typeof MainDbEntities
export const MainDbEntitiesNames = Object.keys(MainDbEntities) export const MainDbEntitiesNames = Object.keys(MainDbEntities)

View file

@ -0,0 +1,65 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, ManyToOne, JoinColumn, UpdateDateColumn } from "typeorm";
import { User } from "./User";
@Entity()
export class TransactionSwap {
@PrimaryGeneratedColumn('uuid')
swap_operation_id: string
@Column()
app_user_id: string
@Column()
swap_quote_id: string
@Column()
swap_tree: string
@Column()
lockup_address: string
@Column()
refund_public_key: string
@Column()
timeout_block_height: number
@Column()
invoice: string
@Column()
invoice_amount: number
@Column()
transaction_amount: number
@Column()
swap_fee_sats: number
@Column()
chain_fee_sats: number
@Column()
preimage: string
@Column()
ephemeral_public_key: string
@Column()
ephemeral_private_key: string
@Column({ default: false })
used: boolean
@Column({ default: "" })
failure_reason: string
@Column({ default: "" })
tx_id: string
@CreateDateColumn()
created_at: Date
@UpdateDateColumn()
updated_at: Date
}

View file

@ -47,6 +47,9 @@ export class UserInvoicePayment {
@Column({ nullable: true }) @Column({ nullable: true })
debit_to_pub: string debit_to_pub: string
@Column({ nullable: true })
swap_operation_id: string
@CreateDateColumn() @CreateDateColumn()
created_at: Date created_at: Date

View file

@ -0,0 +1,26 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class TxSwap1762890527098 implements MigrationInterface {
name = 'TxSwap1762890527098'
public async up(queryRunner: QueryRunner): Promise<void> {
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(`DROP INDEX "IDX_a609a4d3d8d9b07b90692a3c45"`);
await queryRunner.query(`CREATE TABLE "temporary_user_invoice_payment" ("serial_id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "invoice" varchar NOT NULL, "paid_amount" integer NOT NULL, "routing_fees" integer NOT NULL, "service_fees" integer NOT NULL, "paid_at_unix" integer NOT NULL, "internal" boolean NOT NULL DEFAULT (0), "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')), "userSerialId" integer, "linkedApplicationSerialId" integer, "liquidityProvider" varchar, "paymentIndex" integer NOT NULL DEFAULT (-1), "debit_to_pub" varchar, "swap_operation_id" varchar, CONSTRAINT "FK_ef2aa6761ab681bbbd5f94e0fcb" FOREIGN KEY ("userSerialId") REFERENCES "user" ("serial_id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_6bcac90887eea1dc61d37db2994" FOREIGN KEY ("linkedApplicationSerialId") REFERENCES "application" ("serial_id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
await queryRunner.query(`INSERT INTO "temporary_user_invoice_payment"("serial_id", "invoice", "paid_amount", "routing_fees", "service_fees", "paid_at_unix", "internal", "created_at", "updated_at", "userSerialId", "linkedApplicationSerialId", "liquidityProvider", "paymentIndex", "debit_to_pub") SELECT "serial_id", "invoice", "paid_amount", "routing_fees", "service_fees", "paid_at_unix", "internal", "created_at", "updated_at", "userSerialId", "linkedApplicationSerialId", "liquidityProvider", "paymentIndex", "debit_to_pub" FROM "user_invoice_payment"`);
await queryRunner.query(`DROP TABLE "user_invoice_payment"`);
await queryRunner.query(`ALTER TABLE "temporary_user_invoice_payment" RENAME TO "user_invoice_payment"`);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_a609a4d3d8d9b07b90692a3c45" ON "user_invoice_payment" ("invoice") `);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "IDX_a609a4d3d8d9b07b90692a3c45"`);
await queryRunner.query(`ALTER TABLE "user_invoice_payment" RENAME TO "temporary_user_invoice_payment"`);
await queryRunner.query(`CREATE TABLE "user_invoice_payment" ("serial_id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "invoice" varchar NOT NULL, "paid_amount" integer NOT NULL, "routing_fees" integer NOT NULL, "service_fees" integer NOT NULL, "paid_at_unix" integer NOT NULL, "internal" boolean NOT NULL DEFAULT (0), "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')), "userSerialId" integer, "linkedApplicationSerialId" integer, "liquidityProvider" varchar, "paymentIndex" integer NOT NULL DEFAULT (-1), "debit_to_pub" varchar, CONSTRAINT "FK_ef2aa6761ab681bbbd5f94e0fcb" FOREIGN KEY ("userSerialId") REFERENCES "user" ("serial_id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_6bcac90887eea1dc61d37db2994" FOREIGN KEY ("linkedApplicationSerialId") REFERENCES "application" ("serial_id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
await queryRunner.query(`INSERT INTO "user_invoice_payment"("serial_id", "invoice", "paid_amount", "routing_fees", "service_fees", "paid_at_unix", "internal", "created_at", "updated_at", "userSerialId", "linkedApplicationSerialId", "liquidityProvider", "paymentIndex", "debit_to_pub") SELECT "serial_id", "invoice", "paid_amount", "routing_fees", "service_fees", "paid_at_unix", "internal", "created_at", "updated_at", "userSerialId", "linkedApplicationSerialId", "liquidityProvider", "paymentIndex", "debit_to_pub" FROM "temporary_user_invoice_payment"`);
await queryRunner.query(`DROP TABLE "temporary_user_invoice_payment"`);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_a609a4d3d8d9b07b90692a3c45" ON "user_invoice_payment" ("invoice") `);
await queryRunner.query(`DROP TABLE "transaction_swap"`);
}
}

View file

@ -14,6 +14,7 @@ import { Application } from './entity/Application.js';
import TransactionsQueue from "./db/transactionsQueue.js"; import TransactionsQueue from "./db/transactionsQueue.js";
import { LoggedEvent } from './eventsLog.js'; import { LoggedEvent } from './eventsLog.js';
import { StorageInterface } from './db/storageInterface.js'; import { StorageInterface } from './db/storageInterface.js';
import { TransactionSwap } from './entity/TransactionSwap.js';
export type InboundOptionals = { product?: Product, callbackUrl?: string, expiry: number, expectedPayer?: User, linkedApplication?: Application, zapInfo?: ZapInfo, offerId?: string, payerData?: Record<string, string>, rejectUnauthorized?: boolean, token?: string, blind?: boolean } export type InboundOptionals = { product?: Product, callbackUrl?: string, expiry: number, expectedPayer?: User, linkedApplication?: Application, zapInfo?: ZapInfo, offerId?: string, payerData?: Record<string, string>, rejectUnauthorized?: boolean, token?: string, blind?: boolean }
export const defaultInvoiceExpiry = 60 * 60 export const defaultInvoiceExpiry = 60 * 60
export default class { export default class {
@ -158,7 +159,8 @@ export default class {
return this.dbs.FindOne<UserToUserPayment>('UserToUserPayment', { where: { serial_id: serialId } }, txId) return this.dbs.FindOne<UserToUserPayment>('UserToUserPayment', { where: { serial_id: serialId } }, txId)
} }
async AddPendingExternalPayment(userId: string, invoice: string, amounts: { payAmount: number, serviceFee: number, networkFee: number }, linkedApplication: Application, liquidityProvider: string | undefined, txId: string, debitNpub?: string): Promise<UserInvoicePayment> { async AddPendingExternalPayment(userId: string, invoice: string, amounts: { payAmount: number, serviceFee: number, networkFee: number }, linkedApplication: Application, liquidityProvider: string | undefined, txId: string, optionals: { debitNpub?: string, swapOperationId?: string } = {}): Promise<UserInvoicePayment> {
const { debitNpub, swapOperationId } = optionals
const user = await this.userStorage.GetUser(userId, txId) const user = await this.userStorage.GetUser(userId, txId)
return this.dbs.CreateAndSave<UserInvoicePayment>('UserInvoicePayment', { return this.dbs.CreateAndSave<UserInvoicePayment>('UserInvoicePayment', {
user, user,
@ -170,7 +172,8 @@ export default class {
internal: false, internal: false,
linkedApplication, linkedApplication,
liquidityProvider, liquidityProvider,
debit_to_pub: debitNpub debit_to_pub: debitNpub,
swap_operation_id: swapOperationId
}, txId) }, txId)
} }
@ -458,6 +461,36 @@ export default class {
} }
return this.dbs.Find<UserReceivingInvoice>('UserReceivingInvoice', { where }) return this.dbs.Find<UserReceivingInvoice>('UserReceivingInvoice', { where })
} }
async AddTransactionSwap(swap: Partial<TransactionSwap>) {
return this.dbs.CreateAndSave<TransactionSwap>('TransactionSwap', swap)
}
async GetTransactionSwap(swapOperationId: string, txId?: string) {
return this.dbs.FindOne<TransactionSwap>('TransactionSwap', { where: { swap_operation_id: swapOperationId } }, txId)
}
async FinalizeTransactionSwap(swapOperationId: string, txId: string) {
return this.dbs.Update<TransactionSwap>('TransactionSwap', { swap_operation_id: swapOperationId }, {
used: true,
tx_id: txId,
})
}
async FailTransactionSwap(swapOperationId: string, failureReason: string) {
return this.dbs.Update<TransactionSwap>('TransactionSwap', { swap_operation_id: swapOperationId }, {
used: true,
failure_reason: failureReason,
})
}
async DeleteTransactionSwap(swapOperationId: string, txId?: string) {
return this.dbs.Delete<TransactionSwap>('TransactionSwap', { swap_operation_id: swapOperationId }, txId)
}
async DeleteExpiredTransactionSwaps(currentHeight: number, txId?: string) {
return this.dbs.Delete<TransactionSwap>('TransactionSwap', { timeout_block_height: LessThan(currentHeight) }, txId)
}
} }
const orFail = async <T>(resultPromise: Promise<T | null>) => { const orFail = async <T>(resultPromise: Promise<T | null>) => {