diff --git a/proto/autogenerated/client.md b/proto/autogenerated/client.md index 553049fd..0d4ce724 100644 --- a/proto/autogenerated/client.md +++ b/proto/autogenerated/client.md @@ -93,6 +93,11 @@ The nostr server will send back a message response, and inside the body there wi - input: [MessagingToken](#MessagingToken) - This methods has an __empty__ __response__ body +- GetAdminTransactionSwapQuote + - auth type: __Admin__ + - input: [TransactionSwapRequest](#TransactionSwapRequest) + - output: [TransactionSwapQuote](#TransactionSwapQuote) + - GetAppsMetrics - auth type: __Metrics__ - 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) - output: [PayAddressResponse](#PayAddressResponse) +- PayAdminTransactionSwap + - auth type: __Admin__ + - input: [TransactionSwapQuoteRequest](#TransactionSwapQuoteRequest) + - output: [AdminSwapResponse](#AdminSwapResponse) + - PayInvoice - auth type: __User__ - 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) - 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 - auth type: __App__ - 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) - output: [PayAddressResponse](#PayAddressResponse) +- PayAdminTransactionSwap + - auth type: __Admin__ + - http method: __post__ + - http route: __/api/admin/swap/transaction/pay__ + - input: [TransactionSwapQuoteRequest](#TransactionSwapQuoteRequest) + - output: [AdminSwapResponse](#AdminSwapResponse) + - PayAppUserInvoice - auth type: __App__ - http method: __post__ @@ -1062,6 +1086,10 @@ The nostr server will send back a message response, and inside the body there wi - __name__: _string_ - __price_sats__: _number_ +### AdminSwapResponse + - __network_fee__: _number_ + - __tx_id__: _string_ + ### AppMetrics - __app__: _[Application](#Application)_ - __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_ - __transaction_amount_sats__: _number_ +### TransactionSwapQuoteRequest + - __address__: _string_ + - __swap_operation_id__: _string_ + ### TransactionSwapRequest - __transaction_amount_sats__: _number_ diff --git a/proto/autogenerated/go/http_client.go b/proto/autogenerated/go/http_client.go index 77e301b5..b5d4c67b 100644 --- a/proto/autogenerated/go/http_client.go +++ b/proto/autogenerated/go/http_client.go @@ -66,83 +66,85 @@ type Client struct { BanDebit func(req DebitOperation) error BanUser func(req BanUserRequest) (*BanUserResponse, error) // batching method: BatchUser not implemented - CloseChannel func(req CloseChannelRequest) (*CloseChannelResponse, error) - CreateOneTimeInviteLink func(req CreateOneTimeInviteLinkRequest) (*CreateOneTimeInviteLinkResponse, error) - DecodeInvoice func(req DecodeInvoiceRequest) (*DecodeInvoiceResponse, error) - DeleteUserOffer func(req OfferId) error - EditDebit func(req DebitAuthorizationRequest) error - EncryptionExchange func(req EncryptionExchangeRequest) error - EnrollAdminToken func(req EnrollAdminTokenRequest) error - EnrollMessagingToken func(req MessagingToken) error - GetApp func() (*Application, error) - GetAppUser func(req GetAppUserRequest) (*AppUser, error) - GetAppUserLNURLInfo func(req GetAppUserLNURLInfoRequest) (*LnurlPayInfoResponse, error) - GetAppsMetrics func(req AppsMetricsRequest) (*AppsMetrics, error) - GetBundleMetrics func(req LatestBundleMetricReq) (*BundleMetrics, error) - GetDebitAuthorizations func() (*DebitAuthorizations, error) - GetErrorStats func() (*ErrorStats, error) - GetHttpCreds func() (*HttpCreds, error) - GetInviteLinkState func(req GetInviteTokenStateRequest) (*GetInviteTokenStateResponse, error) - GetLNURLChannelLink func() (*LnurlLinkResponse, error) - GetLiveDebitRequests func() (*LiveDebitRequest, error) - GetLiveManageRequests func() (*LiveManageRequest, error) - GetLiveUserOperations func() (*LiveUserOperation, error) - GetLndForwardingMetrics func(req LndMetricsRequest) (*LndForwardingMetrics, error) - GetLndMetrics func(req LndMetricsRequest) (*LndMetrics, error) - GetLnurlPayInfo func(query GetLnurlPayInfo_Query) (*LnurlPayInfoResponse, error) - GetLnurlPayLink func() (*LnurlLinkResponse, error) - GetLnurlWithdrawInfo func(query GetLnurlWithdrawInfo_Query) (*LnurlWithdrawInfoResponse, error) - GetLnurlWithdrawLink func() (*LnurlLinkResponse, error) - GetManageAuthorizations func() (*ManageAuthorizations, error) - GetMigrationUpdate func() (*MigrationUpdate, error) - GetNPubLinkingState func(req GetNPubLinking) (*NPubLinking, error) - GetPaymentState func(req GetPaymentStateRequest) (*PaymentState, error) - GetProvidersDisruption func() (*ProvidersDisruption, error) - GetSeed func() (*LndSeed, error) - GetSingleBundleMetrics func(req SingleMetricReq) (*BundleData, error) - GetSingleUsageMetrics func(req SingleMetricReq) (*UsageMetricTlv, error) - GetTransactionSwapQuote func(req TransactionSwapRequest) (*TransactionSwapQuote, error) - GetUsageMetrics func(req LatestUsageMetricReq) (*UsageMetrics, error) - GetUserInfo func() (*UserInfo, error) - GetUserOffer func(req OfferId) (*OfferConfig, error) - GetUserOfferInvoices func(req GetUserOfferInvoicesReq) (*OfferInvoices, error) - GetUserOffers func() (*UserOffers, error) - GetUserOperations func(req GetUserOperationsRequest) (*GetUserOperationsResponse, error) - HandleLnurlAddress func(routeParams HandleLnurlAddress_RouteParams) (*LnurlPayInfoResponse, error) - HandleLnurlPay func(query HandleLnurlPay_Query) (*HandleLnurlPayResponse, error) - HandleLnurlWithdraw func(query HandleLnurlWithdraw_Query) error - Health func() error - LinkNPubThroughToken func(req LinkNPubThroughTokenRequest) error - ListChannels func() (*LndChannels, error) - ListSwaps func() (*SwapsList, error) - LndGetInfo func(req LndGetInfoRequest) (*LndGetInfoResponse, error) - NewAddress func(req NewAddressRequest) (*NewAddressResponse, error) - NewInvoice func(req NewInvoiceRequest) (*NewInvoiceResponse, error) - NewProductInvoice func(query NewProductInvoice_Query) (*NewInvoiceResponse, error) - OpenChannel func(req OpenChannelRequest) (*OpenChannelResponse, error) - PayAddress func(req PayAddressRequest) (*PayAddressResponse, error) - PayAppUserInvoice func(req PayAppUserInvoiceRequest) (*PayInvoiceResponse, error) - PayInvoice func(req PayInvoiceRequest) (*PayInvoiceResponse, error) - PingSubProcesses func() error - RequestNPubLinkingToken func(req RequestNPubLinkingTokenRequest) (*RequestNPubLinkingTokenResponse, error) - ResetDebit func(req DebitOperation) error - ResetManage func(req ManageOperation) error - ResetMetricsStorages func() error - ResetNPubLinkingToken func(req RequestNPubLinkingTokenRequest) (*RequestNPubLinkingTokenResponse, error) - RespondToDebit func(req DebitResponse) error - SendAppUserToAppPayment func(req SendAppUserToAppPaymentRequest) error - SendAppUserToAppUserPayment func(req SendAppUserToAppUserPaymentRequest) error - SetMockAppBalance func(req SetMockAppBalanceRequest) error - SetMockAppUserBalance func(req SetMockAppUserBalanceRequest) error - SetMockInvoiceAsPaid func(req SetMockInvoiceAsPaidRequest) error - SubToWebRtcCandidates func() (*WebRtcCandidate, error) - SubmitWebRtcMessage func(req WebRtcMessage) (*WebRtcAnswer, error) - UpdateCallbackUrl func(req CallbackUrl) (*CallbackUrl, error) - UpdateChannelPolicy func(req UpdateChannelPolicyRequest) error - UpdateUserOffer func(req OfferConfig) error - UseInviteLink func(req UseInviteLinkRequest) error - UserHealth func() (*UserHealthState, error) - ZipMetricsStorages func() (*ZippedMetrics, error) + CloseChannel func(req CloseChannelRequest) (*CloseChannelResponse, error) + CreateOneTimeInviteLink func(req CreateOneTimeInviteLinkRequest) (*CreateOneTimeInviteLinkResponse, error) + DecodeInvoice func(req DecodeInvoiceRequest) (*DecodeInvoiceResponse, error) + DeleteUserOffer func(req OfferId) error + EditDebit func(req DebitAuthorizationRequest) error + EncryptionExchange func(req EncryptionExchangeRequest) error + EnrollAdminToken func(req EnrollAdminTokenRequest) error + EnrollMessagingToken func(req MessagingToken) error + GetAdminTransactionSwapQuote func(req TransactionSwapRequest) (*TransactionSwapQuote, error) + GetApp func() (*Application, error) + GetAppUser func(req GetAppUserRequest) (*AppUser, error) + GetAppUserLNURLInfo func(req GetAppUserLNURLInfoRequest) (*LnurlPayInfoResponse, error) + GetAppsMetrics func(req AppsMetricsRequest) (*AppsMetrics, error) + GetBundleMetrics func(req LatestBundleMetricReq) (*BundleMetrics, error) + GetDebitAuthorizations func() (*DebitAuthorizations, error) + GetErrorStats func() (*ErrorStats, error) + GetHttpCreds func() (*HttpCreds, error) + GetInviteLinkState func(req GetInviteTokenStateRequest) (*GetInviteTokenStateResponse, error) + GetLNURLChannelLink func() (*LnurlLinkResponse, error) + GetLiveDebitRequests func() (*LiveDebitRequest, error) + GetLiveManageRequests func() (*LiveManageRequest, error) + GetLiveUserOperations func() (*LiveUserOperation, error) + GetLndForwardingMetrics func(req LndMetricsRequest) (*LndForwardingMetrics, error) + GetLndMetrics func(req LndMetricsRequest) (*LndMetrics, error) + GetLnurlPayInfo func(query GetLnurlPayInfo_Query) (*LnurlPayInfoResponse, error) + GetLnurlPayLink func() (*LnurlLinkResponse, error) + GetLnurlWithdrawInfo func(query GetLnurlWithdrawInfo_Query) (*LnurlWithdrawInfoResponse, error) + GetLnurlWithdrawLink func() (*LnurlLinkResponse, error) + GetManageAuthorizations func() (*ManageAuthorizations, error) + GetMigrationUpdate func() (*MigrationUpdate, error) + GetNPubLinkingState func(req GetNPubLinking) (*NPubLinking, error) + GetPaymentState func(req GetPaymentStateRequest) (*PaymentState, error) + GetProvidersDisruption func() (*ProvidersDisruption, error) + GetSeed func() (*LndSeed, error) + GetSingleBundleMetrics func(req SingleMetricReq) (*BundleData, error) + GetSingleUsageMetrics func(req SingleMetricReq) (*UsageMetricTlv, error) + GetTransactionSwapQuote func(req TransactionSwapRequest) (*TransactionSwapQuote, error) + GetUsageMetrics func(req LatestUsageMetricReq) (*UsageMetrics, error) + GetUserInfo func() (*UserInfo, error) + GetUserOffer func(req OfferId) (*OfferConfig, error) + GetUserOfferInvoices func(req GetUserOfferInvoicesReq) (*OfferInvoices, error) + GetUserOffers func() (*UserOffers, error) + GetUserOperations func(req GetUserOperationsRequest) (*GetUserOperationsResponse, error) + HandleLnurlAddress func(routeParams HandleLnurlAddress_RouteParams) (*LnurlPayInfoResponse, error) + HandleLnurlPay func(query HandleLnurlPay_Query) (*HandleLnurlPayResponse, error) + HandleLnurlWithdraw func(query HandleLnurlWithdraw_Query) error + Health func() error + LinkNPubThroughToken func(req LinkNPubThroughTokenRequest) error + ListChannels func() (*LndChannels, error) + ListSwaps func() (*SwapsList, error) + LndGetInfo func(req LndGetInfoRequest) (*LndGetInfoResponse, error) + NewAddress func(req NewAddressRequest) (*NewAddressResponse, error) + NewInvoice func(req NewInvoiceRequest) (*NewInvoiceResponse, error) + NewProductInvoice func(query NewProductInvoice_Query) (*NewInvoiceResponse, error) + OpenChannel func(req OpenChannelRequest) (*OpenChannelResponse, error) + PayAddress func(req PayAddressRequest) (*PayAddressResponse, error) + PayAdminTransactionSwap func(req TransactionSwapQuoteRequest) (*AdminSwapResponse, error) + PayAppUserInvoice func(req PayAppUserInvoiceRequest) (*PayInvoiceResponse, error) + PayInvoice func(req PayInvoiceRequest) (*PayInvoiceResponse, error) + PingSubProcesses func() error + RequestNPubLinkingToken func(req RequestNPubLinkingTokenRequest) (*RequestNPubLinkingTokenResponse, error) + ResetDebit func(req DebitOperation) error + ResetManage func(req ManageOperation) error + ResetMetricsStorages func() error + ResetNPubLinkingToken func(req RequestNPubLinkingTokenRequest) (*RequestNPubLinkingTokenResponse, error) + RespondToDebit func(req DebitResponse) error + SendAppUserToAppPayment func(req SendAppUserToAppPaymentRequest) error + SendAppUserToAppUserPayment func(req SendAppUserToAppUserPaymentRequest) error + SetMockAppBalance func(req SetMockAppBalanceRequest) error + SetMockAppUserBalance func(req SetMockAppUserBalanceRequest) error + SetMockInvoiceAsPaid func(req SetMockInvoiceAsPaidRequest) error + SubToWebRtcCandidates func() (*WebRtcCandidate, error) + SubmitWebRtcMessage func(req WebRtcMessage) (*WebRtcAnswer, error) + UpdateCallbackUrl func(req CallbackUrl) (*CallbackUrl, error) + UpdateChannelPolicy func(req UpdateChannelPolicyRequest) error + UpdateUserOffer func(req OfferConfig) error + UseInviteLink func(req UseInviteLinkRequest) error + UserHealth func() (*UserHealthState, error) + ZipMetricsStorages func() (*ZippedMetrics, error) } func NewClient(params ClientParams) *Client { @@ -664,6 +666,35 @@ func NewClient(params ClientParams) *Client { } 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) { auth, err := params.RetrieveAppAuth() if err != nil { @@ -1834,6 +1865,35 @@ func NewClient(params ClientParams) *Client { } 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) { auth, err := params.RetrieveAppAuth() if err != nil { diff --git a/proto/autogenerated/go/types.go b/proto/autogenerated/go/types.go index a8398d95..e93732cb 100644 --- a/proto/autogenerated/go/types.go +++ b/proto/autogenerated/go/types.go @@ -123,6 +123,10 @@ type AddProductRequest struct { Name string `json:"name"` Price_sats int64 `json:"price_sats"` } +type AdminSwapResponse struct { + Network_fee int64 `json:"network_fee"` + Tx_id string `json:"tx_id"` +} type AppMetrics struct { App *Application `json:"app"` Available int64 `json:"available"` @@ -673,6 +677,10 @@ type TransactionSwapQuote struct { Swap_operation_id string `json:"swap_operation_id"` 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 { Transaction_amount_sats int64 `json:"transaction_amount_sats"` } diff --git a/proto/autogenerated/ts/express_server.ts b/proto/autogenerated/ts/express_server.ts index ded9c78e..bfdc05bc 100644 --- a/proto/autogenerated/ts/express_server.ts +++ b/proto/autogenerated/ts/express_server.ts @@ -869,6 +869,28 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => { opts.metricsCallback([{ ...info, ...stats, ...authContext }]) } catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } }) + if (!opts.allowNotImplementedMethods && !methods.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') app.post('/api/app/get', async (req, res) => { 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 }]) } 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') app.post('/api/app/invoice/pay', async (req, res) => { const info: Types.RequestInfo = { rpcName: 'PayAppUserInvoice', batch: false, nostr: false, batchSize: 0} diff --git a/proto/autogenerated/ts/http_client.ts b/proto/autogenerated/ts/http_client.ts index 2f011982..9759db19 100644 --- a/proto/autogenerated/ts/http_client.ts +++ b/proto/autogenerated/ts/http_client.ts @@ -273,6 +273,20 @@ export default (params: ClientParams) => ({ } return { status: 'ERROR', reason: 'invalid response' } }, + GetAdminTransactionSwapQuote: async (request: Types.TransactionSwapRequest): Promise => { + 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 => { const auth = await params.retrieveAppAuth() if (auth === null) throw new Error('retrieveAppAuth() returned null') @@ -881,6 +895,20 @@ export default (params: ClientParams) => ({ } return { status: 'ERROR', reason: 'invalid response' } }, + PayAdminTransactionSwap: async (request: Types.TransactionSwapQuoteRequest): Promise => { + 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 => { const auth = await params.retrieveAppAuth() if (auth === null) throw new Error('retrieveAppAuth() returned null') diff --git a/proto/autogenerated/ts/nostr_client.ts b/proto/autogenerated/ts/nostr_client.ts index 98719c5f..a010bea7 100644 --- a/proto/autogenerated/ts/nostr_client.ts +++ b/proto/autogenerated/ts/nostr_client.ts @@ -230,6 +230,21 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ } return { status: 'ERROR', reason: 'invalid response' } }, + GetAdminTransactionSwapQuote: async (request: Types.TransactionSwapRequest): Promise => { + 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 => { const auth = await params.retrieveNostrMetricsAuth() 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' } }, + PayAdminTransactionSwap: async (request: Types.TransactionSwapQuoteRequest): Promise => { + 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 => { const auth = await params.retrieveNostrUserAuth() if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') diff --git a/proto/autogenerated/ts/nostr_transport.ts b/proto/autogenerated/ts/nostr_transport.ts index 124c7443..38d855df 100644 --- a/proto/autogenerated/ts/nostr_transport.ts +++ b/proto/autogenerated/ts/nostr_transport.ts @@ -687,6 +687,22 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => { opts.metricsCallback([{ ...info, ...stats, ...authContext }]) }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } break + case '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': try { 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 }]) }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 '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': try { if (!methods.PayInvoice) throw new Error('method: PayInvoice is not implemented') diff --git a/proto/autogenerated/ts/types.ts b/proto/autogenerated/ts/types.ts index 271c9f34..70d51fc6 100644 --- a/proto/autogenerated/ts/types.ts +++ b/proto/autogenerated/ts/types.ts @@ -7,8 +7,8 @@ export type RequestMetric = AuthContext & RequestInfo & RequestStats & { error?: export type AdminContext = { 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 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 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 | GetAdminTransactionSwapQuote_Output | GetInviteLinkState_Output | GetSeed_Output | ListChannels_Output | LndGetInfo_Output | OpenChannel_Output | PayAdminTransactionSwap_Output | UpdateChannelPolicy_Output export type AppContext = { 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_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_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_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_Output = ResultError | ({ status: 'OK' } & PayInvoiceResponse) @@ -348,6 +354,7 @@ export type ServerMethods = { EncryptionExchange?: (req: EncryptionExchange_Input & {ctx: GuestContext }) => Promise EnrollAdminToken?: (req: EnrollAdminToken_Input & {ctx: UserContext }) => Promise EnrollMessagingToken?: (req: EnrollMessagingToken_Input & {ctx: UserContext }) => Promise + GetAdminTransactionSwapQuote?: (req: GetAdminTransactionSwapQuote_Input & {ctx: AdminContext }) => Promise GetApp?: (req: GetApp_Input & {ctx: AppContext }) => Promise GetAppUser?: (req: GetAppUser_Input & {ctx: AppContext }) => Promise GetAppUserLNURLInfo?: (req: GetAppUserLNURLInfo_Input & {ctx: AppContext }) => Promise @@ -395,6 +402,7 @@ export type ServerMethods = { NewProductInvoice?: (req: NewProductInvoice_Input & {ctx: UserContext }) => Promise OpenChannel?: (req: OpenChannel_Input & {ctx: AdminContext }) => Promise PayAddress?: (req: PayAddress_Input & {ctx: UserContext }) => Promise + PayAdminTransactionSwap?: (req: PayAdminTransactionSwap_Input & {ctx: AdminContext }) => Promise PayAppUserInvoice?: (req: PayAppUserInvoice_Input & {ctx: AppContext }) => Promise PayInvoice?: (req: PayInvoice_Input & {ctx: UserContext }) => Promise PingSubProcesses?: (req: PingSubProcesses_Input & {ctx: MetricsContext }) => Promise @@ -659,6 +667,29 @@ export const AddProductRequestValidate = (o?: AddProductRequest, opts: AddProduc 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 = { app: Application available: number @@ -3962,6 +3993,29 @@ export const TransactionSwapQuoteValidate = (o?: TransactionSwapQuote, opts: Tra 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 = { transaction_amount_sats: number } diff --git a/proto/service/methods.proto b/proto/service/methods.proto index 20f155a3..c533ed3e 100644 --- a/proto/service/methods.proto +++ b/proto/service/methods.proto @@ -175,6 +175,20 @@ service LightningPub { 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) { option (auth_type) = "Metrics"; option (http_method) = "post"; @@ -480,7 +494,7 @@ service LightningPub { option (http_method) = "post"; option (http_route) = "/api/user/operations"; option (nostr) = true; - } + } rpc NewAddress(structs.NewAddressRequest) returns (structs.NewAddressResponse) { option (auth_type) = "User"; diff --git a/proto/service/structs.proto b/proto/service/structs.proto index 186475dc..2966bc8a 100644 --- a/proto/service/structs.proto +++ b/proto/service/structs.proto @@ -834,6 +834,11 @@ message TransactionSwapRequest { int64 transaction_amount_sats = 2; } +message TransactionSwapQuoteRequest { + string address = 1; + string swap_operation_id = 2; +} + message TransactionSwapQuote { string swap_operation_id = 1; int64 invoice_amount_sats = 2; @@ -844,6 +849,11 @@ message TransactionSwapQuote { int64 service_fee_sats = 7; } +message AdminSwapResponse { + string tx_id = 1; + int64 network_fee = 2; +} + message SwapOperation { string swap_operation_id = 1; optional UserOperation operation_payment = 2; diff --git a/src/services/lnd/swaps.ts b/src/services/lnd/swaps.ts index df0cd3af..62a69e0e 100644 --- a/src/services/lnd/swaps.ts +++ b/src/services/lnd/swaps.ts @@ -15,7 +15,8 @@ import { getLogger, PubLogger, ERROR } from '../helpers/logger.js'; import SettingsManager from '../main/settingsManager.js'; import * as Types from '../../../proto/autogenerated/ts/types.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 InvoiceSwapInfo = { paymentHash: string, keys: ECPairInterface } type InvoiceSwapData = { createdResponse: InvoiceSwapResponse, info: InvoiceSwapInfo } @@ -47,9 +48,17 @@ export type TransactionSwapData = { createdResponse: TransactionSwapResponse, in export class Swaps { reverseSwaps: ReverseSwaps submarineSwaps: SubmarineSwaps - constructor(settings: SettingsManager) { + storage: Storage + lnd: LND + log = getLogger({ component: 'swaps' }) + constructor(settings: SettingsManager, storage: Storage) { this.reverseSwaps = new ReverseSwaps(settings) this.submarineSwaps = new SubmarineSwaps(settings) + this.storage = storage + } + + SetLnd = (lnd: LND) => { + this.lnd = lnd } Stop = () => { } @@ -58,6 +67,110 @@ export class Swaps { const keys = ECPairFactory(ecc).fromPrivateKey(Buffer.from(privateKey, 'hex')) return keys } + + async GetTxSwapQuote(appUserId: string, amt: number, getServiceFee: (decodedAmt: number) => number): Promise { + 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) { + 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 { diff --git a/src/services/main/adminManager.ts b/src/services/main/adminManager.ts index 01383599..4531eb30 100644 --- a/src/services/main/adminManager.ts +++ b/src/services/main/adminManager.ts @@ -5,7 +5,9 @@ import Storage from "../storage/index.js"; import * as Types from '../../../proto/autogenerated/ts/types.js' import LND from "../lnd/lnd.js"; import SettingsManager from "./settingsManager.js"; +import { Swaps } from "../lnd/swaps.js"; export class AdminManager { + settings: SettingsManager storage: Storage log = getLogger({ component: "adminManager" }) adminNpub = "" @@ -17,10 +19,13 @@ export class AdminManager { interval: NodeJS.Timer appNprofile: string lnd: LND + swaps: Swaps nostrConnected: boolean = false private nostrReset: () => Promise = 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.swaps = swaps this.dataDir = settings.getStorageSettings().dataDir this.adminNpubPath = getDataPath(this.dataDir, 'admin.npub') this.adminEnrollTokenPath = getDataPath(this.dataDir, 'admin.enroll') @@ -45,6 +50,7 @@ export class AdminManager { setLND = (lnd: LND) => { this.lnd = lnd + this.swaps.SetLnd(lnd) } setNostrConnected = (connected: boolean) => { @@ -253,6 +259,25 @@ export class AdminManager { closing_txid: Buffer.from(res.txid).toString('hex') } } + + async GetAdminTransactionSwapQuote(req: Types.TransactionSwapRequest): Promise { + return this.swaps.GetTxSwapQuote("admin", req.transaction_amount_sats, () => 0) + } + async PayAdminTransactionSwap(req: Types.TransactionSwapQuoteRequest): Promise { + 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) => { diff --git a/src/services/main/index.ts b/src/services/main/index.ts index ef07529e..ab528b84 100644 --- a/src/services/main/index.ts +++ b/src/services/main/index.ts @@ -80,7 +80,7 @@ export default class { 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.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.applicationManager = new ApplicationManager(this.storage, this.settings, this.paymentManager) this.appUserManager = new AppUserManager(this.storage, this.settings, this.applicationManager) diff --git a/src/services/main/init.ts b/src/services/main/init.ts index dd3051e8..3cbba602 100644 --- a/src/services/main/init.ts +++ b/src/services/main/init.ts @@ -11,6 +11,7 @@ import { AdminManager } from "./adminManager.js" import SettingsManager from "./settingsManager.js" import { LoadStorageSettingsFromEnv } from "../storage/index.js" import { NostrSender } from "../nostr/sender.js" +import { Swaps } from "../lnd/swaps.js" export type AppData = { privateKey: string; publicKey: string; @@ -32,7 +33,8 @@ export const initMainHandler = async (log: PubLogger, settingsManager: SettingsM const utils = storageManager.utils const unlocker = new Unlocker(settingsManager, storageManager) 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 if (settingsManager.getSettings().serviceSettings.wizard) { wizard = new Wizard(settingsManager, storageManager, adminManager) diff --git a/src/services/main/liquidityProvider.ts b/src/services/main/liquidityProvider.ts index 474c04ba..b03c30f3 100644 --- a/src/services/main/liquidityProvider.ts +++ b/src/services/main/liquidityProvider.ts @@ -121,12 +121,11 @@ export class LiquidityProvider { await this.invoicePaidCb(res.operation.identifier, res.operation.amount, 'provider') this.incrementProviderBalance(res.operation.amount) this.latestReceivedBalance = res.latest_balance - if (!res.operation.inbound && !res.operation.confirmed) { - delete this.pendingPaymentsAck[res.operation.identifier] - } } catch (err: any) { this.log("error processing incoming invoice", err.message) } + } else if (res.operation.type === Types.UserOperationType.OUTGOING_INVOICE) { + delete this.pendingPaymentsAck[res.operation.identifier] } }) } diff --git a/src/services/main/paymentManager.ts b/src/services/main/paymentManager.ts index 87b1b06f..242defa4 100644 --- a/src/services/main/paymentManager.ts +++ b/src/services/main/paymentManager.ts @@ -61,7 +61,7 @@ export default class { swaps: Swaps invoiceLock: InvoiceLock 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.metrics = metrics this.settings = settings @@ -69,7 +69,7 @@ export default class { this.liquidityManager = liquidityManager this.utils = utils 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.invoicePaidCb = invoicePaidCb this.invoiceLock = new InvoiceLock() @@ -539,54 +539,16 @@ export default class { } } + + async GetTransactionSwapQuote(ctx: Types.UserContext, req: Types.TransactionSwapRequest): Promise { - 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 isManagedUser = ctx.user_id !== app.owner.user_id - const serviceFee = this.getSendServiceFee(Types.UserOperationType.OUTGOING_INVOICE, decoded.numSatoshis, isManagedUser) - 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 this.swaps.GetTxSwapQuote(ctx.app_user_id, req.transaction_amount_sats, decodedAmt => { + const isManagedUser = ctx.user_id !== app.owner.user_id + return this.getSendServiceFee(Types.UserOperationType.OUTGOING_INVOICE, decodedAmt, isManagedUser) }) - 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 { await this.watchDog.PaymentRequested() 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") } 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 - 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, { amount: 0, - invoice: txSwap.invoice + invoice: invoice }, app, { swapOperationId: req.swap_operation_id }) - if (!swapResult.ok) { - this.log("invoice payment successful, but swap failed") - await this.storage.paymentStorage.FailTransactionSwap(req.swap_operation_id, 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 { - txId: swapResult.txId, - network_fee: networkFeesTotal, - service_fee: payment.service_fee, - operation_id: payment.operation_id, + txId: swap.txId, + network_fee: swap.network_fee, + service_fee: payment!.service_fee, + operation_id: payment!.operation_id, } } diff --git a/src/services/serverMethods/index.ts b/src/services/serverMethods/index.ts index 4a175ee6..5a044167 100644 --- a/src/services/serverMethods/index.ts +++ b/src/services/serverMethods/index.ts @@ -91,6 +91,13 @@ export default (mainHandler: Main): Types.ServerMethods => { if (err != null) throw new Error(err.message) 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 () => { return mainHandler.metricsManager.GetProvidersDisruption() },