swaps wired
This commit is contained in:
parent
ec2d48664a
commit
f20d40e44f
21 changed files with 679 additions and 137 deletions
|
|
@ -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
|
||||||
|
|
@ -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)_
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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"`
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
13
src/index.ts
13
src/index.ts
|
|
@ -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)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -458,5 +458,3 @@ export default class {
|
||||||
this.nostrReset(s)
|
this.nostrReset(s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 })
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
65
src/services/storage/entity/TransactionSwap.ts
Normal file
65
src/services/storage/entity/TransactionSwap.ts
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
26
src/services/storage/migrations/1762890527098-tx_swap.ts
Normal file
26
src/services/storage/migrations/1762890527098-tx_swap.ts
Normal 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"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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>) => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue