admin swaps

This commit is contained in:
boufni95 2026-01-09 17:04:51 +00:00
parent e9a8865192
commit 0a385188ae
17 changed files with 560 additions and 188 deletions

View file

@ -93,6 +93,11 @@ The nostr server will send back a message response, and inside the body there wi
- input: [MessagingToken](#MessagingToken) - input: [MessagingToken](#MessagingToken)
- This methods has an __empty__ __response__ body - This methods has an __empty__ __response__ body
- GetAdminTransactionSwapQuote
- auth type: __Admin__
- input: [TransactionSwapRequest](#TransactionSwapRequest)
- output: [TransactionSwapQuote](#TransactionSwapQuote)
- GetAppsMetrics - GetAppsMetrics
- auth type: __Metrics__ - auth type: __Metrics__
- input: [AppsMetricsRequest](#AppsMetricsRequest) - input: [AppsMetricsRequest](#AppsMetricsRequest)
@ -280,6 +285,11 @@ The nostr server will send back a message response, and inside the body there wi
- input: [PayAddressRequest](#PayAddressRequest) - input: [PayAddressRequest](#PayAddressRequest)
- output: [PayAddressResponse](#PayAddressResponse) - output: [PayAddressResponse](#PayAddressResponse)
- PayAdminTransactionSwap
- auth type: __Admin__
- input: [TransactionSwapQuoteRequest](#TransactionSwapQuoteRequest)
- output: [AdminSwapResponse](#AdminSwapResponse)
- PayInvoice - PayInvoice
- auth type: __User__ - auth type: __User__
- input: [PayInvoiceRequest](#PayInvoiceRequest) - input: [PayInvoiceRequest](#PayInvoiceRequest)
@ -525,6 +535,13 @@ The nostr server will send back a message response, and inside the body there wi
- input: [MessagingToken](#MessagingToken) - input: [MessagingToken](#MessagingToken)
- This methods has an __empty__ __response__ body - This methods has an __empty__ __response__ body
- GetAdminTransactionSwapQuote
- auth type: __Admin__
- http method: __post__
- http route: __/api/admin/swap/transaction/quote__
- input: [TransactionSwapRequest](#TransactionSwapRequest)
- output: [TransactionSwapQuote](#TransactionSwapQuote)
- GetApp - GetApp
- auth type: __App__ - auth type: __App__
- http method: __post__ - http method: __post__
@ -870,6 +887,13 @@ The nostr server will send back a message response, and inside the body there wi
- input: [PayAddressRequest](#PayAddressRequest) - input: [PayAddressRequest](#PayAddressRequest)
- output: [PayAddressResponse](#PayAddressResponse) - output: [PayAddressResponse](#PayAddressResponse)
- PayAdminTransactionSwap
- auth type: __Admin__
- http method: __post__
- http route: __/api/admin/swap/transaction/pay__
- input: [TransactionSwapQuoteRequest](#TransactionSwapQuoteRequest)
- output: [AdminSwapResponse](#AdminSwapResponse)
- PayAppUserInvoice - PayAppUserInvoice
- auth type: __App__ - auth type: __App__
- http method: __post__ - http method: __post__
@ -1062,6 +1086,10 @@ The nostr server will send back a message response, and inside the body there wi
- __name__: _string_ - __name__: _string_
- __price_sats__: _number_ - __price_sats__: _number_
### AdminSwapResponse
- __network_fee__: _number_
- __tx_id__: _string_
### AppMetrics ### AppMetrics
- __app__: _[Application](#Application)_ - __app__: _[Application](#Application)_
- __available__: _number_ - __available__: _number_
@ -1612,6 +1640,10 @@ The nostr server will send back a message response, and inside the body there wi
- __swap_operation_id__: _string_ - __swap_operation_id__: _string_
- __transaction_amount_sats__: _number_ - __transaction_amount_sats__: _number_
### TransactionSwapQuoteRequest
- __address__: _string_
- __swap_operation_id__: _string_
### TransactionSwapRequest ### TransactionSwapRequest
- __transaction_amount_sats__: _number_ - __transaction_amount_sats__: _number_

View file

@ -66,83 +66,85 @@ type Client struct {
BanDebit func(req DebitOperation) error BanDebit func(req DebitOperation) error
BanUser func(req BanUserRequest) (*BanUserResponse, error) BanUser func(req BanUserRequest) (*BanUserResponse, error)
// batching method: BatchUser not implemented // batching method: BatchUser not implemented
CloseChannel func(req CloseChannelRequest) (*CloseChannelResponse, error) CloseChannel func(req CloseChannelRequest) (*CloseChannelResponse, error)
CreateOneTimeInviteLink func(req CreateOneTimeInviteLinkRequest) (*CreateOneTimeInviteLinkResponse, error) CreateOneTimeInviteLink func(req CreateOneTimeInviteLinkRequest) (*CreateOneTimeInviteLinkResponse, error)
DecodeInvoice func(req DecodeInvoiceRequest) (*DecodeInvoiceResponse, error) DecodeInvoice func(req DecodeInvoiceRequest) (*DecodeInvoiceResponse, error)
DeleteUserOffer func(req OfferId) error DeleteUserOffer func(req OfferId) error
EditDebit func(req DebitAuthorizationRequest) error EditDebit func(req DebitAuthorizationRequest) error
EncryptionExchange func(req EncryptionExchangeRequest) error EncryptionExchange func(req EncryptionExchangeRequest) error
EnrollAdminToken func(req EnrollAdminTokenRequest) error EnrollAdminToken func(req EnrollAdminTokenRequest) error
EnrollMessagingToken func(req MessagingToken) error EnrollMessagingToken func(req MessagingToken) error
GetApp func() (*Application, error) GetAdminTransactionSwapQuote func(req TransactionSwapRequest) (*TransactionSwapQuote, error)
GetAppUser func(req GetAppUserRequest) (*AppUser, error) GetApp func() (*Application, error)
GetAppUserLNURLInfo func(req GetAppUserLNURLInfoRequest) (*LnurlPayInfoResponse, error) GetAppUser func(req GetAppUserRequest) (*AppUser, error)
GetAppsMetrics func(req AppsMetricsRequest) (*AppsMetrics, error) GetAppUserLNURLInfo func(req GetAppUserLNURLInfoRequest) (*LnurlPayInfoResponse, error)
GetBundleMetrics func(req LatestBundleMetricReq) (*BundleMetrics, error) GetAppsMetrics func(req AppsMetricsRequest) (*AppsMetrics, error)
GetDebitAuthorizations func() (*DebitAuthorizations, error) GetBundleMetrics func(req LatestBundleMetricReq) (*BundleMetrics, error)
GetErrorStats func() (*ErrorStats, error) GetDebitAuthorizations func() (*DebitAuthorizations, error)
GetHttpCreds func() (*HttpCreds, error) GetErrorStats func() (*ErrorStats, error)
GetInviteLinkState func(req GetInviteTokenStateRequest) (*GetInviteTokenStateResponse, error) GetHttpCreds func() (*HttpCreds, error)
GetLNURLChannelLink func() (*LnurlLinkResponse, error) GetInviteLinkState func(req GetInviteTokenStateRequest) (*GetInviteTokenStateResponse, error)
GetLiveDebitRequests func() (*LiveDebitRequest, error) GetLNURLChannelLink func() (*LnurlLinkResponse, error)
GetLiveManageRequests func() (*LiveManageRequest, error) GetLiveDebitRequests func() (*LiveDebitRequest, error)
GetLiveUserOperations func() (*LiveUserOperation, error) GetLiveManageRequests func() (*LiveManageRequest, error)
GetLndForwardingMetrics func(req LndMetricsRequest) (*LndForwardingMetrics, error) GetLiveUserOperations func() (*LiveUserOperation, error)
GetLndMetrics func(req LndMetricsRequest) (*LndMetrics, error) GetLndForwardingMetrics func(req LndMetricsRequest) (*LndForwardingMetrics, error)
GetLnurlPayInfo func(query GetLnurlPayInfo_Query) (*LnurlPayInfoResponse, error) GetLndMetrics func(req LndMetricsRequest) (*LndMetrics, error)
GetLnurlPayLink func() (*LnurlLinkResponse, error) GetLnurlPayInfo func(query GetLnurlPayInfo_Query) (*LnurlPayInfoResponse, error)
GetLnurlWithdrawInfo func(query GetLnurlWithdrawInfo_Query) (*LnurlWithdrawInfoResponse, error) GetLnurlPayLink func() (*LnurlLinkResponse, error)
GetLnurlWithdrawLink func() (*LnurlLinkResponse, error) GetLnurlWithdrawInfo func(query GetLnurlWithdrawInfo_Query) (*LnurlWithdrawInfoResponse, error)
GetManageAuthorizations func() (*ManageAuthorizations, error) GetLnurlWithdrawLink func() (*LnurlLinkResponse, error)
GetMigrationUpdate func() (*MigrationUpdate, error) GetManageAuthorizations func() (*ManageAuthorizations, error)
GetNPubLinkingState func(req GetNPubLinking) (*NPubLinking, error) GetMigrationUpdate func() (*MigrationUpdate, error)
GetPaymentState func(req GetPaymentStateRequest) (*PaymentState, error) GetNPubLinkingState func(req GetNPubLinking) (*NPubLinking, error)
GetProvidersDisruption func() (*ProvidersDisruption, error) GetPaymentState func(req GetPaymentStateRequest) (*PaymentState, error)
GetSeed func() (*LndSeed, error) GetProvidersDisruption func() (*ProvidersDisruption, error)
GetSingleBundleMetrics func(req SingleMetricReq) (*BundleData, error) GetSeed func() (*LndSeed, error)
GetSingleUsageMetrics func(req SingleMetricReq) (*UsageMetricTlv, error) GetSingleBundleMetrics func(req SingleMetricReq) (*BundleData, error)
GetTransactionSwapQuote func(req TransactionSwapRequest) (*TransactionSwapQuote, error) GetSingleUsageMetrics func(req SingleMetricReq) (*UsageMetricTlv, error)
GetUsageMetrics func(req LatestUsageMetricReq) (*UsageMetrics, error) GetTransactionSwapQuote func(req TransactionSwapRequest) (*TransactionSwapQuote, error)
GetUserInfo func() (*UserInfo, error) GetUsageMetrics func(req LatestUsageMetricReq) (*UsageMetrics, error)
GetUserOffer func(req OfferId) (*OfferConfig, error) GetUserInfo func() (*UserInfo, error)
GetUserOfferInvoices func(req GetUserOfferInvoicesReq) (*OfferInvoices, error) GetUserOffer func(req OfferId) (*OfferConfig, error)
GetUserOffers func() (*UserOffers, error) GetUserOfferInvoices func(req GetUserOfferInvoicesReq) (*OfferInvoices, error)
GetUserOperations func(req GetUserOperationsRequest) (*GetUserOperationsResponse, error) GetUserOffers func() (*UserOffers, error)
HandleLnurlAddress func(routeParams HandleLnurlAddress_RouteParams) (*LnurlPayInfoResponse, error) GetUserOperations func(req GetUserOperationsRequest) (*GetUserOperationsResponse, error)
HandleLnurlPay func(query HandleLnurlPay_Query) (*HandleLnurlPayResponse, error) HandleLnurlAddress func(routeParams HandleLnurlAddress_RouteParams) (*LnurlPayInfoResponse, error)
HandleLnurlWithdraw func(query HandleLnurlWithdraw_Query) error HandleLnurlPay func(query HandleLnurlPay_Query) (*HandleLnurlPayResponse, error)
Health func() error HandleLnurlWithdraw func(query HandleLnurlWithdraw_Query) error
LinkNPubThroughToken func(req LinkNPubThroughTokenRequest) error Health func() error
ListChannels func() (*LndChannels, error) LinkNPubThroughToken func(req LinkNPubThroughTokenRequest) error
ListSwaps func() (*SwapsList, error) ListChannels func() (*LndChannels, error)
LndGetInfo func(req LndGetInfoRequest) (*LndGetInfoResponse, error) ListSwaps func() (*SwapsList, error)
NewAddress func(req NewAddressRequest) (*NewAddressResponse, error) LndGetInfo func(req LndGetInfoRequest) (*LndGetInfoResponse, error)
NewInvoice func(req NewInvoiceRequest) (*NewInvoiceResponse, error) NewAddress func(req NewAddressRequest) (*NewAddressResponse, error)
NewProductInvoice func(query NewProductInvoice_Query) (*NewInvoiceResponse, error) NewInvoice func(req NewInvoiceRequest) (*NewInvoiceResponse, error)
OpenChannel func(req OpenChannelRequest) (*OpenChannelResponse, error) NewProductInvoice func(query NewProductInvoice_Query) (*NewInvoiceResponse, error)
PayAddress func(req PayAddressRequest) (*PayAddressResponse, error) OpenChannel func(req OpenChannelRequest) (*OpenChannelResponse, error)
PayAppUserInvoice func(req PayAppUserInvoiceRequest) (*PayInvoiceResponse, error) PayAddress func(req PayAddressRequest) (*PayAddressResponse, error)
PayInvoice func(req PayInvoiceRequest) (*PayInvoiceResponse, error) PayAdminTransactionSwap func(req TransactionSwapQuoteRequest) (*AdminSwapResponse, error)
PingSubProcesses func() error PayAppUserInvoice func(req PayAppUserInvoiceRequest) (*PayInvoiceResponse, error)
RequestNPubLinkingToken func(req RequestNPubLinkingTokenRequest) (*RequestNPubLinkingTokenResponse, error) PayInvoice func(req PayInvoiceRequest) (*PayInvoiceResponse, error)
ResetDebit func(req DebitOperation) error PingSubProcesses func() error
ResetManage func(req ManageOperation) error RequestNPubLinkingToken func(req RequestNPubLinkingTokenRequest) (*RequestNPubLinkingTokenResponse, error)
ResetMetricsStorages func() error ResetDebit func(req DebitOperation) error
ResetNPubLinkingToken func(req RequestNPubLinkingTokenRequest) (*RequestNPubLinkingTokenResponse, error) ResetManage func(req ManageOperation) error
RespondToDebit func(req DebitResponse) error ResetMetricsStorages func() error
SendAppUserToAppPayment func(req SendAppUserToAppPaymentRequest) error ResetNPubLinkingToken func(req RequestNPubLinkingTokenRequest) (*RequestNPubLinkingTokenResponse, error)
SendAppUserToAppUserPayment func(req SendAppUserToAppUserPaymentRequest) error RespondToDebit func(req DebitResponse) error
SetMockAppBalance func(req SetMockAppBalanceRequest) error SendAppUserToAppPayment func(req SendAppUserToAppPaymentRequest) error
SetMockAppUserBalance func(req SetMockAppUserBalanceRequest) error SendAppUserToAppUserPayment func(req SendAppUserToAppUserPaymentRequest) error
SetMockInvoiceAsPaid func(req SetMockInvoiceAsPaidRequest) error SetMockAppBalance func(req SetMockAppBalanceRequest) error
SubToWebRtcCandidates func() (*WebRtcCandidate, error) SetMockAppUserBalance func(req SetMockAppUserBalanceRequest) error
SubmitWebRtcMessage func(req WebRtcMessage) (*WebRtcAnswer, error) SetMockInvoiceAsPaid func(req SetMockInvoiceAsPaidRequest) error
UpdateCallbackUrl func(req CallbackUrl) (*CallbackUrl, error) SubToWebRtcCandidates func() (*WebRtcCandidate, error)
UpdateChannelPolicy func(req UpdateChannelPolicyRequest) error SubmitWebRtcMessage func(req WebRtcMessage) (*WebRtcAnswer, error)
UpdateUserOffer func(req OfferConfig) error UpdateCallbackUrl func(req CallbackUrl) (*CallbackUrl, error)
UseInviteLink func(req UseInviteLinkRequest) error UpdateChannelPolicy func(req UpdateChannelPolicyRequest) error
UserHealth func() (*UserHealthState, error) UpdateUserOffer func(req OfferConfig) error
ZipMetricsStorages func() (*ZippedMetrics, error) UseInviteLink func(req UseInviteLinkRequest) error
UserHealth func() (*UserHealthState, error)
ZipMetricsStorages func() (*ZippedMetrics, error)
} }
func NewClient(params ClientParams) *Client { func NewClient(params ClientParams) *Client {
@ -664,6 +666,35 @@ func NewClient(params ClientParams) *Client {
} }
return nil return nil
}, },
GetAdminTransactionSwapQuote: func(req TransactionSwapRequest) (*TransactionSwapQuote, error) {
auth, err := params.RetrieveAdminAuth()
if err != nil {
return nil, err
}
finalRoute := "/api/admin/swap/transaction/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
},
GetApp: func() (*Application, error) { GetApp: func() (*Application, error) {
auth, err := params.RetrieveAppAuth() auth, err := params.RetrieveAppAuth()
if err != nil { if err != nil {
@ -1834,6 +1865,35 @@ func NewClient(params ClientParams) *Client {
} }
return &res, nil return &res, nil
}, },
PayAdminTransactionSwap: func(req TransactionSwapQuoteRequest) (*AdminSwapResponse, error) {
auth, err := params.RetrieveAdminAuth()
if err != nil {
return nil, err
}
finalRoute := "/api/admin/swap/transaction/pay"
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 := AdminSwapResponse{}
err = json.Unmarshal(resBody, &res)
if err != nil {
return nil, err
}
return &res, nil
},
PayAppUserInvoice: func(req PayAppUserInvoiceRequest) (*PayInvoiceResponse, error) { PayAppUserInvoice: func(req PayAppUserInvoiceRequest) (*PayInvoiceResponse, error) {
auth, err := params.RetrieveAppAuth() auth, err := params.RetrieveAppAuth()
if err != nil { if err != nil {

View file

@ -123,6 +123,10 @@ type AddProductRequest struct {
Name string `json:"name"` Name string `json:"name"`
Price_sats int64 `json:"price_sats"` Price_sats int64 `json:"price_sats"`
} }
type AdminSwapResponse struct {
Network_fee int64 `json:"network_fee"`
Tx_id string `json:"tx_id"`
}
type AppMetrics struct { type AppMetrics struct {
App *Application `json:"app"` App *Application `json:"app"`
Available int64 `json:"available"` Available int64 `json:"available"`
@ -673,6 +677,10 @@ type TransactionSwapQuote struct {
Swap_operation_id string `json:"swap_operation_id"` Swap_operation_id string `json:"swap_operation_id"`
Transaction_amount_sats int64 `json:"transaction_amount_sats"` Transaction_amount_sats int64 `json:"transaction_amount_sats"`
} }
type TransactionSwapQuoteRequest struct {
Address string `json:"address"`
Swap_operation_id string `json:"swap_operation_id"`
}
type TransactionSwapRequest struct { type TransactionSwapRequest struct {
Transaction_amount_sats int64 `json:"transaction_amount_sats"` Transaction_amount_sats int64 `json:"transaction_amount_sats"`
} }

View file

@ -869,6 +869,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.GetAdminTransactionSwapQuote) throw new Error('method: GetAdminTransactionSwapQuote is not implemented')
app.post('/api/admin/swap/transaction/quote', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'GetAdminTransactionSwapQuote', 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.GetAdminTransactionSwapQuote) throw new Error('method: GetAdminTransactionSwapQuote is not implemented')
const authContext = await opts.AdminAuthGuard(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.GetAdminTransactionSwapQuote({rpcName:'GetAdminTransactionSwapQuote', 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.GetApp) throw new Error('method: GetApp is not implemented') if (!opts.allowNotImplementedMethods && !methods.GetApp) throw new Error('method: GetApp is not implemented')
app.post('/api/app/get', async (req, res) => { app.post('/api/app/get', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'GetApp', batch: false, nostr: false, batchSize: 0} const info: Types.RequestInfo = { rpcName: 'GetApp', batch: false, nostr: false, batchSize: 0}
@ -1752,6 +1774,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.PayAdminTransactionSwap) throw new Error('method: PayAdminTransactionSwap is not implemented')
app.post('/api/admin/swap/transaction/pay', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'PayAdminTransactionSwap', 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.PayAdminTransactionSwap) throw new Error('method: PayAdminTransactionSwap is not implemented')
const authContext = await opts.AdminAuthGuard(req.headers['authorization'])
authCtx = authContext
stats.guard = process.hrtime.bigint()
const request = req.body
const error = Types.TransactionSwapQuoteRequestValidate(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.PayAdminTransactionSwap({rpcName:'PayAdminTransactionSwap', 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.PayAppUserInvoice) throw new Error('method: PayAppUserInvoice is not implemented') if (!opts.allowNotImplementedMethods && !methods.PayAppUserInvoice) throw new Error('method: PayAppUserInvoice is not implemented')
app.post('/api/app/invoice/pay', async (req, res) => { app.post('/api/app/invoice/pay', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'PayAppUserInvoice', batch: false, nostr: false, batchSize: 0} const info: Types.RequestInfo = { rpcName: 'PayAppUserInvoice', batch: false, nostr: false, batchSize: 0}

View file

@ -273,6 +273,20 @@ export default (params: ClientParams) => ({
} }
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
}, },
GetAdminTransactionSwapQuote: async (request: Types.TransactionSwapRequest): Promise<ResultError | ({ status: 'OK' }& Types.TransactionSwapQuote)> => {
const auth = await params.retrieveAdminAuth()
if (auth === null) throw new Error('retrieveAdminAuth() returned null')
let finalRoute = '/api/admin/swap/transaction/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' }
},
GetApp: async (): Promise<ResultError | ({ status: 'OK' }& Types.Application)> => { GetApp: async (): Promise<ResultError | ({ status: 'OK' }& Types.Application)> => {
const auth = await params.retrieveAppAuth() const auth = await params.retrieveAppAuth()
if (auth === null) throw new Error('retrieveAppAuth() returned null') if (auth === null) throw new Error('retrieveAppAuth() returned null')
@ -881,6 +895,20 @@ export default (params: ClientParams) => ({
} }
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
}, },
PayAdminTransactionSwap: async (request: Types.TransactionSwapQuoteRequest): Promise<ResultError | ({ status: 'OK' }& Types.AdminSwapResponse)> => {
const auth = await params.retrieveAdminAuth()
if (auth === null) throw new Error('retrieveAdminAuth() returned null')
let finalRoute = '/api/admin/swap/transaction/pay'
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.AdminSwapResponseValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
PayAppUserInvoice: async (request: Types.PayAppUserInvoiceRequest): Promise<ResultError | ({ status: 'OK' }& Types.PayInvoiceResponse)> => { PayAppUserInvoice: async (request: Types.PayAppUserInvoiceRequest): Promise<ResultError | ({ status: 'OK' }& Types.PayInvoiceResponse)> => {
const auth = await params.retrieveAppAuth() const auth = await params.retrieveAppAuth()
if (auth === null) throw new Error('retrieveAppAuth() returned null') if (auth === null) throw new Error('retrieveAppAuth() returned null')

View file

@ -230,6 +230,21 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ
} }
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
}, },
GetAdminTransactionSwapQuote: async (request: Types.TransactionSwapRequest): Promise<ResultError | ({ status: 'OK' }& Types.TransactionSwapQuote)> => {
const auth = await params.retrieveNostrAdminAuth()
if (auth === null) throw new Error('retrieveNostrAdminAuth() returned null')
const nostrRequest: NostrRequest = {}
nostrRequest.body = request
const data = await send(params.pubDestination, {rpcName:'GetAdminTransactionSwapQuote',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' }
},
GetAppsMetrics: async (request: Types.AppsMetricsRequest): Promise<ResultError | ({ status: 'OK' }& Types.AppsMetrics)> => { GetAppsMetrics: async (request: Types.AppsMetricsRequest): Promise<ResultError | ({ status: 'OK' }& Types.AppsMetrics)> => {
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')
@ -769,6 +784,21 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ
} }
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
}, },
PayAdminTransactionSwap: async (request: Types.TransactionSwapQuoteRequest): Promise<ResultError | ({ status: 'OK' }& Types.AdminSwapResponse)> => {
const auth = await params.retrieveNostrAdminAuth()
if (auth === null) throw new Error('retrieveNostrAdminAuth() returned null')
const nostrRequest: NostrRequest = {}
nostrRequest.body = request
const data = await send(params.pubDestination, {rpcName:'PayAdminTransactionSwap',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.AdminSwapResponseValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
PayInvoice: async (request: Types.PayInvoiceRequest): Promise<ResultError | ({ status: 'OK' }& Types.PayInvoiceResponse)> => { PayInvoice: async (request: Types.PayInvoiceRequest): Promise<ResultError | ({ status: 'OK' }& Types.PayInvoiceResponse)> => {
const auth = await params.retrieveNostrUserAuth() const auth = await params.retrieveNostrUserAuth()
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')

View file

@ -687,6 +687,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 'GetAdminTransactionSwapQuote':
try {
if (!methods.GetAdminTransactionSwapQuote) throw new Error('method: GetAdminTransactionSwapQuote is not implemented')
const authContext = await opts.NostrAdminAuthGuard(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.GetAdminTransactionSwapQuote({rpcName:'GetAdminTransactionSwapQuote', 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 'GetAppsMetrics': case 'GetAppsMetrics':
try { try {
if (!methods.GetAppsMetrics) throw new Error('method: GetAppsMetrics is not implemented') if (!methods.GetAppsMetrics) throw new Error('method: GetAppsMetrics is not implemented')
@ -1225,6 +1241,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 'PayAdminTransactionSwap':
try {
if (!methods.PayAdminTransactionSwap) throw new Error('method: PayAdminTransactionSwap is not implemented')
const authContext = await opts.NostrAdminAuthGuard(req.appId, req.authIdentifier)
stats.guard = process.hrtime.bigint()
authCtx = authContext
const request = req.body
const error = Types.TransactionSwapQuoteRequestValidate(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.PayAdminTransactionSwap({rpcName:'PayAdminTransactionSwap', 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 'PayInvoice': case 'PayInvoice':
try { try {
if (!methods.PayInvoice) throw new Error('method: PayInvoice is not implemented') if (!methods.PayInvoice) throw new Error('method: PayInvoice is not implemented')

View file

@ -7,8 +7,8 @@ export type RequestMetric = AuthContext & RequestInfo & RequestStats & { error?:
export type AdminContext = { export type AdminContext = {
admin_id: string admin_id: string
} }
export type AdminMethodInputs = AddApp_Input | AddPeer_Input | AuthApp_Input | BanUser_Input | CloseChannel_Input | CreateOneTimeInviteLink_Input | GetInviteLinkState_Input | GetSeed_Input | ListChannels_Input | LndGetInfo_Input | OpenChannel_Input | UpdateChannelPolicy_Input export type AdminMethodInputs = AddApp_Input | AddPeer_Input | AuthApp_Input | BanUser_Input | CloseChannel_Input | CreateOneTimeInviteLink_Input | GetAdminTransactionSwapQuote_Input | GetInviteLinkState_Input | GetSeed_Input | ListChannels_Input | LndGetInfo_Input | OpenChannel_Input | PayAdminTransactionSwap_Input | UpdateChannelPolicy_Input
export type AdminMethodOutputs = AddApp_Output | AddPeer_Output | AuthApp_Output | BanUser_Output | CloseChannel_Output | CreateOneTimeInviteLink_Output | GetInviteLinkState_Output | GetSeed_Output | ListChannels_Output | LndGetInfo_Output | OpenChannel_Output | UpdateChannelPolicy_Output export type AdminMethodOutputs = AddApp_Output | AddPeer_Output | AuthApp_Output | BanUser_Output | CloseChannel_Output | CreateOneTimeInviteLink_Output | GetAdminTransactionSwapQuote_Output | GetInviteLinkState_Output | GetSeed_Output | ListChannels_Output | LndGetInfo_Output | OpenChannel_Output | PayAdminTransactionSwap_Output | UpdateChannelPolicy_Output
export type AppContext = { export type AppContext = {
app_id: string app_id: string
} }
@ -99,6 +99,9 @@ export type EnrollAdminToken_Output = ResultError | { status: 'OK' }
export type EnrollMessagingToken_Input = {rpcName:'EnrollMessagingToken', req: MessagingToken} export type EnrollMessagingToken_Input = {rpcName:'EnrollMessagingToken', req: MessagingToken}
export type EnrollMessagingToken_Output = ResultError | { status: 'OK' } export type EnrollMessagingToken_Output = ResultError | { status: 'OK' }
export type GetAdminTransactionSwapQuote_Input = {rpcName:'GetAdminTransactionSwapQuote', req: TransactionSwapRequest}
export type GetAdminTransactionSwapQuote_Output = ResultError | ({ status: 'OK' } & TransactionSwapQuote)
export type GetApp_Input = {rpcName:'GetApp'} export type GetApp_Input = {rpcName:'GetApp'}
export type GetApp_Output = ResultError | ({ status: 'OK' } & Application) export type GetApp_Output = ResultError | ({ status: 'OK' } & Application)
@ -262,6 +265,9 @@ export type OpenChannel_Output = ResultError | ({ status: 'OK' } & OpenChannelRe
export type PayAddress_Input = {rpcName:'PayAddress', req: PayAddressRequest} export type PayAddress_Input = {rpcName:'PayAddress', req: PayAddressRequest}
export type PayAddress_Output = ResultError | ({ status: 'OK' } & PayAddressResponse) export type PayAddress_Output = ResultError | ({ status: 'OK' } & PayAddressResponse)
export type PayAdminTransactionSwap_Input = {rpcName:'PayAdminTransactionSwap', req: TransactionSwapQuoteRequest}
export type PayAdminTransactionSwap_Output = ResultError | ({ status: 'OK' } & AdminSwapResponse)
export type PayAppUserInvoice_Input = {rpcName:'PayAppUserInvoice', req: PayAppUserInvoiceRequest} export type PayAppUserInvoice_Input = {rpcName:'PayAppUserInvoice', req: PayAppUserInvoiceRequest}
export type PayAppUserInvoice_Output = ResultError | ({ status: 'OK' } & PayInvoiceResponse) export type PayAppUserInvoice_Output = ResultError | ({ status: 'OK' } & PayInvoiceResponse)
@ -348,6 +354,7 @@ export type ServerMethods = {
EncryptionExchange?: (req: EncryptionExchange_Input & {ctx: GuestContext }) => Promise<void> EncryptionExchange?: (req: EncryptionExchange_Input & {ctx: GuestContext }) => Promise<void>
EnrollAdminToken?: (req: EnrollAdminToken_Input & {ctx: UserContext }) => Promise<void> EnrollAdminToken?: (req: EnrollAdminToken_Input & {ctx: UserContext }) => Promise<void>
EnrollMessagingToken?: (req: EnrollMessagingToken_Input & {ctx: UserContext }) => Promise<void> EnrollMessagingToken?: (req: EnrollMessagingToken_Input & {ctx: UserContext }) => Promise<void>
GetAdminTransactionSwapQuote?: (req: GetAdminTransactionSwapQuote_Input & {ctx: AdminContext }) => Promise<TransactionSwapQuote>
GetApp?: (req: GetApp_Input & {ctx: AppContext }) => Promise<Application> GetApp?: (req: GetApp_Input & {ctx: AppContext }) => Promise<Application>
GetAppUser?: (req: GetAppUser_Input & {ctx: AppContext }) => Promise<AppUser> GetAppUser?: (req: GetAppUser_Input & {ctx: AppContext }) => Promise<AppUser>
GetAppUserLNURLInfo?: (req: GetAppUserLNURLInfo_Input & {ctx: AppContext }) => Promise<LnurlPayInfoResponse> GetAppUserLNURLInfo?: (req: GetAppUserLNURLInfo_Input & {ctx: AppContext }) => Promise<LnurlPayInfoResponse>
@ -395,6 +402,7 @@ export type ServerMethods = {
NewProductInvoice?: (req: NewProductInvoice_Input & {ctx: UserContext }) => Promise<NewInvoiceResponse> NewProductInvoice?: (req: NewProductInvoice_Input & {ctx: UserContext }) => Promise<NewInvoiceResponse>
OpenChannel?: (req: OpenChannel_Input & {ctx: AdminContext }) => Promise<OpenChannelResponse> OpenChannel?: (req: OpenChannel_Input & {ctx: AdminContext }) => Promise<OpenChannelResponse>
PayAddress?: (req: PayAddress_Input & {ctx: UserContext }) => Promise<PayAddressResponse> PayAddress?: (req: PayAddress_Input & {ctx: UserContext }) => Promise<PayAddressResponse>
PayAdminTransactionSwap?: (req: PayAdminTransactionSwap_Input & {ctx: AdminContext }) => Promise<AdminSwapResponse>
PayAppUserInvoice?: (req: PayAppUserInvoice_Input & {ctx: AppContext }) => Promise<PayInvoiceResponse> PayAppUserInvoice?: (req: PayAppUserInvoice_Input & {ctx: AppContext }) => Promise<PayInvoiceResponse>
PayInvoice?: (req: PayInvoice_Input & {ctx: UserContext }) => Promise<PayInvoiceResponse> PayInvoice?: (req: PayInvoice_Input & {ctx: UserContext }) => Promise<PayInvoiceResponse>
PingSubProcesses?: (req: PingSubProcesses_Input & {ctx: MetricsContext }) => Promise<void> PingSubProcesses?: (req: PingSubProcesses_Input & {ctx: MetricsContext }) => Promise<void>
@ -659,6 +667,29 @@ export const AddProductRequestValidate = (o?: AddProductRequest, opts: AddProduc
return null return null
} }
export type AdminSwapResponse = {
network_fee: number
tx_id: string
}
export const AdminSwapResponseOptionalFields: [] = []
export type AdminSwapResponseOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
network_fee_CustomCheck?: (v: number) => boolean
tx_id_CustomCheck?: (v: string) => boolean
}
export const AdminSwapResponseValidate = (o?: AdminSwapResponse, opts: AdminSwapResponseOptions = {}, path: string = 'AdminSwapResponse::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.network_fee !== 'number') return new Error(`${path}.network_fee: is not a number`)
if (opts.network_fee_CustomCheck && !opts.network_fee_CustomCheck(o.network_fee)) return new Error(`${path}.network_fee: custom check failed`)
if (typeof o.tx_id !== 'string') return new Error(`${path}.tx_id: is not a string`)
if (opts.tx_id_CustomCheck && !opts.tx_id_CustomCheck(o.tx_id)) return new Error(`${path}.tx_id: custom check failed`)
return null
}
export type AppMetrics = { export type AppMetrics = {
app: Application app: Application
available: number available: number
@ -3962,6 +3993,29 @@ export const TransactionSwapQuoteValidate = (o?: TransactionSwapQuote, opts: Tra
return null return null
} }
export type TransactionSwapQuoteRequest = {
address: string
swap_operation_id: string
}
export const TransactionSwapQuoteRequestOptionalFields: [] = []
export type TransactionSwapQuoteRequestOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
address_CustomCheck?: (v: string) => boolean
swap_operation_id_CustomCheck?: (v: string) => boolean
}
export const TransactionSwapQuoteRequestValidate = (o?: TransactionSwapQuoteRequest, opts: TransactionSwapQuoteRequestOptions = {}, path: string = 'TransactionSwapQuoteRequest::root.'): Error | null => {
if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message')
if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null')
if (typeof o.address !== 'string') return new Error(`${path}.address: is not a string`)
if (opts.address_CustomCheck && !opts.address_CustomCheck(o.address)) return new Error(`${path}.address: 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`)
return null
}
export type TransactionSwapRequest = { export type TransactionSwapRequest = {
transaction_amount_sats: number transaction_amount_sats: number
} }

View file

@ -175,6 +175,20 @@ service LightningPub {
option (nostr) = true; option (nostr) = true;
} }
rpc GetAdminTransactionSwapQuote(structs.TransactionSwapRequest) returns (structs.TransactionSwapQuote) {
option (auth_type) = "Admin";
option (http_method) = "post";
option (http_route) = "/api/admin/swap/transaction/quote";
option (nostr) = true;
}
rpc PayAdminTransactionSwap(structs.TransactionSwapQuoteRequest) returns (structs.AdminSwapResponse) {
option (auth_type) = "Admin";
option (http_method) = "post";
option (http_route) = "/api/admin/swap/transaction/pay";
option (nostr) = true;
}
rpc GetUsageMetrics(structs.LatestUsageMetricReq) returns (structs.UsageMetrics) { rpc GetUsageMetrics(structs.LatestUsageMetricReq) returns (structs.UsageMetrics) {
option (auth_type) = "Metrics"; option (auth_type) = "Metrics";
option (http_method) = "post"; option (http_method) = "post";

View file

@ -834,6 +834,11 @@ message TransactionSwapRequest {
int64 transaction_amount_sats = 2; int64 transaction_amount_sats = 2;
} }
message TransactionSwapQuoteRequest {
string address = 1;
string swap_operation_id = 2;
}
message TransactionSwapQuote { message TransactionSwapQuote {
string swap_operation_id = 1; string swap_operation_id = 1;
int64 invoice_amount_sats = 2; int64 invoice_amount_sats = 2;
@ -844,6 +849,11 @@ message TransactionSwapQuote {
int64 service_fee_sats = 7; int64 service_fee_sats = 7;
} }
message AdminSwapResponse {
string tx_id = 1;
int64 network_fee = 2;
}
message SwapOperation { message SwapOperation {
string swap_operation_id = 1; string swap_operation_id = 1;
optional UserOperation operation_payment = 2; optional UserOperation operation_payment = 2;

View file

@ -15,7 +15,8 @@ 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'; import * as Types from '../../../proto/autogenerated/ts/types.js';
import { BTCNetwork } from '../main/settings.js'; import { BTCNetwork } from '../main/settings.js';
import Storage from '../storage/index.js';
import LND from './lnd.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 }
@ -47,9 +48,17 @@ export type TransactionSwapData = { createdResponse: TransactionSwapResponse, in
export class Swaps { export class Swaps {
reverseSwaps: ReverseSwaps reverseSwaps: ReverseSwaps
submarineSwaps: SubmarineSwaps submarineSwaps: SubmarineSwaps
constructor(settings: SettingsManager) { storage: Storage
lnd: LND
log = getLogger({ component: 'swaps' })
constructor(settings: SettingsManager, storage: Storage) {
this.reverseSwaps = new ReverseSwaps(settings) this.reverseSwaps = new ReverseSwaps(settings)
this.submarineSwaps = new SubmarineSwaps(settings) this.submarineSwaps = new SubmarineSwaps(settings)
this.storage = storage
}
SetLnd = (lnd: LND) => {
this.lnd = lnd
} }
Stop = () => { } Stop = () => { }
@ -58,6 +67,110 @@ export class Swaps {
const keys = ECPairFactory(ecc).fromPrivateKey(Buffer.from(privateKey, 'hex')) const keys = ECPairFactory(ecc).fromPrivateKey(Buffer.from(privateKey, 'hex'))
return keys return keys
} }
async GetTxSwapQuote(appUserId: string, amt: number, getServiceFee: (decodedAmt: number) => number): Promise<Types.TransactionSwapQuote> {
this.log("getting transaction swap quote")
const feesRes = await this.reverseSwaps.GetFees()
if (!feesRes.ok) {
throw new Error(feesRes.error)
}
const { claim, lockup } = feesRes.fees.minerFees
const minerFee = claim + lockup
const chainTotal = amt + minerFee
const res = await this.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 serviceFee = getServiceFee(decoded.numSatoshis)
const newSwap = await this.storage.paymentStorage.AddTransactionSwap({
app_user_id: appUserId,
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: amt,
chain_fee_sats: minerFee,
service_fee_sats: serviceFee,
}
}
async PayAddrWithSwap(appUserId: string, swapOpId: string, address: string, payInvoice: (invoice: string, amt: number) => Promise<void>) {
this.log("paying address with swap", { appUserId, swapOpId, address })
if (!swapOpId) {
throw new Error("request a swap quote before paying an external address")
}
const txSwap = await this.storage.paymentStorage.GetTransactionSwap(swapOpId, appUserId)
if (!txSwap) {
throw new Error("swap quote not found")
}
const info = await this.lnd.GetInfo()
if (info.blockHeight >= txSwap.timeout_block_height) {
throw new Error("swap timeout")
}
const keys = this.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: address,
keys,
chainFee: txSwap.chain_fee_sats,
preimage: Buffer.from(txSwap.preimage, 'hex'),
}
}
// the swap and the invoice payment are linked, swap will not start until the invoice payment is started, and will not complete once the invoice payment is completed
let swapResult = { ok: false, error: "swap never completed" } as { ok: true, txId: string } | { ok: false, error: string }
this.reverseSwaps.SubscribeToTransactionSwap(data, result => {
swapResult = result
})
try {
await payInvoice(txSwap.invoice, txSwap.invoice_amount)
if (!swapResult.ok) {
this.log("invoice payment successful, but swap failed")
await this.storage.paymentStorage.FailTransactionSwap(swapOpId, address, swapResult.error)
throw new Error(swapResult.error)
}
this.log("swap completed successfully")
await this.storage.paymentStorage.FinalizeTransactionSwap(swapOpId, address, swapResult.txId)
} catch (err: any) {
if (swapResult.ok) {
this.log("failed to pay swap invoice, but swap completed successfully", swapResult.txId)
await this.storage.paymentStorage.FailTransactionSwap(swapOpId, address, err.message)
} else {
this.log("failed to pay swap invoice and swap failed", swapResult.error)
await this.storage.paymentStorage.FailTransactionSwap(swapOpId, address, swapResult.error)
}
throw err
}
const networkFeesTotal = txSwap.chain_fee_sats + txSwap.swap_fee_sats
return {
txId: swapResult.txId,
network_fee: networkFeesTotal
}
}
} }
export class SubmarineSwaps { export class SubmarineSwaps {

View file

@ -5,7 +5,9 @@ import Storage from "../storage/index.js";
import * as Types from '../../../proto/autogenerated/ts/types.js' import * as Types from '../../../proto/autogenerated/ts/types.js'
import LND from "../lnd/lnd.js"; import LND from "../lnd/lnd.js";
import SettingsManager from "./settingsManager.js"; import SettingsManager from "./settingsManager.js";
import { Swaps } from "../lnd/swaps.js";
export class AdminManager { export class AdminManager {
settings: SettingsManager
storage: Storage storage: Storage
log = getLogger({ component: "adminManager" }) log = getLogger({ component: "adminManager" })
adminNpub = "" adminNpub = ""
@ -17,10 +19,13 @@ export class AdminManager {
interval: NodeJS.Timer interval: NodeJS.Timer
appNprofile: string appNprofile: string
lnd: LND lnd: LND
swaps: Swaps
nostrConnected: boolean = false nostrConnected: boolean = false
private nostrReset: () => Promise<void> = async () => { this.log("nostr reset not initialized yet") } private nostrReset: () => Promise<void> = async () => { this.log("nostr reset not initialized yet") }
constructor(settings: SettingsManager, storage: Storage) { constructor(settings: SettingsManager, storage: Storage, swaps: Swaps) {
this.settings = settings
this.storage = storage this.storage = storage
this.swaps = swaps
this.dataDir = settings.getStorageSettings().dataDir this.dataDir = settings.getStorageSettings().dataDir
this.adminNpubPath = getDataPath(this.dataDir, 'admin.npub') this.adminNpubPath = getDataPath(this.dataDir, 'admin.npub')
this.adminEnrollTokenPath = getDataPath(this.dataDir, 'admin.enroll') this.adminEnrollTokenPath = getDataPath(this.dataDir, 'admin.enroll')
@ -45,6 +50,7 @@ export class AdminManager {
setLND = (lnd: LND) => { setLND = (lnd: LND) => {
this.lnd = lnd this.lnd = lnd
this.swaps.SetLnd(lnd)
} }
setNostrConnected = (connected: boolean) => { setNostrConnected = (connected: boolean) => {
@ -253,6 +259,25 @@ export class AdminManager {
closing_txid: Buffer.from(res.txid).toString('hex') closing_txid: Buffer.from(res.txid).toString('hex')
} }
} }
async GetAdminTransactionSwapQuote(req: Types.TransactionSwapRequest): Promise<Types.TransactionSwapQuote> {
return this.swaps.GetTxSwapQuote("admin", req.transaction_amount_sats, () => 0)
}
async PayAdminTransactionSwap(req: Types.TransactionSwapQuoteRequest): Promise<Types.AdminSwapResponse> {
const routingFloor = this.settings.getSettings().lndSettings.routingFeeFloor
const routingLimit = this.settings.getSettings().lndSettings.routingFeeLimitBps / 10000
const swap = await this.swaps.PayAddrWithSwap("admin", req.swap_operation_id, req.address, async (invoice, amt) => {
const r = Math.max(Math.ceil(routingLimit * amt), routingFloor)
const payment = await this.lnd.PayInvoice(invoice, 0, { routingFeeLimit: r, serviceFee: 0 }, amt, { useProvider: false, from: 'system' })
await this.storage.metricsStorage.AddRootOperation("invoice_payment", invoice, amt + payment.feeSat)
})
return {
tx_id: swap.txId,
network_fee: swap.network_fee,
}
}
} }
const getDataPath = (dataDir: string, dataPath: string) => { const getDataPath = (dataDir: string, dataPath: string) => {

View file

@ -80,7 +80,7 @@ export default class {
this.liquidityManager = new LiquidityManager(this.settings, this.storage, this.utils, this.liquidityProvider, this.lnd, this.rugPullTracker) this.liquidityManager = new LiquidityManager(this.settings, this.storage, this.utils, this.liquidityProvider, this.lnd, this.rugPullTracker)
this.metricsManager = new MetricsManager(this.storage, this.lnd) this.metricsManager = new MetricsManager(this.storage, this.lnd)
this.paymentManager = new PaymentManager(this.storage, this.metricsManager, this.lnd, this.settings, this.liquidityManager, this.utils, this.addressPaidCb, this.invoicePaidCb) this.paymentManager = new PaymentManager(this.storage, this.metricsManager, this.lnd, adminManager.swaps, this.settings, this.liquidityManager, this.utils, this.addressPaidCb, this.invoicePaidCb)
this.productManager = new ProductManager(this.storage, this.paymentManager, this.settings) this.productManager = new ProductManager(this.storage, this.paymentManager, this.settings)
this.applicationManager = new ApplicationManager(this.storage, this.settings, this.paymentManager) this.applicationManager = new ApplicationManager(this.storage, this.settings, this.paymentManager)
this.appUserManager = new AppUserManager(this.storage, this.settings, this.applicationManager) this.appUserManager = new AppUserManager(this.storage, this.settings, this.applicationManager)

View file

@ -11,6 +11,7 @@ import { AdminManager } from "./adminManager.js"
import SettingsManager from "./settingsManager.js" import SettingsManager from "./settingsManager.js"
import { LoadStorageSettingsFromEnv } from "../storage/index.js" import { LoadStorageSettingsFromEnv } from "../storage/index.js"
import { NostrSender } from "../nostr/sender.js" import { NostrSender } from "../nostr/sender.js"
import { Swaps } from "../lnd/swaps.js"
export type AppData = { export type AppData = {
privateKey: string; privateKey: string;
publicKey: string; publicKey: string;
@ -32,7 +33,8 @@ export const initMainHandler = async (log: PubLogger, settingsManager: SettingsM
const utils = storageManager.utils const utils = storageManager.utils
const unlocker = new Unlocker(settingsManager, storageManager) const unlocker = new Unlocker(settingsManager, storageManager)
await unlocker.Unlock() await unlocker.Unlock()
const adminManager = new AdminManager(settingsManager, storageManager) const swaps = new Swaps(settingsManager, storageManager)
const adminManager = new AdminManager(settingsManager, storageManager, swaps)
let wizard: Wizard | null = null let wizard: Wizard | null = null
if (settingsManager.getSettings().serviceSettings.wizard) { if (settingsManager.getSettings().serviceSettings.wizard) {
wizard = new Wizard(settingsManager, storageManager, adminManager) wizard = new Wizard(settingsManager, storageManager, adminManager)

View file

@ -121,12 +121,11 @@ export class LiquidityProvider {
await this.invoicePaidCb(res.operation.identifier, res.operation.amount, 'provider') await this.invoicePaidCb(res.operation.identifier, res.operation.amount, 'provider')
this.incrementProviderBalance(res.operation.amount) this.incrementProviderBalance(res.operation.amount)
this.latestReceivedBalance = res.latest_balance this.latestReceivedBalance = res.latest_balance
if (!res.operation.inbound && !res.operation.confirmed) {
delete this.pendingPaymentsAck[res.operation.identifier]
}
} catch (err: any) { } catch (err: any) {
this.log("error processing incoming invoice", err.message) this.log("error processing incoming invoice", err.message)
} }
} else if (res.operation.type === Types.UserOperationType.OUTGOING_INVOICE) {
delete this.pendingPaymentsAck[res.operation.identifier]
} }
}) })
} }

View file

@ -61,7 +61,7 @@ export default class {
swaps: Swaps swaps: Swaps
invoiceLock: InvoiceLock invoiceLock: InvoiceLock
metrics: Metrics metrics: Metrics
constructor(storage: Storage, metrics: Metrics, lnd: LND, settings: SettingsManager, liquidityManager: LiquidityManager, utils: Utils, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb) { constructor(storage: Storage, metrics: Metrics, lnd: LND, swaps: Swaps, settings: SettingsManager, liquidityManager: LiquidityManager, utils: Utils, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb) {
this.storage = storage this.storage = storage
this.metrics = metrics this.metrics = metrics
this.settings = settings this.settings = settings
@ -69,7 +69,7 @@ export default class {
this.liquidityManager = liquidityManager this.liquidityManager = liquidityManager
this.utils = utils this.utils = utils
this.watchDog = new Watchdog(settings, this.liquidityManager, this.lnd, this.storage, this.utils, this.liquidityManager.rugPullTracker) this.watchDog = new Watchdog(settings, this.liquidityManager, this.lnd, this.storage, this.utils, this.liquidityManager.rugPullTracker)
this.swaps = new Swaps(settings) this.swaps = swaps
this.addressPaidCb = addressPaidCb this.addressPaidCb = addressPaidCb
this.invoicePaidCb = invoicePaidCb this.invoicePaidCb = invoicePaidCb
this.invoiceLock = new InvoiceLock() this.invoiceLock = new InvoiceLock()
@ -539,54 +539,16 @@ export default class {
} }
} }
async GetTransactionSwapQuote(ctx: Types.UserContext, req: Types.TransactionSwapRequest): Promise<Types.TransactionSwapQuote> { 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 app = await this.storage.applicationStorage.GetApplication(ctx.app_id) const app = await this.storage.applicationStorage.GetApplication(ctx.app_id)
const isManagedUser = ctx.user_id !== app.owner.user_id return this.swaps.GetTxSwapQuote(ctx.app_user_id, req.transaction_amount_sats, decodedAmt => {
const serviceFee = this.getSendServiceFee(Types.UserOperationType.OUTGOING_INVOICE, decoded.numSatoshis, isManagedUser) const isManagedUser = ctx.user_id !== app.owner.user_id
const newSwap = await this.storage.paymentStorage.AddTransactionSwap({ return this.getSendServiceFee(Types.UserOperationType.OUTGOING_INVOICE, decodedAmt, isManagedUser)
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,
service_fee_sats: serviceFee,
}
} }
async PayAddress(ctx: Types.UserContext, req: Types.PayAddressRequest): Promise<Types.PayAddressResponse> { async PayAddress(ctx: Types.UserContext, req: Types.PayAddressRequest): Promise<Types.PayAddressResponse> {
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)
@ -607,66 +569,18 @@ export default class {
throw new Error("request a swap quote before paying an external address") throw new Error("request a swap quote before paying an external address")
} }
const app = await this.storage.applicationStorage.GetApplication(ctx.app_id) const app = await this.storage.applicationStorage.GetApplication(ctx.app_id)
const txSwap = await this.storage.paymentStorage.GetTransactionSwap(req.swap_operation_id, ctx.app_user_id)
if (!txSwap) {
throw new Error("swap quote not found")
}
const info = await this.lnd.GetInfo()
if (info.blockHeight >= txSwap.timeout_block_height) {
throw new Error("swap timeout")
}
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'),
}
}
// the swap and the invoice payment are linked, swap will not start until the invoice payment is started, and will not complete once the invoice payment is completed
let swapResult = { ok: false, error: "swap never completed" } as { ok: true, txId: string } | { ok: false, error: string }
this.swaps.reverseSwaps.SubscribeToTransactionSwap(data, result => {
swapResult = result
})
let payment: Types.PayInvoiceResponse let payment: Types.PayInvoiceResponse
try { const swap = await this.swaps.PayAddrWithSwap(ctx.app_user_id, req.swap_operation_id, req.address, async (invoice) => {
payment = await this.PayInvoice(ctx.user_id, { payment = await this.PayInvoice(ctx.user_id, {
amount: 0, amount: 0,
invoice: txSwap.invoice invoice: invoice
}, app, { swapOperationId: req.swap_operation_id }) }, app, { swapOperationId: req.swap_operation_id })
if (!swapResult.ok) { })
this.log("invoice payment successful, but swap failed")
await this.storage.paymentStorage.FailTransactionSwap(req.swap_operation_id, req.address, swapResult.error)
throw new Error(swapResult.error)
}
this.log("swap completed successfully")
await this.storage.paymentStorage.FinalizeTransactionSwap(req.swap_operation_id, req.address, swapResult.txId)
} catch (err: any) {
if (swapResult.ok) {
this.log("failed to pay swap invoice, but swap completed successfully", swapResult.txId)
await this.storage.paymentStorage.FailTransactionSwap(req.swap_operation_id, req.address, err.message)
} else {
this.log("failed to pay swap invoice and swap failed", swapResult.error)
await this.storage.paymentStorage.FailTransactionSwap(req.swap_operation_id, req.address, swapResult.error)
}
throw err
}
const networkFeesTotal = txSwap.chain_fee_sats + txSwap.swap_fee_sats // + payment.network_fee
return { return {
txId: swapResult.txId, txId: swap.txId,
network_fee: networkFeesTotal, network_fee: swap.network_fee,
service_fee: payment.service_fee, service_fee: payment!.service_fee,
operation_id: payment.operation_id, operation_id: payment!.operation_id,
} }
} }

View file

@ -91,6 +91,13 @@ export default (mainHandler: Main): Types.ServerMethods => {
if (err != null) throw new Error(err.message) if (err != null) throw new Error(err.message)
return mainHandler.adminManager.CloseChannel(req) return mainHandler.adminManager.CloseChannel(req)
}, },
GetAdminTransactionSwapQuote: async ({ ctx, req }) => {
const err = Types.TransactionSwapRequestValidate(req, {
transaction_amount_sats_CustomCheck: amt => amt > 0
})
if (err != null) throw new Error(err.message)
return mainHandler.adminManager.GetAdminTransactionSwapQuote(req)
},
GetProvidersDisruption: async () => { GetProvidersDisruption: async () => {
return mainHandler.metricsManager.GetProvidersDisruption() return mainHandler.metricsManager.GetProvidersDisruption()
}, },