Merge pull request #869 from shocknet/admin-swaps

admin swaps
This commit is contained in:
Justin (shocknet) 2026-02-05 12:05:58 -05:00 committed by GitHub
commit a362cb4b91
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 2626 additions and 870 deletions

View file

@ -22,6 +22,7 @@ import { AppUserDevice } from "./build/src/services/storage/entity/AppUserDevice
import { UserAccess } from "./build/src/services/storage/entity/UserAccess.js"
import { AdminSettings } from "./build/src/services/storage/entity/AdminSettings.js"
import { TransactionSwap } from "./build/src/services/storage/entity/TransactionSwap.js"
import { InvoiceSwap } from "./build/src/services/storage/entity/InvoiceSwap.js"
import { Initial1703170309875 } from './build/src/services/storage/migrations/1703170309875-initial.js'
import { LspOrder1718387847693 } from './build/src/services/storage/migrations/1718387847693-lsp_order.js'
@ -47,6 +48,8 @@ import { TxSwap1762890527098 } from './build/src/services/storage/migrations/176
import { TxSwapAddress1764779178945 } from './build/src/services/storage/migrations/1764779178945-tx_swap_address.js'
import { ClinkRequester1765497600000 } from './build/src/services/storage/migrations/1765497600000-clink_requester.js'
import { TrackedProviderHeight1766504040000 } from './build/src/services/storage/migrations/1766504040000-tracked_provider_height.js'
import { SwapsServiceUrl1768413055036 } from './build/src/services/storage/migrations/1768413055036-swaps_service_url.js'
import { InvoiceSwaps1769529793283 } from './build/src/services/storage/migrations/1769529793283-invoice_swaps.js'
export default new DataSource({
type: "better-sqlite3",
@ -56,11 +59,11 @@ export default new DataSource({
PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264, DebitToPub1727105758354, UserCbUrl1727112281043,
UserOffer1733502626042, ManagementGrant1751307732346, InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611,
AppUserDevice1753285173175, UserAccess1759426050669, AddBlindToUserOffer1760000000000, ApplicationAvatarUrl1761000001000, AdminSettings1761683639419, TxSwap1762890527098,
TxSwapAddress1764779178945, ClinkRequester1765497600000, TrackedProviderHeight1766504040000],
TxSwapAddress1764779178945, ClinkRequester1765497600000, TrackedProviderHeight1766504040000, SwapsServiceUrl1768413055036, InvoiceSwaps1769529793283],
entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment,
UserBasicAuth, UserEphemeralKey, Product, UserToUserPayment, Application, ApplicationUser, UserToUserPayment, LspOrder, LndNodeInfo,
TrackedProvider, InviteToken, DebitAccess, UserOffer, ManagementGrant, AppUserDevice, UserAccess, AdminSettings, TransactionSwap],
TrackedProvider, InviteToken, DebitAccess, UserOffer, ManagementGrant, AppUserDevice, UserAccess, AdminSettings, TransactionSwap, InvoiceSwap],
// synchronize: true,
})
//npx typeorm migration:generate ./src/services/storage/migrations/swaps_service_url -d ./datasource.js
//npx typeorm migration:generate ./src/services/storage/migrations/invoice_swaps_fixes -d ./datasource.js

View file

@ -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
- GetAdminInvoiceSwapQuotes
- auth type: __Admin__
- input: [InvoiceSwapRequest](#InvoiceSwapRequest)
- output: [InvoiceSwapQuoteList](#InvoiceSwapQuoteList)
- GetAdminTransactionSwapQuotes
- auth type: __Admin__
- input: [TransactionSwapRequest](#TransactionSwapRequest)
@ -243,20 +248,25 @@ The nostr server will send back a message response, and inside the body there wi
- input: [LinkNPubThroughTokenRequest](#LinkNPubThroughTokenRequest)
- This methods has an __empty__ __response__ body
- ListAdminSwaps
- ListAdminInvoiceSwaps
- auth type: __Admin__
- This methods has an __empty__ __request__ body
- output: [SwapsList](#SwapsList)
- output: [InvoiceSwapsList](#InvoiceSwapsList)
- ListAdminTxSwaps
- auth type: __Admin__
- This methods has an __empty__ __request__ body
- output: [TxSwapsList](#TxSwapsList)
- ListChannels
- auth type: __Admin__
- This methods has an __empty__ __request__ body
- output: [LndChannels](#LndChannels)
- ListSwaps
- ListTxSwaps
- auth type: __User__
- This methods has an __empty__ __request__ body
- output: [SwapsList](#SwapsList)
- output: [TxSwapsList](#TxSwapsList)
- LndGetInfo
- auth type: __Admin__
@ -290,10 +300,15 @@ The nostr server will send back a message response, and inside the body there wi
- input: [PayAddressRequest](#PayAddressRequest)
- output: [PayAddressResponse](#PayAddressResponse)
- PayAdminInvoiceSwap
- auth type: __Admin__
- input: [PayAdminInvoiceSwapRequest](#PayAdminInvoiceSwapRequest)
- output: [AdminInvoiceSwapResponse](#AdminInvoiceSwapResponse)
- PayAdminTransactionSwap
- auth type: __Admin__
- input: [PayAdminTransactionSwapRequest](#PayAdminTransactionSwapRequest)
- output: [AdminSwapResponse](#AdminSwapResponse)
- output: [AdminTxSwapResponse](#AdminTxSwapResponse)
- PayInvoice
- auth type: __User__
@ -305,6 +320,11 @@ The nostr server will send back a message response, and inside the body there wi
- This methods has an __empty__ __request__ body
- This methods has an __empty__ __response__ body
- RefundAdminInvoiceSwap
- auth type: __Admin__
- input: [RefundAdminInvoiceSwapRequest](#RefundAdminInvoiceSwapRequest)
- output: [AdminInvoiceSwapResponse](#AdminInvoiceSwapResponse)
- ResetDebit
- auth type: __User__
- input: [DebitOperation](#DebitOperation)
@ -540,6 +560,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
- GetAdminInvoiceSwapQuotes
- auth type: __Admin__
- http method: __post__
- http route: __/api/admin/swap/invoice/quote__
- input: [InvoiceSwapRequest](#InvoiceSwapRequest)
- output: [InvoiceSwapQuoteList](#InvoiceSwapQuoteList)
- GetAdminTransactionSwapQuotes
- auth type: __Admin__
- http method: __post__
@ -743,7 +770,7 @@ The nostr server will send back a message response, and inside the body there wi
- GetTransactionSwapQuotes
- auth type: __User__
- http method: __post__
- http route: __/api/user/swap/quote__
- http route: __/api/user/swap/transaction/quote__
- input: [TransactionSwapRequest](#TransactionSwapRequest)
- output: [TransactionSwapQuoteList](#TransactionSwapQuoteList)
@ -834,12 +861,19 @@ The nostr server will send back a message response, and inside the body there wi
- input: [LinkNPubThroughTokenRequest](#LinkNPubThroughTokenRequest)
- This methods has an __empty__ __response__ body
- ListAdminSwaps
- ListAdminInvoiceSwaps
- auth type: __Admin__
- http method: __post__
- http route: __/api/admin/swap/list__
- http route: __/api/admin/swap/invoice/list__
- This methods has an __empty__ __request__ body
- output: [SwapsList](#SwapsList)
- output: [InvoiceSwapsList](#InvoiceSwapsList)
- ListAdminTxSwaps
- auth type: __Admin__
- http method: __post__
- http route: __/api/admin/swap/transaction/list__
- This methods has an __empty__ __request__ body
- output: [TxSwapsList](#TxSwapsList)
- ListChannels
- auth type: __Admin__
@ -848,12 +882,12 @@ The nostr server will send back a message response, and inside the body there wi
- This methods has an __empty__ __request__ body
- output: [LndChannels](#LndChannels)
- ListSwaps
- ListTxSwaps
- auth type: __User__
- http method: __post__
- http route: __/api/user/swap/list__
- http route: __/api/user/swap/transaction/list__
- This methods has an __empty__ __request__ body
- output: [SwapsList](#SwapsList)
- output: [TxSwapsList](#TxSwapsList)
- LndGetInfo
- auth type: __Admin__
@ -899,12 +933,19 @@ The nostr server will send back a message response, and inside the body there wi
- input: [PayAddressRequest](#PayAddressRequest)
- output: [PayAddressResponse](#PayAddressResponse)
- PayAdminInvoiceSwap
- auth type: __Admin__
- http method: __post__
- http route: __/api/admin/swap/invoice/pay__
- input: [PayAdminInvoiceSwapRequest](#PayAdminInvoiceSwapRequest)
- output: [AdminInvoiceSwapResponse](#AdminInvoiceSwapResponse)
- PayAdminTransactionSwap
- auth type: __Admin__
- http method: __post__
- http route: __/api/admin/swap/transaction/pay__
- input: [PayAdminTransactionSwapRequest](#PayAdminTransactionSwapRequest)
- output: [AdminSwapResponse](#AdminSwapResponse)
- output: [AdminTxSwapResponse](#AdminTxSwapResponse)
- PayAppUserInvoice
- auth type: __App__
@ -927,6 +968,13 @@ The nostr server will send back a message response, and inside the body there wi
- This methods has an __empty__ __request__ body
- This methods has an __empty__ __response__ body
- RefundAdminInvoiceSwap
- auth type: __Admin__
- http method: __post__
- http route: __/api/admin/swap/invoice/refund__
- input: [RefundAdminInvoiceSwapRequest](#RefundAdminInvoiceSwapRequest)
- output: [AdminInvoiceSwapResponse](#AdminInvoiceSwapResponse)
- RequestNPubLinkingToken
- auth type: __App__
- http method: __post__
@ -1098,7 +1146,10 @@ The nostr server will send back a message response, and inside the body there wi
- __name__: _string_
- __price_sats__: _number_
### AdminSwapResponse
### AdminInvoiceSwapResponse
- __tx_id__: _string_
### AdminTxSwapResponse
- __network_fee__: _number_
- __tx_id__: _string_
@ -1331,6 +1382,35 @@ The nostr server will send back a message response, and inside the body there wi
- __token__: _string_
- __url__: _string_
### InvoiceSwapOperation
- __failure_reason__: _string_ *this field is optional
- __invoice_paid__: _string_
- __operation_payment__: _[UserOperation](#UserOperation)_ *this field is optional
- __swap_operation_id__: _string_
- __tx_id__: _string_
### InvoiceSwapQuote
- __address__: _string_
- __chain_fee_sats__: _number_
- __invoice__: _string_
- __invoice_amount_sats__: _number_
- __service_fee_sats__: _number_
- __service_url__: _string_
- __swap_fee_sats__: _number_
- __swap_operation_id__: _string_
- __transaction_amount_sats__: _number_
- __tx_id__: _string_
### InvoiceSwapQuoteList
- __quotes__: ARRAY of: _[InvoiceSwapQuote](#InvoiceSwapQuote)_
### InvoiceSwapRequest
- __amount_sats__: _number_
### InvoiceSwapsList
- __quotes__: ARRAY of: _[InvoiceSwapQuote](#InvoiceSwapQuote)_
- __swaps__: ARRAY of: _[InvoiceSwapOperation](#InvoiceSwapOperation)_
### LatestBundleMetricReq
- __limit__: _number_ *this field is optional
@ -1534,6 +1614,11 @@ The nostr server will send back a message response, and inside the body there wi
- __service_fee__: _number_
- __txId__: _string_
### PayAdminInvoiceSwapRequest
- __no_claim__: _boolean_ *this field is optional
- __sat_per_v_byte__: _number_
- __swap_operation_id__: _string_
### PayAdminTransactionSwapRequest
- __address__: _string_
- __swap_operation_id__: _string_
@ -1584,6 +1669,10 @@ The nostr server will send back a message response, and inside the body there wi
### ProvidersDisruption
- __disruptions__: ARRAY of: _[ProviderDisruption](#ProviderDisruption)_
### RefundAdminInvoiceSwapRequest
- __sat_per_v_byte__: _number_
- __swap_operation_id__: _string_
### RelaysMigration
- __relays__: ARRAY of: _string_
@ -1640,16 +1729,6 @@ The nostr server will send back a message response, and inside the body there wi
- __page__: _number_
- __request_id__: _number_ *this field is optional
### SwapOperation
- __address_paid__: _string_
- __failure_reason__: _string_ *this field is optional
- __operation_payment__: _[UserOperation](#UserOperation)_ *this field is optional
- __swap_operation_id__: _string_
### SwapsList
- __quotes__: ARRAY of: _[TransactionSwapQuote](#TransactionSwapQuote)_
- __swaps__: ARRAY of: _[SwapOperation](#SwapOperation)_
### TransactionSwapQuote
- __chain_fee_sats__: _number_
- __invoice_amount_sats__: _number_
@ -1665,6 +1744,16 @@ The nostr server will send back a message response, and inside the body there wi
### TransactionSwapRequest
- __transaction_amount_sats__: _number_
### TxSwapOperation
- __address_paid__: _string_
- __failure_reason__: _string_ *this field is optional
- __operation_payment__: _[UserOperation](#UserOperation)_ *this field is optional
- __swap_operation_id__: _string_
### TxSwapsList
- __quotes__: ARRAY of: _[TransactionSwapQuote](#TransactionSwapQuote)_
- __swaps__: ARRAY of: _[TxSwapOperation](#TxSwapOperation)_
### UpdateChannelPolicyRequest
- __policy__: _[ChannelPolicy](#ChannelPolicy)_
- __update__: _[UpdateChannelPolicyRequest_update](#UpdateChannelPolicyRequest_update)_

View file

@ -74,6 +74,7 @@ type Client struct {
EncryptionExchange func(req EncryptionExchangeRequest) error
EnrollAdminToken func(req EnrollAdminTokenRequest) error
EnrollMessagingToken func(req MessagingToken) error
GetAdminInvoiceSwapQuotes func(req InvoiceSwapRequest) (*InvoiceSwapQuoteList, error)
GetAdminTransactionSwapQuotes func(req TransactionSwapRequest) (*TransactionSwapQuoteList, error)
GetApp func() (*Application, error)
GetAppUser func(req GetAppUserRequest) (*AppUser, error)
@ -114,19 +115,22 @@ type Client struct {
HandleLnurlWithdraw func(query HandleLnurlWithdraw_Query) error
Health func() error
LinkNPubThroughToken func(req LinkNPubThroughTokenRequest) error
ListAdminSwaps func() (*SwapsList, error)
ListAdminInvoiceSwaps func() (*InvoiceSwapsList, error)
ListAdminTxSwaps func() (*TxSwapsList, error)
ListChannels func() (*LndChannels, error)
ListSwaps func() (*SwapsList, error)
ListTxSwaps func() (*TxSwapsList, 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 PayAdminTransactionSwapRequest) (*AdminSwapResponse, error)
PayAdminInvoiceSwap func(req PayAdminInvoiceSwapRequest) (*AdminInvoiceSwapResponse, error)
PayAdminTransactionSwap func(req PayAdminTransactionSwapRequest) (*AdminTxSwapResponse, error)
PayAppUserInvoice func(req PayAppUserInvoiceRequest) (*PayInvoiceResponse, error)
PayInvoice func(req PayInvoiceRequest) (*PayInvoiceResponse, error)
PingSubProcesses func() error
RefundAdminInvoiceSwap func(req RefundAdminInvoiceSwapRequest) (*AdminInvoiceSwapResponse, error)
RequestNPubLinkingToken func(req RequestNPubLinkingTokenRequest) (*RequestNPubLinkingTokenResponse, error)
ResetDebit func(req DebitOperation) error
ResetManage func(req ManageOperation) error
@ -667,6 +671,35 @@ func NewClient(params ClientParams) *Client {
}
return nil
},
GetAdminInvoiceSwapQuotes: func(req InvoiceSwapRequest) (*InvoiceSwapQuoteList, error) {
auth, err := params.RetrieveAdminAuth()
if err != nil {
return nil, err
}
finalRoute := "/api/admin/swap/invoice/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 := InvoiceSwapQuoteList{}
err = json.Unmarshal(resBody, &res)
if err != nil {
return nil, err
}
return &res, nil
},
GetAdminTransactionSwapQuotes: func(req TransactionSwapRequest) (*TransactionSwapQuoteList, error) {
auth, err := params.RetrieveAdminAuth()
if err != nil {
@ -1324,7 +1357,7 @@ func NewClient(params ClientParams) *Client {
if err != nil {
return nil, err
}
finalRoute := "/api/user/swap/quote"
finalRoute := "/api/user/swap/transaction/quote"
body, err := json.Marshal(req)
if err != nil {
return nil, err
@ -1643,12 +1676,12 @@ func NewClient(params ClientParams) *Client {
}
return nil
},
ListAdminSwaps: func() (*SwapsList, error) {
ListAdminInvoiceSwaps: func() (*InvoiceSwapsList, error) {
auth, err := params.RetrieveAdminAuth()
if err != nil {
return nil, err
}
finalRoute := "/api/admin/swap/list"
finalRoute := "/api/admin/swap/invoice/list"
body := []byte{}
resBody, err := doPostRequest(params.BaseURL+finalRoute, body, auth)
if err != nil {
@ -1662,7 +1695,33 @@ func NewClient(params ClientParams) *Client {
if result.Status == "ERROR" {
return nil, fmt.Errorf(result.Reason)
}
res := SwapsList{}
res := InvoiceSwapsList{}
err = json.Unmarshal(resBody, &res)
if err != nil {
return nil, err
}
return &res, nil
},
ListAdminTxSwaps: func() (*TxSwapsList, error) {
auth, err := params.RetrieveAdminAuth()
if err != nil {
return nil, err
}
finalRoute := "/api/admin/swap/transaction/list"
body := []byte{}
resBody, err := doPostRequest(params.BaseURL+finalRoute, body, auth)
if err != nil {
return nil, err
}
result := ResultError{}
err = json.Unmarshal(resBody, &result)
if err != nil {
return nil, err
}
if result.Status == "ERROR" {
return nil, fmt.Errorf(result.Reason)
}
res := TxSwapsList{}
err = json.Unmarshal(resBody, &res)
if err != nil {
return nil, err
@ -1691,12 +1750,12 @@ func NewClient(params ClientParams) *Client {
}
return &res, nil
},
ListSwaps: func() (*SwapsList, error) {
ListTxSwaps: func() (*TxSwapsList, error) {
auth, err := params.RetrieveUserAuth()
if err != nil {
return nil, err
}
finalRoute := "/api/user/swap/list"
finalRoute := "/api/user/swap/transaction/list"
body := []byte{}
resBody, err := doPostRequest(params.BaseURL+finalRoute, body, auth)
if err != nil {
@ -1710,7 +1769,7 @@ func NewClient(params ClientParams) *Client {
if result.Status == "ERROR" {
return nil, fmt.Errorf(result.Reason)
}
res := SwapsList{}
res := TxSwapsList{}
err = json.Unmarshal(resBody, &res)
if err != nil {
return nil, err
@ -1892,7 +1951,36 @@ func NewClient(params ClientParams) *Client {
}
return &res, nil
},
PayAdminTransactionSwap: func(req PayAdminTransactionSwapRequest) (*AdminSwapResponse, error) {
PayAdminInvoiceSwap: func(req PayAdminInvoiceSwapRequest) (*AdminInvoiceSwapResponse, error) {
auth, err := params.RetrieveAdminAuth()
if err != nil {
return nil, err
}
finalRoute := "/api/admin/swap/invoice/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 := AdminInvoiceSwapResponse{}
err = json.Unmarshal(resBody, &res)
if err != nil {
return nil, err
}
return &res, nil
},
PayAdminTransactionSwap: func(req PayAdminTransactionSwapRequest) (*AdminTxSwapResponse, error) {
auth, err := params.RetrieveAdminAuth()
if err != nil {
return nil, err
@ -1914,7 +2002,7 @@ func NewClient(params ClientParams) *Client {
if result.Status == "ERROR" {
return nil, fmt.Errorf(result.Reason)
}
res := AdminSwapResponse{}
res := AdminTxSwapResponse{}
err = json.Unmarshal(resBody, &res)
if err != nil {
return nil, err
@ -2000,6 +2088,35 @@ func NewClient(params ClientParams) *Client {
}
return nil
},
RefundAdminInvoiceSwap: func(req RefundAdminInvoiceSwapRequest) (*AdminInvoiceSwapResponse, error) {
auth, err := params.RetrieveAdminAuth()
if err != nil {
return nil, err
}
finalRoute := "/api/admin/swap/invoice/refund"
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 := AdminInvoiceSwapResponse{}
err = json.Unmarshal(resBody, &res)
if err != nil {
return nil, err
}
return &res, nil
},
RequestNPubLinkingToken: func(req RequestNPubLinkingTokenRequest) (*RequestNPubLinkingTokenResponse, error) {
auth, err := params.RetrieveAppAuth()
if err != nil {

View file

@ -123,7 +123,10 @@ type AddProductRequest struct {
Name string `json:"name"`
Price_sats int64 `json:"price_sats"`
}
type AdminSwapResponse struct {
type AdminInvoiceSwapResponse struct {
Tx_id string `json:"tx_id"`
}
type AdminTxSwapResponse struct {
Network_fee int64 `json:"network_fee"`
Tx_id string `json:"tx_id"`
}
@ -356,6 +359,35 @@ type HttpCreds struct {
Token string `json:"token"`
Url string `json:"url"`
}
type InvoiceSwapOperation struct {
Failure_reason string `json:"failure_reason"`
Invoice_paid string `json:"invoice_paid"`
Operation_payment *UserOperation `json:"operation_payment"`
Swap_operation_id string `json:"swap_operation_id"`
Tx_id string `json:"tx_id"`
}
type InvoiceSwapQuote struct {
Address string `json:"address"`
Chain_fee_sats int64 `json:"chain_fee_sats"`
Invoice string `json:"invoice"`
Invoice_amount_sats int64 `json:"invoice_amount_sats"`
Service_fee_sats int64 `json:"service_fee_sats"`
Service_url string `json:"service_url"`
Swap_fee_sats int64 `json:"swap_fee_sats"`
Swap_operation_id string `json:"swap_operation_id"`
Transaction_amount_sats int64 `json:"transaction_amount_sats"`
Tx_id string `json:"tx_id"`
}
type InvoiceSwapQuoteList struct {
Quotes []InvoiceSwapQuote `json:"quotes"`
}
type InvoiceSwapRequest struct {
Amount_sats int64 `json:"amount_sats"`
}
type InvoiceSwapsList struct {
Quotes []InvoiceSwapQuote `json:"quotes"`
Swaps []InvoiceSwapOperation `json:"swaps"`
}
type LatestBundleMetricReq struct {
Limit int64 `json:"limit"`
}
@ -559,6 +591,11 @@ type PayAddressResponse struct {
Service_fee int64 `json:"service_fee"`
Txid string `json:"txId"`
}
type PayAdminInvoiceSwapRequest struct {
No_claim bool `json:"no_claim"`
Sat_per_v_byte int64 `json:"sat_per_v_byte"`
Swap_operation_id string `json:"swap_operation_id"`
}
type PayAdminTransactionSwapRequest struct {
Address string `json:"address"`
Swap_operation_id string `json:"swap_operation_id"`
@ -609,6 +646,10 @@ type ProviderDisruption struct {
type ProvidersDisruption struct {
Disruptions []ProviderDisruption `json:"disruptions"`
}
type RefundAdminInvoiceSwapRequest struct {
Sat_per_v_byte int64 `json:"sat_per_v_byte"`
Swap_operation_id string `json:"swap_operation_id"`
}
type RelaysMigration struct {
Relays []string `json:"relays"`
}
@ -665,16 +706,6 @@ type SingleMetricReq struct {
Page int64 `json:"page"`
Request_id int64 `json:"request_id"`
}
type SwapOperation struct {
Address_paid string `json:"address_paid"`
Failure_reason string `json:"failure_reason"`
Operation_payment *UserOperation `json:"operation_payment"`
Swap_operation_id string `json:"swap_operation_id"`
}
type SwapsList struct {
Quotes []TransactionSwapQuote `json:"quotes"`
Swaps []SwapOperation `json:"swaps"`
}
type TransactionSwapQuote struct {
Chain_fee_sats int64 `json:"chain_fee_sats"`
Invoice_amount_sats int64 `json:"invoice_amount_sats"`
@ -690,6 +721,16 @@ type TransactionSwapQuoteList struct {
type TransactionSwapRequest struct {
Transaction_amount_sats int64 `json:"transaction_amount_sats"`
}
type TxSwapOperation struct {
Address_paid string `json:"address_paid"`
Failure_reason string `json:"failure_reason"`
Operation_payment *UserOperation `json:"operation_payment"`
Swap_operation_id string `json:"swap_operation_id"`
}
type TxSwapsList struct {
Quotes []TransactionSwapQuote `json:"quotes"`
Swaps []TxSwapOperation `json:"swaps"`
}
type UpdateChannelPolicyRequest struct {
Policy *ChannelPolicy `json:"policy"`
Update *UpdateChannelPolicyRequest_update `json:"update"`

View file

@ -545,12 +545,12 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => {
callsMetrics.push({ ...opInfo, ...opStats, ...ctx })
}
break
case 'ListSwaps':
if (!methods.ListSwaps) {
throw new Error('method ListSwaps not found' )
case 'ListTxSwaps':
if (!methods.ListTxSwaps) {
throw new Error('method ListTxSwaps not found' )
} else {
opStats.validate = opStats.guard
const res = await methods.ListSwaps({...operation, ctx}); responses.push({ status: 'OK', ...res })
const res = await methods.ListTxSwaps({...operation, ctx}); responses.push({ status: 'OK', ...res })
opStats.handle = process.hrtime.bigint()
callsMetrics.push({ ...opInfo, ...opStats, ...ctx })
}
@ -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.GetAdminInvoiceSwapQuotes) throw new Error('method: GetAdminInvoiceSwapQuotes is not implemented')
app.post('/api/admin/swap/invoice/quote', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'GetAdminInvoiceSwapQuotes', 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.GetAdminInvoiceSwapQuotes) throw new Error('method: GetAdminInvoiceSwapQuotes 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.InvoiceSwapRequestValidate(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.GetAdminInvoiceSwapQuotes({rpcName:'GetAdminInvoiceSwapQuotes', 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.GetAdminTransactionSwapQuotes) throw new Error('method: GetAdminTransactionSwapQuotes is not implemented')
app.post('/api/admin/swap/transaction/quote', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'GetAdminTransactionSwapQuotes', batch: false, nostr: false, batchSize: 0}
@ -1362,7 +1384,7 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => {
} 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.GetTransactionSwapQuotes) throw new Error('method: GetTransactionSwapQuotes is not implemented')
app.post('/api/user/swap/quote', async (req, res) => {
app.post('/api/user/swap/transaction/quote', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'GetTransactionSwapQuotes', 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 = {}
@ -1607,20 +1629,39 @@ 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.ListAdminSwaps) throw new Error('method: ListAdminSwaps is not implemented')
app.post('/api/admin/swap/list', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'ListAdminSwaps', batch: false, nostr: false, batchSize: 0}
if (!opts.allowNotImplementedMethods && !methods.ListAdminInvoiceSwaps) throw new Error('method: ListAdminInvoiceSwaps is not implemented')
app.post('/api/admin/swap/invoice/list', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'ListAdminInvoiceSwaps', 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.ListAdminSwaps) throw new Error('method: ListAdminSwaps is not implemented')
if (!methods.ListAdminInvoiceSwaps) throw new Error('method: ListAdminInvoiceSwaps is not implemented')
const authContext = await opts.AdminAuthGuard(req.headers['authorization'])
authCtx = authContext
stats.guard = process.hrtime.bigint()
stats.validate = stats.guard
const query = req.query
const params = req.params
const response = await methods.ListAdminSwaps({rpcName:'ListAdminSwaps', ctx:authContext })
const response = await methods.ListAdminInvoiceSwaps({rpcName:'ListAdminInvoiceSwaps', ctx:authContext })
stats.handle = process.hrtime.bigint()
res.json({status: 'OK', ...response})
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
})
if (!opts.allowNotImplementedMethods && !methods.ListAdminTxSwaps) throw new Error('method: ListAdminTxSwaps is not implemented')
app.post('/api/admin/swap/transaction/list', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'ListAdminTxSwaps', 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.ListAdminTxSwaps) throw new Error('method: ListAdminTxSwaps is not implemented')
const authContext = await opts.AdminAuthGuard(req.headers['authorization'])
authCtx = authContext
stats.guard = process.hrtime.bigint()
stats.validate = stats.guard
const query = req.query
const params = req.params
const response = await methods.ListAdminTxSwaps({rpcName:'ListAdminTxSwaps', ctx:authContext })
stats.handle = process.hrtime.bigint()
res.json({status: 'OK', ...response})
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
@ -1645,20 +1686,20 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => {
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
})
if (!opts.allowNotImplementedMethods && !methods.ListSwaps) throw new Error('method: ListSwaps is not implemented')
app.post('/api/user/swap/list', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'ListSwaps', batch: false, nostr: false, batchSize: 0}
if (!opts.allowNotImplementedMethods && !methods.ListTxSwaps) throw new Error('method: ListTxSwaps is not implemented')
app.post('/api/user/swap/transaction/list', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'ListTxSwaps', batch: false, nostr: false, batchSize: 0}
const stats: Types.RequestStats = { startMs:req.startTimeMs || 0, start:req.startTime || 0n, parse: process.hrtime.bigint(), guard: 0n, validate: 0n, handle: 0n }
let authCtx: Types.AuthContext = {}
try {
if (!methods.ListSwaps) throw new Error('method: ListSwaps is not implemented')
if (!methods.ListTxSwaps) throw new Error('method: ListTxSwaps is not implemented')
const authContext = await opts.UserAuthGuard(req.headers['authorization'])
authCtx = authContext
stats.guard = process.hrtime.bigint()
stats.validate = stats.guard
const query = req.query
const params = req.params
const response = await methods.ListSwaps({rpcName:'ListSwaps', ctx:authContext })
const response = await methods.ListTxSwaps({rpcName:'ListTxSwaps', ctx:authContext })
stats.handle = process.hrtime.bigint()
res.json({status: 'OK', ...response})
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
@ -1793,6 +1834,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.PayAdminInvoiceSwap) throw new Error('method: PayAdminInvoiceSwap is not implemented')
app.post('/api/admin/swap/invoice/pay', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'PayAdminInvoiceSwap', 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.PayAdminInvoiceSwap) throw new Error('method: PayAdminInvoiceSwap 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.PayAdminInvoiceSwapRequestValidate(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.PayAdminInvoiceSwap({rpcName:'PayAdminInvoiceSwap', 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.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}
@ -1878,6 +1941,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.RefundAdminInvoiceSwap) throw new Error('method: RefundAdminInvoiceSwap is not implemented')
app.post('/api/admin/swap/invoice/refund', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'RefundAdminInvoiceSwap', 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.RefundAdminInvoiceSwap) throw new Error('method: RefundAdminInvoiceSwap 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.RefundAdminInvoiceSwapRequestValidate(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.RefundAdminInvoiceSwap({rpcName:'RefundAdminInvoiceSwap', 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.RequestNPubLinkingToken) throw new Error('method: RequestNPubLinkingToken is not implemented')
app.post('/api/app/user/npub/token', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'RequestNPubLinkingToken', batch: false, nostr: false, batchSize: 0}

View file

@ -273,6 +273,20 @@ export default (params: ClientParams) => ({
}
return { status: 'ERROR', reason: 'invalid response' }
},
GetAdminInvoiceSwapQuotes: async (request: Types.InvoiceSwapRequest): Promise<ResultError | ({ status: 'OK' }& Types.InvoiceSwapQuoteList)> => {
const auth = await params.retrieveAdminAuth()
if (auth === null) throw new Error('retrieveAdminAuth() returned null')
let finalRoute = '/api/admin/swap/invoice/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.InvoiceSwapQuoteListValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
GetAdminTransactionSwapQuotes: async (request: Types.TransactionSwapRequest): Promise<ResultError | ({ status: 'OK' }& Types.TransactionSwapQuoteList)> => {
const auth = await params.retrieveAdminAuth()
if (auth === null) throw new Error('retrieveAdminAuth() returned null')
@ -620,7 +634,7 @@ export default (params: ClientParams) => ({
GetTransactionSwapQuotes: async (request: Types.TransactionSwapRequest): Promise<ResultError | ({ status: 'OK' }& Types.TransactionSwapQuoteList)> => {
const auth = await params.retrieveUserAuth()
if (auth === null) throw new Error('retrieveUserAuth() returned null')
let finalRoute = '/api/user/swap/quote'
let finalRoute = '/api/user/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') {
@ -781,16 +795,30 @@ export default (params: ClientParams) => ({
}
return { status: 'ERROR', reason: 'invalid response' }
},
ListAdminSwaps: async (): Promise<ResultError | ({ status: 'OK' }& Types.SwapsList)> => {
ListAdminInvoiceSwaps: async (): Promise<ResultError | ({ status: 'OK' }& Types.InvoiceSwapsList)> => {
const auth = await params.retrieveAdminAuth()
if (auth === null) throw new Error('retrieveAdminAuth() returned null')
let finalRoute = '/api/admin/swap/list'
let finalRoute = '/api/admin/swap/invoice/list'
const { data } = await axios.post(params.baseUrl + finalRoute, {}, { headers: { 'authorization': auth } })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') {
const result = data
if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.SwapsListValidate(result)
const error = Types.InvoiceSwapsListValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
ListAdminTxSwaps: async (): Promise<ResultError | ({ status: 'OK' }& Types.TxSwapsList)> => {
const auth = await params.retrieveAdminAuth()
if (auth === null) throw new Error('retrieveAdminAuth() returned null')
let finalRoute = '/api/admin/swap/transaction/list'
const { data } = await axios.post(params.baseUrl + finalRoute, {}, { headers: { 'authorization': auth } })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') {
const result = data
if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.TxSwapsListValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
@ -809,16 +837,16 @@ export default (params: ClientParams) => ({
}
return { status: 'ERROR', reason: 'invalid response' }
},
ListSwaps: async (): Promise<ResultError | ({ status: 'OK' }& Types.SwapsList)> => {
ListTxSwaps: async (): Promise<ResultError | ({ status: 'OK' }& Types.TxSwapsList)> => {
const auth = await params.retrieveUserAuth()
if (auth === null) throw new Error('retrieveUserAuth() returned null')
let finalRoute = '/api/user/swap/list'
let finalRoute = '/api/user/swap/transaction/list'
const { data } = await axios.post(params.baseUrl + finalRoute, {}, { headers: { 'authorization': auth } })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') {
const result = data
if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.SwapsListValidate(result)
const error = Types.TxSwapsListValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
@ -909,7 +937,21 @@ export default (params: ClientParams) => ({
}
return { status: 'ERROR', reason: 'invalid response' }
},
PayAdminTransactionSwap: async (request: Types.PayAdminTransactionSwapRequest): Promise<ResultError | ({ status: 'OK' }& Types.AdminSwapResponse)> => {
PayAdminInvoiceSwap: async (request: Types.PayAdminInvoiceSwapRequest): Promise<ResultError | ({ status: 'OK' }& Types.AdminInvoiceSwapResponse)> => {
const auth = await params.retrieveAdminAuth()
if (auth === null) throw new Error('retrieveAdminAuth() returned null')
let finalRoute = '/api/admin/swap/invoice/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.AdminInvoiceSwapResponseValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
PayAdminTransactionSwap: async (request: Types.PayAdminTransactionSwapRequest): Promise<ResultError | ({ status: 'OK' }& Types.AdminTxSwapResponse)> => {
const auth = await params.retrieveAdminAuth()
if (auth === null) throw new Error('retrieveAdminAuth() returned null')
let finalRoute = '/api/admin/swap/transaction/pay'
@ -918,7 +960,7 @@ export default (params: ClientParams) => ({
if (data.status === 'OK') {
const result = data
if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.AdminSwapResponseValidate(result)
const error = Types.AdminTxSwapResponseValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
@ -962,6 +1004,20 @@ export default (params: ClientParams) => ({
}
return { status: 'ERROR', reason: 'invalid response' }
},
RefundAdminInvoiceSwap: async (request: Types.RefundAdminInvoiceSwapRequest): Promise<ResultError | ({ status: 'OK' }& Types.AdminInvoiceSwapResponse)> => {
const auth = await params.retrieveAdminAuth()
if (auth === null) throw new Error('retrieveAdminAuth() returned null')
let finalRoute = '/api/admin/swap/invoice/refund'
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.AdminInvoiceSwapResponseValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
RequestNPubLinkingToken: async (request: Types.RequestNPubLinkingTokenRequest): Promise<ResultError | ({ status: 'OK' }& Types.RequestNPubLinkingTokenResponse)> => {
const auth = await params.retrieveAppAuth()
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' }
},
GetAdminInvoiceSwapQuotes: async (request: Types.InvoiceSwapRequest): Promise<ResultError | ({ status: 'OK' }& Types.InvoiceSwapQuoteList)> => {
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:'GetAdminInvoiceSwapQuotes',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.InvoiceSwapQuoteListValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
GetAdminTransactionSwapQuotes: async (request: Types.TransactionSwapRequest): Promise<ResultError | ({ status: 'OK' }& Types.TransactionSwapQuoteList)> => {
const auth = await params.retrieveNostrAdminAuth()
if (auth === null) throw new Error('retrieveNostrAdminAuth() returned null')
@ -666,16 +681,30 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ
}
return { status: 'ERROR', reason: 'invalid response' }
},
ListAdminSwaps: async (): Promise<ResultError | ({ status: 'OK' }& Types.SwapsList)> => {
ListAdminInvoiceSwaps: async (): Promise<ResultError | ({ status: 'OK' }& Types.InvoiceSwapsList)> => {
const auth = await params.retrieveNostrAdminAuth()
if (auth === null) throw new Error('retrieveNostrAdminAuth() returned null')
const nostrRequest: NostrRequest = {}
const data = await send(params.pubDestination, {rpcName:'ListAdminSwaps',authIdentifier:auth, ...nostrRequest })
const data = await send(params.pubDestination, {rpcName:'ListAdminInvoiceSwaps',authIdentifier:auth, ...nostrRequest })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') {
const result = data
if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.SwapsListValidate(result)
const error = Types.InvoiceSwapsListValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
ListAdminTxSwaps: async (): Promise<ResultError | ({ status: 'OK' }& Types.TxSwapsList)> => {
const auth = await params.retrieveNostrAdminAuth()
if (auth === null) throw new Error('retrieveNostrAdminAuth() returned null')
const nostrRequest: NostrRequest = {}
const data = await send(params.pubDestination, {rpcName:'ListAdminTxSwaps',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.TxSwapsListValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
@ -694,16 +723,16 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ
}
return { status: 'ERROR', reason: 'invalid response' }
},
ListSwaps: async (): Promise<ResultError | ({ status: 'OK' }& Types.SwapsList)> => {
ListTxSwaps: async (): Promise<ResultError | ({ status: 'OK' }& Types.TxSwapsList)> => {
const auth = await params.retrieveNostrUserAuth()
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
const nostrRequest: NostrRequest = {}
const data = await send(params.pubDestination, {rpcName:'ListSwaps',authIdentifier:auth, ...nostrRequest })
const data = await send(params.pubDestination, {rpcName:'ListTxSwaps',authIdentifier:auth, ...nostrRequest })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') {
const result = data
if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.SwapsListValidate(result)
const error = Types.TxSwapsListValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
@ -798,7 +827,22 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ
}
return { status: 'ERROR', reason: 'invalid response' }
},
PayAdminTransactionSwap: async (request: Types.PayAdminTransactionSwapRequest): Promise<ResultError | ({ status: 'OK' }& Types.AdminSwapResponse)> => {
PayAdminInvoiceSwap: async (request: Types.PayAdminInvoiceSwapRequest): Promise<ResultError | ({ status: 'OK' }& Types.AdminInvoiceSwapResponse)> => {
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:'PayAdminInvoiceSwap',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.AdminInvoiceSwapResponseValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
PayAdminTransactionSwap: async (request: Types.PayAdminTransactionSwapRequest): Promise<ResultError | ({ status: 'OK' }& Types.AdminTxSwapResponse)> => {
const auth = await params.retrieveNostrAdminAuth()
if (auth === null) throw new Error('retrieveNostrAdminAuth() returned null')
const nostrRequest: NostrRequest = {}
@ -808,7 +852,7 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ
if (data.status === 'OK') {
const result = data
if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.AdminSwapResponseValidate(result)
const error = Types.AdminTxSwapResponseValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
@ -839,6 +883,21 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ
}
return { status: 'ERROR', reason: 'invalid response' }
},
RefundAdminInvoiceSwap: async (request: Types.RefundAdminInvoiceSwapRequest): Promise<ResultError | ({ status: 'OK' }& Types.AdminInvoiceSwapResponse)> => {
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:'RefundAdminInvoiceSwap',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.AdminInvoiceSwapResponseValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
ResetDebit: async (request: Types.DebitOperation): Promise<ResultError | ({ status: 'OK' })> => {
const auth = await params.retrieveNostrUserAuth()
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')

View file

@ -427,12 +427,12 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => {
callsMetrics.push({ ...opInfo, ...opStats, ...ctx })
}
break
case 'ListSwaps':
if (!methods.ListSwaps) {
throw new Error('method not defined: ListSwaps')
case 'ListTxSwaps':
if (!methods.ListTxSwaps) {
throw new Error('method not defined: ListTxSwaps')
} else {
opStats.validate = opStats.guard
const res = await methods.ListSwaps({...operation, ctx}); responses.push({ status: 'OK', ...res })
const res = await methods.ListTxSwaps({...operation, ctx}); responses.push({ status: 'OK', ...res })
opStats.handle = process.hrtime.bigint()
callsMetrics.push({ ...opInfo, ...opStats, ...ctx })
}
@ -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 'GetAdminInvoiceSwapQuotes':
try {
if (!methods.GetAdminInvoiceSwapQuotes) throw new Error('method: GetAdminInvoiceSwapQuotes 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.InvoiceSwapRequestValidate(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.GetAdminInvoiceSwapQuotes({rpcName:'GetAdminInvoiceSwapQuotes', 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 'GetAdminTransactionSwapQuotes':
try {
if (!methods.GetAdminTransactionSwapQuotes) throw new Error('method: GetAdminTransactionSwapQuotes is not implemented')
@ -1122,14 +1138,27 @@ 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 'ListAdminSwaps':
case 'ListAdminInvoiceSwaps':
try {
if (!methods.ListAdminSwaps) throw new Error('method: ListAdminSwaps is not implemented')
if (!methods.ListAdminInvoiceSwaps) throw new Error('method: ListAdminInvoiceSwaps is not implemented')
const authContext = await opts.NostrAdminAuthGuard(req.appId, req.authIdentifier)
stats.guard = process.hrtime.bigint()
authCtx = authContext
stats.validate = stats.guard
const response = await methods.ListAdminSwaps({rpcName:'ListAdminSwaps', ctx:authContext })
const response = await methods.ListAdminInvoiceSwaps({rpcName:'ListAdminInvoiceSwaps', ctx:authContext })
stats.handle = process.hrtime.bigint()
res({status: 'OK', ...response})
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
}catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
break
case 'ListAdminTxSwaps':
try {
if (!methods.ListAdminTxSwaps) throw new Error('method: ListAdminTxSwaps is not implemented')
const authContext = await opts.NostrAdminAuthGuard(req.appId, req.authIdentifier)
stats.guard = process.hrtime.bigint()
authCtx = authContext
stats.validate = stats.guard
const response = await methods.ListAdminTxSwaps({rpcName:'ListAdminTxSwaps', ctx:authContext })
stats.handle = process.hrtime.bigint()
res({status: 'OK', ...response})
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
@ -1148,14 +1177,14 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => {
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
}catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
break
case 'ListSwaps':
case 'ListTxSwaps':
try {
if (!methods.ListSwaps) throw new Error('method: ListSwaps is not implemented')
if (!methods.ListTxSwaps) throw new Error('method: ListTxSwaps is not implemented')
const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier)
stats.guard = process.hrtime.bigint()
authCtx = authContext
stats.validate = stats.guard
const response = await methods.ListSwaps({rpcName:'ListSwaps', ctx:authContext })
const response = await methods.ListTxSwaps({rpcName:'ListTxSwaps', ctx:authContext })
stats.handle = process.hrtime.bigint()
res({status: 'OK', ...response})
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
@ -1254,6 +1283,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 'PayAdminInvoiceSwap':
try {
if (!methods.PayAdminInvoiceSwap) throw new Error('method: PayAdminInvoiceSwap 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.PayAdminInvoiceSwapRequestValidate(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.PayAdminInvoiceSwap({rpcName:'PayAdminInvoiceSwap', 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 'PayAdminTransactionSwap':
try {
if (!methods.PayAdminTransactionSwap) throw new Error('method: PayAdminTransactionSwap is not implemented')
@ -1299,6 +1344,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 'RefundAdminInvoiceSwap':
try {
if (!methods.RefundAdminInvoiceSwap) throw new Error('method: RefundAdminInvoiceSwap 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.RefundAdminInvoiceSwapRequestValidate(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.RefundAdminInvoiceSwap({rpcName:'RefundAdminInvoiceSwap', 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 'ResetDebit':
try {
if (!methods.ResetDebit) throw new Error('method: ResetDebit is not implemented')

View file

@ -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 | GetAdminTransactionSwapQuotes_Input | GetInviteLinkState_Input | GetSeed_Input | ListAdminSwaps_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 | GetAdminTransactionSwapQuotes_Output | GetInviteLinkState_Output | GetSeed_Output | ListAdminSwaps_Output | ListChannels_Output | LndGetInfo_Output | OpenChannel_Output | PayAdminTransactionSwap_Output | UpdateChannelPolicy_Output
export type AdminMethodInputs = AddApp_Input | AddPeer_Input | AuthApp_Input | BanUser_Input | CloseChannel_Input | CreateOneTimeInviteLink_Input | GetAdminInvoiceSwapQuotes_Input | GetAdminTransactionSwapQuotes_Input | GetInviteLinkState_Input | GetSeed_Input | ListAdminInvoiceSwaps_Input | ListAdminTxSwaps_Input | ListChannels_Input | LndGetInfo_Input | OpenChannel_Input | PayAdminInvoiceSwap_Input | PayAdminTransactionSwap_Input | RefundAdminInvoiceSwap_Input | UpdateChannelPolicy_Input
export type AdminMethodOutputs = AddApp_Output | AddPeer_Output | AuthApp_Output | BanUser_Output | CloseChannel_Output | CreateOneTimeInviteLink_Output | GetAdminInvoiceSwapQuotes_Output | GetAdminTransactionSwapQuotes_Output | GetInviteLinkState_Output | GetSeed_Output | ListAdminInvoiceSwaps_Output | ListAdminTxSwaps_Output | ListChannels_Output | LndGetInfo_Output | OpenChannel_Output | PayAdminInvoiceSwap_Output | PayAdminTransactionSwap_Output | RefundAdminInvoiceSwap_Output | UpdateChannelPolicy_Output
export type AppContext = {
app_id: string
}
@ -35,8 +35,8 @@ export type UserContext = {
app_user_id: string
user_id: string
}
export type UserMethodInputs = AddProduct_Input | AddUserOffer_Input | AuthorizeManage_Input | BanDebit_Input | DecodeInvoice_Input | DeleteUserOffer_Input | EditDebit_Input | EnrollAdminToken_Input | EnrollMessagingToken_Input | GetDebitAuthorizations_Input | GetHttpCreds_Input | GetLNURLChannelLink_Input | GetLnurlPayLink_Input | GetLnurlWithdrawLink_Input | GetManageAuthorizations_Input | GetPaymentState_Input | GetTransactionSwapQuotes_Input | GetUserInfo_Input | GetUserOffer_Input | GetUserOfferInvoices_Input | GetUserOffers_Input | GetUserOperations_Input | ListSwaps_Input | NewAddress_Input | NewInvoice_Input | NewProductInvoice_Input | PayAddress_Input | PayInvoice_Input | ResetDebit_Input | ResetManage_Input | RespondToDebit_Input | UpdateCallbackUrl_Input | UpdateUserOffer_Input | UserHealth_Input
export type UserMethodOutputs = AddProduct_Output | AddUserOffer_Output | AuthorizeManage_Output | BanDebit_Output | DecodeInvoice_Output | DeleteUserOffer_Output | EditDebit_Output | EnrollAdminToken_Output | EnrollMessagingToken_Output | GetDebitAuthorizations_Output | GetHttpCreds_Output | GetLNURLChannelLink_Output | GetLnurlPayLink_Output | GetLnurlWithdrawLink_Output | GetManageAuthorizations_Output | GetPaymentState_Output | GetTransactionSwapQuotes_Output | GetUserInfo_Output | GetUserOffer_Output | GetUserOfferInvoices_Output | GetUserOffers_Output | GetUserOperations_Output | ListSwaps_Output | NewAddress_Output | NewInvoice_Output | NewProductInvoice_Output | PayAddress_Output | PayInvoice_Output | ResetDebit_Output | ResetManage_Output | RespondToDebit_Output | UpdateCallbackUrl_Output | UpdateUserOffer_Output | UserHealth_Output
export type UserMethodInputs = AddProduct_Input | AddUserOffer_Input | AuthorizeManage_Input | BanDebit_Input | DecodeInvoice_Input | DeleteUserOffer_Input | EditDebit_Input | EnrollAdminToken_Input | EnrollMessagingToken_Input | GetDebitAuthorizations_Input | GetHttpCreds_Input | GetLNURLChannelLink_Input | GetLnurlPayLink_Input | GetLnurlWithdrawLink_Input | GetManageAuthorizations_Input | GetPaymentState_Input | GetTransactionSwapQuotes_Input | GetUserInfo_Input | GetUserOffer_Input | GetUserOfferInvoices_Input | GetUserOffers_Input | GetUserOperations_Input | ListTxSwaps_Input | NewAddress_Input | NewInvoice_Input | NewProductInvoice_Input | PayAddress_Input | PayInvoice_Input | ResetDebit_Input | ResetManage_Input | RespondToDebit_Input | UpdateCallbackUrl_Input | UpdateUserOffer_Input | UserHealth_Input
export type UserMethodOutputs = AddProduct_Output | AddUserOffer_Output | AuthorizeManage_Output | BanDebit_Output | DecodeInvoice_Output | DeleteUserOffer_Output | EditDebit_Output | EnrollAdminToken_Output | EnrollMessagingToken_Output | GetDebitAuthorizations_Output | GetHttpCreds_Output | GetLNURLChannelLink_Output | GetLnurlPayLink_Output | GetLnurlWithdrawLink_Output | GetManageAuthorizations_Output | GetPaymentState_Output | GetTransactionSwapQuotes_Output | GetUserInfo_Output | GetUserOffer_Output | GetUserOfferInvoices_Output | GetUserOffers_Output | GetUserOperations_Output | ListTxSwaps_Output | NewAddress_Output | NewInvoice_Output | NewProductInvoice_Output | PayAddress_Output | PayInvoice_Output | ResetDebit_Output | ResetManage_Output | RespondToDebit_Output | UpdateCallbackUrl_Output | UpdateUserOffer_Output | UserHealth_Output
export type AuthContext = AdminContext | AppContext | GuestContext | GuestWithPubContext | MetricsContext | UserContext
export type AddApp_Input = {rpcName:'AddApp', req: AddAppRequest}
@ -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 GetAdminInvoiceSwapQuotes_Input = {rpcName:'GetAdminInvoiceSwapQuotes', req: InvoiceSwapRequest}
export type GetAdminInvoiceSwapQuotes_Output = ResultError | ({ status: 'OK' } & InvoiceSwapQuoteList)
export type GetAdminTransactionSwapQuotes_Input = {rpcName:'GetAdminTransactionSwapQuotes', req: TransactionSwapRequest}
export type GetAdminTransactionSwapQuotes_Output = ResultError | ({ status: 'OK' } & TransactionSwapQuoteList)
@ -238,14 +241,17 @@ export type Health_Output = ResultError | { status: 'OK' }
export type LinkNPubThroughToken_Input = {rpcName:'LinkNPubThroughToken', req: LinkNPubThroughTokenRequest}
export type LinkNPubThroughToken_Output = ResultError | { status: 'OK' }
export type ListAdminSwaps_Input = {rpcName:'ListAdminSwaps'}
export type ListAdminSwaps_Output = ResultError | ({ status: 'OK' } & SwapsList)
export type ListAdminInvoiceSwaps_Input = {rpcName:'ListAdminInvoiceSwaps'}
export type ListAdminInvoiceSwaps_Output = ResultError | ({ status: 'OK' } & InvoiceSwapsList)
export type ListAdminTxSwaps_Input = {rpcName:'ListAdminTxSwaps'}
export type ListAdminTxSwaps_Output = ResultError | ({ status: 'OK' } & TxSwapsList)
export type ListChannels_Input = {rpcName:'ListChannels'}
export type ListChannels_Output = ResultError | ({ status: 'OK' } & LndChannels)
export type ListSwaps_Input = {rpcName:'ListSwaps'}
export type ListSwaps_Output = ResultError | ({ status: 'OK' } & SwapsList)
export type ListTxSwaps_Input = {rpcName:'ListTxSwaps'}
export type ListTxSwaps_Output = ResultError | ({ status: 'OK' } & TxSwapsList)
export type LndGetInfo_Input = {rpcName:'LndGetInfo', req: LndGetInfoRequest}
export type LndGetInfo_Output = ResultError | ({ status: 'OK' } & LndGetInfoResponse)
@ -268,8 +274,11 @@ 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 PayAdminInvoiceSwap_Input = {rpcName:'PayAdminInvoiceSwap', req: PayAdminInvoiceSwapRequest}
export type PayAdminInvoiceSwap_Output = ResultError | ({ status: 'OK' } & AdminInvoiceSwapResponse)
export type PayAdminTransactionSwap_Input = {rpcName:'PayAdminTransactionSwap', req: PayAdminTransactionSwapRequest}
export type PayAdminTransactionSwap_Output = ResultError | ({ status: 'OK' } & AdminSwapResponse)
export type PayAdminTransactionSwap_Output = ResultError | ({ status: 'OK' } & AdminTxSwapResponse)
export type PayAppUserInvoice_Input = {rpcName:'PayAppUserInvoice', req: PayAppUserInvoiceRequest}
export type PayAppUserInvoice_Output = ResultError | ({ status: 'OK' } & PayInvoiceResponse)
@ -280,6 +289,9 @@ export type PayInvoice_Output = ResultError | ({ status: 'OK' } & PayInvoiceResp
export type PingSubProcesses_Input = {rpcName:'PingSubProcesses'}
export type PingSubProcesses_Output = ResultError | { status: 'OK' }
export type RefundAdminInvoiceSwap_Input = {rpcName:'RefundAdminInvoiceSwap', req: RefundAdminInvoiceSwapRequest}
export type RefundAdminInvoiceSwap_Output = ResultError | ({ status: 'OK' } & AdminInvoiceSwapResponse)
export type RequestNPubLinkingToken_Input = {rpcName:'RequestNPubLinkingToken', req: RequestNPubLinkingTokenRequest}
export type RequestNPubLinkingToken_Output = ResultError | ({ status: 'OK' } & RequestNPubLinkingTokenResponse)
@ -357,6 +369,7 @@ export type ServerMethods = {
EncryptionExchange?: (req: EncryptionExchange_Input & {ctx: GuestContext }) => Promise<void>
EnrollAdminToken?: (req: EnrollAdminToken_Input & {ctx: UserContext }) => Promise<void>
EnrollMessagingToken?: (req: EnrollMessagingToken_Input & {ctx: UserContext }) => Promise<void>
GetAdminInvoiceSwapQuotes?: (req: GetAdminInvoiceSwapQuotes_Input & {ctx: AdminContext }) => Promise<InvoiceSwapQuoteList>
GetAdminTransactionSwapQuotes?: (req: GetAdminTransactionSwapQuotes_Input & {ctx: AdminContext }) => Promise<TransactionSwapQuoteList>
GetApp?: (req: GetApp_Input & {ctx: AppContext }) => Promise<Application>
GetAppUser?: (req: GetAppUser_Input & {ctx: AppContext }) => Promise<AppUser>
@ -397,19 +410,22 @@ export type ServerMethods = {
HandleLnurlWithdraw?: (req: HandleLnurlWithdraw_Input & {ctx: GuestContext }) => Promise<void>
Health?: (req: Health_Input & {ctx: GuestContext }) => Promise<void>
LinkNPubThroughToken?: (req: LinkNPubThroughToken_Input & {ctx: GuestWithPubContext }) => Promise<void>
ListAdminSwaps?: (req: ListAdminSwaps_Input & {ctx: AdminContext }) => Promise<SwapsList>
ListAdminInvoiceSwaps?: (req: ListAdminInvoiceSwaps_Input & {ctx: AdminContext }) => Promise<InvoiceSwapsList>
ListAdminTxSwaps?: (req: ListAdminTxSwaps_Input & {ctx: AdminContext }) => Promise<TxSwapsList>
ListChannels?: (req: ListChannels_Input & {ctx: AdminContext }) => Promise<LndChannels>
ListSwaps?: (req: ListSwaps_Input & {ctx: UserContext }) => Promise<SwapsList>
ListTxSwaps?: (req: ListTxSwaps_Input & {ctx: UserContext }) => Promise<TxSwapsList>
LndGetInfo?: (req: LndGetInfo_Input & {ctx: AdminContext }) => Promise<LndGetInfoResponse>
NewAddress?: (req: NewAddress_Input & {ctx: UserContext }) => Promise<NewAddressResponse>
NewInvoice?: (req: NewInvoice_Input & {ctx: UserContext }) => Promise<NewInvoiceResponse>
NewProductInvoice?: (req: NewProductInvoice_Input & {ctx: UserContext }) => Promise<NewInvoiceResponse>
OpenChannel?: (req: OpenChannel_Input & {ctx: AdminContext }) => Promise<OpenChannelResponse>
PayAddress?: (req: PayAddress_Input & {ctx: UserContext }) => Promise<PayAddressResponse>
PayAdminTransactionSwap?: (req: PayAdminTransactionSwap_Input & {ctx: AdminContext }) => Promise<AdminSwapResponse>
PayAdminInvoiceSwap?: (req: PayAdminInvoiceSwap_Input & {ctx: AdminContext }) => Promise<AdminInvoiceSwapResponse>
PayAdminTransactionSwap?: (req: PayAdminTransactionSwap_Input & {ctx: AdminContext }) => Promise<AdminTxSwapResponse>
PayAppUserInvoice?: (req: PayAppUserInvoice_Input & {ctx: AppContext }) => Promise<PayInvoiceResponse>
PayInvoice?: (req: PayInvoice_Input & {ctx: UserContext }) => Promise<PayInvoiceResponse>
PingSubProcesses?: (req: PingSubProcesses_Input & {ctx: MetricsContext }) => Promise<void>
RefundAdminInvoiceSwap?: (req: RefundAdminInvoiceSwap_Input & {ctx: AdminContext }) => Promise<AdminInvoiceSwapResponse>
RequestNPubLinkingToken?: (req: RequestNPubLinkingToken_Input & {ctx: AppContext }) => Promise<RequestNPubLinkingTokenResponse>
ResetDebit?: (req: ResetDebit_Input & {ctx: UserContext }) => Promise<void>
ResetManage?: (req: ResetManage_Input & {ctx: UserContext }) => Promise<void>
@ -671,17 +687,35 @@ export const AddProductRequestValidate = (o?: AddProductRequest, opts: AddProduc
return null
}
export type AdminSwapResponse = {
export type AdminInvoiceSwapResponse = {
tx_id: string
}
export const AdminInvoiceSwapResponseOptionalFields: [] = []
export type AdminInvoiceSwapResponseOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
tx_id_CustomCheck?: (v: string) => boolean
}
export const AdminInvoiceSwapResponseValidate = (o?: AdminInvoiceSwapResponse, opts: AdminInvoiceSwapResponseOptions = {}, path: string = 'AdminInvoiceSwapResponse::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.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 AdminTxSwapResponse = {
network_fee: number
tx_id: string
}
export const AdminSwapResponseOptionalFields: [] = []
export type AdminSwapResponseOptions = OptionsBaseMessage & {
export const AdminTxSwapResponseOptionalFields: [] = []
export type AdminTxSwapResponseOptions = 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 => {
export const AdminTxSwapResponseValidate = (o?: AdminTxSwapResponse, opts: AdminTxSwapResponseOptions = {}, path: string = 'AdminTxSwapResponse::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')
@ -2088,6 +2122,185 @@ export const HttpCredsValidate = (o?: HttpCreds, opts: HttpCredsOptions = {}, pa
return null
}
export type InvoiceSwapOperation = {
failure_reason?: string
invoice_paid: string
operation_payment?: UserOperation
swap_operation_id: string
tx_id: string
}
export type InvoiceSwapOperationOptionalField = 'failure_reason' | 'operation_payment'
export const InvoiceSwapOperationOptionalFields: InvoiceSwapOperationOptionalField[] = ['failure_reason', 'operation_payment']
export type InvoiceSwapOperationOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: InvoiceSwapOperationOptionalField[]
failure_reason_CustomCheck?: (v?: string) => boolean
invoice_paid_CustomCheck?: (v: string) => boolean
operation_payment_Options?: UserOperationOptions
swap_operation_id_CustomCheck?: (v: string) => boolean
tx_id_CustomCheck?: (v: string) => boolean
}
export const InvoiceSwapOperationValidate = (o?: InvoiceSwapOperation, opts: InvoiceSwapOperationOptions = {}, path: string = 'InvoiceSwapOperation::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 ((o.failure_reason || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('failure_reason')) && typeof o.failure_reason !== 'string') return new Error(`${path}.failure_reason: is not a string`)
if (opts.failure_reason_CustomCheck && !opts.failure_reason_CustomCheck(o.failure_reason)) return new Error(`${path}.failure_reason: custom check failed`)
if (typeof o.invoice_paid !== 'string') return new Error(`${path}.invoice_paid: is not a string`)
if (opts.invoice_paid_CustomCheck && !opts.invoice_paid_CustomCheck(o.invoice_paid)) return new Error(`${path}.invoice_paid: custom check failed`)
if (typeof o.operation_payment === 'object' || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('operation_payment')) {
const operation_paymentErr = UserOperationValidate(o.operation_payment, opts.operation_payment_Options, `${path}.operation_payment`)
if (operation_paymentErr !== null) return operation_paymentErr
}
if (typeof o.swap_operation_id !== 'string') return new Error(`${path}.swap_operation_id: is not a string`)
if (opts.swap_operation_id_CustomCheck && !opts.swap_operation_id_CustomCheck(o.swap_operation_id)) return new Error(`${path}.swap_operation_id: custom check failed`)
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 InvoiceSwapQuote = {
address: string
chain_fee_sats: number
invoice: string
invoice_amount_sats: number
service_fee_sats: number
service_url: string
swap_fee_sats: number
swap_operation_id: string
transaction_amount_sats: number
tx_id: string
}
export const InvoiceSwapQuoteOptionalFields: [] = []
export type InvoiceSwapQuoteOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
address_CustomCheck?: (v: string) => boolean
chain_fee_sats_CustomCheck?: (v: number) => boolean
invoice_CustomCheck?: (v: string) => boolean
invoice_amount_sats_CustomCheck?: (v: number) => boolean
service_fee_sats_CustomCheck?: (v: number) => boolean
service_url_CustomCheck?: (v: string) => boolean
swap_fee_sats_CustomCheck?: (v: number) => boolean
swap_operation_id_CustomCheck?: (v: string) => boolean
transaction_amount_sats_CustomCheck?: (v: number) => boolean
tx_id_CustomCheck?: (v: string) => boolean
}
export const InvoiceSwapQuoteValidate = (o?: InvoiceSwapQuote, opts: InvoiceSwapQuoteOptions = {}, path: string = 'InvoiceSwapQuote::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.chain_fee_sats !== 'number') return new Error(`${path}.chain_fee_sats: is not a number`)
if (opts.chain_fee_sats_CustomCheck && !opts.chain_fee_sats_CustomCheck(o.chain_fee_sats)) return new Error(`${path}.chain_fee_sats: custom check failed`)
if (typeof o.invoice !== 'string') return new Error(`${path}.invoice: is not a string`)
if (opts.invoice_CustomCheck && !opts.invoice_CustomCheck(o.invoice)) return new Error(`${path}.invoice: custom check failed`)
if (typeof o.invoice_amount_sats !== 'number') return new Error(`${path}.invoice_amount_sats: is not a number`)
if (opts.invoice_amount_sats_CustomCheck && !opts.invoice_amount_sats_CustomCheck(o.invoice_amount_sats)) return new Error(`${path}.invoice_amount_sats: custom check failed`)
if (typeof o.service_fee_sats !== 'number') return new Error(`${path}.service_fee_sats: is not a number`)
if (opts.service_fee_sats_CustomCheck && !opts.service_fee_sats_CustomCheck(o.service_fee_sats)) return new Error(`${path}.service_fee_sats: custom check failed`)
if (typeof o.service_url !== 'string') return new Error(`${path}.service_url: is not a string`)
if (opts.service_url_CustomCheck && !opts.service_url_CustomCheck(o.service_url)) return new Error(`${path}.service_url: custom check failed`)
if (typeof o.swap_fee_sats !== 'number') return new Error(`${path}.swap_fee_sats: is not a number`)
if (opts.swap_fee_sats_CustomCheck && !opts.swap_fee_sats_CustomCheck(o.swap_fee_sats)) return new Error(`${path}.swap_fee_sats: custom check failed`)
if (typeof o.swap_operation_id !== 'string') return new Error(`${path}.swap_operation_id: is not a string`)
if (opts.swap_operation_id_CustomCheck && !opts.swap_operation_id_CustomCheck(o.swap_operation_id)) return new Error(`${path}.swap_operation_id: custom check failed`)
if (typeof o.transaction_amount_sats !== 'number') return new Error(`${path}.transaction_amount_sats: is not a number`)
if (opts.transaction_amount_sats_CustomCheck && !opts.transaction_amount_sats_CustomCheck(o.transaction_amount_sats)) return new Error(`${path}.transaction_amount_sats: custom check failed`)
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 InvoiceSwapQuoteList = {
quotes: InvoiceSwapQuote[]
}
export const InvoiceSwapQuoteListOptionalFields: [] = []
export type InvoiceSwapQuoteListOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
quotes_ItemOptions?: InvoiceSwapQuoteOptions
quotes_CustomCheck?: (v: InvoiceSwapQuote[]) => boolean
}
export const InvoiceSwapQuoteListValidate = (o?: InvoiceSwapQuoteList, opts: InvoiceSwapQuoteListOptions = {}, path: string = 'InvoiceSwapQuoteList::root.'): Error | null => {
if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message')
if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null')
if (!Array.isArray(o.quotes)) return new Error(`${path}.quotes: is not an array`)
for (let index = 0; index < o.quotes.length; index++) {
const quotesErr = InvoiceSwapQuoteValidate(o.quotes[index], opts.quotes_ItemOptions, `${path}.quotes[${index}]`)
if (quotesErr !== null) return quotesErr
}
if (opts.quotes_CustomCheck && !opts.quotes_CustomCheck(o.quotes)) return new Error(`${path}.quotes: custom check failed`)
return null
}
export type InvoiceSwapRequest = {
amount_sats: number
}
export const InvoiceSwapRequestOptionalFields: [] = []
export type InvoiceSwapRequestOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
amount_sats_CustomCheck?: (v: number) => boolean
}
export const InvoiceSwapRequestValidate = (o?: InvoiceSwapRequest, opts: InvoiceSwapRequestOptions = {}, path: string = 'InvoiceSwapRequest::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.amount_sats !== 'number') return new Error(`${path}.amount_sats: is not a number`)
if (opts.amount_sats_CustomCheck && !opts.amount_sats_CustomCheck(o.amount_sats)) return new Error(`${path}.amount_sats: custom check failed`)
return null
}
export type InvoiceSwapsList = {
quotes: InvoiceSwapQuote[]
swaps: InvoiceSwapOperation[]
}
export const InvoiceSwapsListOptionalFields: [] = []
export type InvoiceSwapsListOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
quotes_ItemOptions?: InvoiceSwapQuoteOptions
quotes_CustomCheck?: (v: InvoiceSwapQuote[]) => boolean
swaps_ItemOptions?: InvoiceSwapOperationOptions
swaps_CustomCheck?: (v: InvoiceSwapOperation[]) => boolean
}
export const InvoiceSwapsListValidate = (o?: InvoiceSwapsList, opts: InvoiceSwapsListOptions = {}, path: string = 'InvoiceSwapsList::root.'): Error | null => {
if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message')
if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null')
if (!Array.isArray(o.quotes)) return new Error(`${path}.quotes: is not an array`)
for (let index = 0; index < o.quotes.length; index++) {
const quotesErr = InvoiceSwapQuoteValidate(o.quotes[index], opts.quotes_ItemOptions, `${path}.quotes[${index}]`)
if (quotesErr !== null) return quotesErr
}
if (opts.quotes_CustomCheck && !opts.quotes_CustomCheck(o.quotes)) return new Error(`${path}.quotes: custom check failed`)
if (!Array.isArray(o.swaps)) return new Error(`${path}.swaps: is not an array`)
for (let index = 0; index < o.swaps.length; index++) {
const swapsErr = InvoiceSwapOperationValidate(o.swaps[index], opts.swaps_ItemOptions, `${path}.swaps[${index}]`)
if (swapsErr !== null) return swapsErr
}
if (opts.swaps_CustomCheck && !opts.swaps_CustomCheck(o.swaps)) return new Error(`${path}.swaps: custom check failed`)
return null
}
export type LatestBundleMetricReq = {
limit?: number
}
@ -3308,6 +3521,35 @@ export const PayAddressResponseValidate = (o?: PayAddressResponse, opts: PayAddr
return null
}
export type PayAdminInvoiceSwapRequest = {
no_claim?: boolean
sat_per_v_byte: number
swap_operation_id: string
}
export type PayAdminInvoiceSwapRequestOptionalField = 'no_claim'
export const PayAdminInvoiceSwapRequestOptionalFields: PayAdminInvoiceSwapRequestOptionalField[] = ['no_claim']
export type PayAdminInvoiceSwapRequestOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: PayAdminInvoiceSwapRequestOptionalField[]
no_claim_CustomCheck?: (v?: boolean) => boolean
sat_per_v_byte_CustomCheck?: (v: number) => boolean
swap_operation_id_CustomCheck?: (v: string) => boolean
}
export const PayAdminInvoiceSwapRequestValidate = (o?: PayAdminInvoiceSwapRequest, opts: PayAdminInvoiceSwapRequestOptions = {}, path: string = 'PayAdminInvoiceSwapRequest::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 ((o.no_claim || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('no_claim')) && typeof o.no_claim !== 'boolean') return new Error(`${path}.no_claim: is not a boolean`)
if (opts.no_claim_CustomCheck && !opts.no_claim_CustomCheck(o.no_claim)) return new Error(`${path}.no_claim: custom check failed`)
if (typeof o.sat_per_v_byte !== 'number') return new Error(`${path}.sat_per_v_byte: is not a number`)
if (opts.sat_per_v_byte_CustomCheck && !opts.sat_per_v_byte_CustomCheck(o.sat_per_v_byte)) return new Error(`${path}.sat_per_v_byte: 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 PayAdminTransactionSwapRequest = {
address: string
swap_operation_id: string
@ -3600,6 +3842,29 @@ export const ProvidersDisruptionValidate = (o?: ProvidersDisruption, opts: Provi
return null
}
export type RefundAdminInvoiceSwapRequest = {
sat_per_v_byte: number
swap_operation_id: string
}
export const RefundAdminInvoiceSwapRequestOptionalFields: [] = []
export type RefundAdminInvoiceSwapRequestOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
sat_per_v_byte_CustomCheck?: (v: number) => boolean
swap_operation_id_CustomCheck?: (v: string) => boolean
}
export const RefundAdminInvoiceSwapRequestValidate = (o?: RefundAdminInvoiceSwapRequest, opts: RefundAdminInvoiceSwapRequestOptions = {}, path: string = 'RefundAdminInvoiceSwapRequest::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.sat_per_v_byte !== 'number') return new Error(`${path}.sat_per_v_byte: is not a number`)
if (opts.sat_per_v_byte_CustomCheck && !opts.sat_per_v_byte_CustomCheck(o.sat_per_v_byte)) return new Error(`${path}.sat_per_v_byte: 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 RelaysMigration = {
relays: string[]
}
@ -3917,76 +4182,6 @@ export const SingleMetricReqValidate = (o?: SingleMetricReq, opts: SingleMetricR
return null
}
export type SwapOperation = {
address_paid: string
failure_reason?: string
operation_payment?: UserOperation
swap_operation_id: string
}
export type SwapOperationOptionalField = 'failure_reason' | 'operation_payment'
export const SwapOperationOptionalFields: SwapOperationOptionalField[] = ['failure_reason', 'operation_payment']
export type SwapOperationOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: SwapOperationOptionalField[]
address_paid_CustomCheck?: (v: string) => boolean
failure_reason_CustomCheck?: (v?: string) => boolean
operation_payment_Options?: UserOperationOptions
swap_operation_id_CustomCheck?: (v: string) => boolean
}
export const SwapOperationValidate = (o?: SwapOperation, opts: SwapOperationOptions = {}, path: string = 'SwapOperation::root.'): Error | null => {
if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message')
if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null')
if (typeof o.address_paid !== 'string') return new Error(`${path}.address_paid: is not a string`)
if (opts.address_paid_CustomCheck && !opts.address_paid_CustomCheck(o.address_paid)) return new Error(`${path}.address_paid: custom check failed`)
if ((o.failure_reason || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('failure_reason')) && typeof o.failure_reason !== 'string') return new Error(`${path}.failure_reason: is not a string`)
if (opts.failure_reason_CustomCheck && !opts.failure_reason_CustomCheck(o.failure_reason)) return new Error(`${path}.failure_reason: custom check failed`)
if (typeof o.operation_payment === 'object' || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('operation_payment')) {
const operation_paymentErr = UserOperationValidate(o.operation_payment, opts.operation_payment_Options, `${path}.operation_payment`)
if (operation_paymentErr !== null) return operation_paymentErr
}
if (typeof o.swap_operation_id !== 'string') return new Error(`${path}.swap_operation_id: is not a string`)
if (opts.swap_operation_id_CustomCheck && !opts.swap_operation_id_CustomCheck(o.swap_operation_id)) return new Error(`${path}.swap_operation_id: custom check failed`)
return null
}
export type SwapsList = {
quotes: TransactionSwapQuote[]
swaps: SwapOperation[]
}
export const SwapsListOptionalFields: [] = []
export type SwapsListOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
quotes_ItemOptions?: TransactionSwapQuoteOptions
quotes_CustomCheck?: (v: TransactionSwapQuote[]) => boolean
swaps_ItemOptions?: SwapOperationOptions
swaps_CustomCheck?: (v: SwapOperation[]) => boolean
}
export const SwapsListValidate = (o?: SwapsList, opts: SwapsListOptions = {}, path: string = 'SwapsList::root.'): Error | null => {
if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message')
if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null')
if (!Array.isArray(o.quotes)) return new Error(`${path}.quotes: is not an array`)
for (let index = 0; index < o.quotes.length; index++) {
const quotesErr = TransactionSwapQuoteValidate(o.quotes[index], opts.quotes_ItemOptions, `${path}.quotes[${index}]`)
if (quotesErr !== null) return quotesErr
}
if (opts.quotes_CustomCheck && !opts.quotes_CustomCheck(o.quotes)) return new Error(`${path}.quotes: custom check failed`)
if (!Array.isArray(o.swaps)) return new Error(`${path}.swaps: is not an array`)
for (let index = 0; index < o.swaps.length; index++) {
const swapsErr = SwapOperationValidate(o.swaps[index], opts.swaps_ItemOptions, `${path}.swaps[${index}]`)
if (swapsErr !== null) return swapsErr
}
if (opts.swaps_CustomCheck && !opts.swaps_CustomCheck(o.swaps)) return new Error(`${path}.swaps: custom check failed`)
return null
}
export type TransactionSwapQuote = {
chain_fee_sats: number
invoice_amount_sats: number
@ -4076,6 +4271,76 @@ export const TransactionSwapRequestValidate = (o?: TransactionSwapRequest, opts:
return null
}
export type TxSwapOperation = {
address_paid: string
failure_reason?: string
operation_payment?: UserOperation
swap_operation_id: string
}
export type TxSwapOperationOptionalField = 'failure_reason' | 'operation_payment'
export const TxSwapOperationOptionalFields: TxSwapOperationOptionalField[] = ['failure_reason', 'operation_payment']
export type TxSwapOperationOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: TxSwapOperationOptionalField[]
address_paid_CustomCheck?: (v: string) => boolean
failure_reason_CustomCheck?: (v?: string) => boolean
operation_payment_Options?: UserOperationOptions
swap_operation_id_CustomCheck?: (v: string) => boolean
}
export const TxSwapOperationValidate = (o?: TxSwapOperation, opts: TxSwapOperationOptions = {}, path: string = 'TxSwapOperation::root.'): Error | null => {
if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message')
if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null')
if (typeof o.address_paid !== 'string') return new Error(`${path}.address_paid: is not a string`)
if (opts.address_paid_CustomCheck && !opts.address_paid_CustomCheck(o.address_paid)) return new Error(`${path}.address_paid: custom check failed`)
if ((o.failure_reason || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('failure_reason')) && typeof o.failure_reason !== 'string') return new Error(`${path}.failure_reason: is not a string`)
if (opts.failure_reason_CustomCheck && !opts.failure_reason_CustomCheck(o.failure_reason)) return new Error(`${path}.failure_reason: custom check failed`)
if (typeof o.operation_payment === 'object' || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('operation_payment')) {
const operation_paymentErr = UserOperationValidate(o.operation_payment, opts.operation_payment_Options, `${path}.operation_payment`)
if (operation_paymentErr !== null) return operation_paymentErr
}
if (typeof o.swap_operation_id !== 'string') return new Error(`${path}.swap_operation_id: is not a string`)
if (opts.swap_operation_id_CustomCheck && !opts.swap_operation_id_CustomCheck(o.swap_operation_id)) return new Error(`${path}.swap_operation_id: custom check failed`)
return null
}
export type TxSwapsList = {
quotes: TransactionSwapQuote[]
swaps: TxSwapOperation[]
}
export const TxSwapsListOptionalFields: [] = []
export type TxSwapsListOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
quotes_ItemOptions?: TransactionSwapQuoteOptions
quotes_CustomCheck?: (v: TransactionSwapQuote[]) => boolean
swaps_ItemOptions?: TxSwapOperationOptions
swaps_CustomCheck?: (v: TxSwapOperation[]) => boolean
}
export const TxSwapsListValidate = (o?: TxSwapsList, opts: TxSwapsListOptions = {}, path: string = 'TxSwapsList::root.'): Error | null => {
if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message')
if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null')
if (!Array.isArray(o.quotes)) return new Error(`${path}.quotes: is not an array`)
for (let index = 0; index < o.quotes.length; index++) {
const quotesErr = TransactionSwapQuoteValidate(o.quotes[index], opts.quotes_ItemOptions, `${path}.quotes[${index}]`)
if (quotesErr !== null) return quotesErr
}
if (opts.quotes_CustomCheck && !opts.quotes_CustomCheck(o.quotes)) return new Error(`${path}.quotes: custom check failed`)
if (!Array.isArray(o.swaps)) return new Error(`${path}.swaps: is not an array`)
for (let index = 0; index < o.swaps.length; index++) {
const swapsErr = TxSwapOperationValidate(o.swaps[index], opts.swaps_ItemOptions, `${path}.swaps[${index}]`)
if (swapsErr !== null) return swapsErr
}
if (opts.swaps_CustomCheck && !opts.swaps_CustomCheck(o.swaps)) return new Error(`${path}.swaps: custom check failed`)
return null
}
export type UpdateChannelPolicyRequest = {
policy: ChannelPolicy
update: UpdateChannelPolicyRequest_update

View file

@ -175,6 +175,34 @@ service LightningPub {
option (nostr) = true;
}
rpc GetAdminInvoiceSwapQuotes(structs.InvoiceSwapRequest) returns (structs.InvoiceSwapQuoteList) {
option (auth_type) = "Admin";
option (http_method) = "post";
option (http_route) = "/api/admin/swap/invoice/quote";
option (nostr) = true;
}
rpc ListAdminInvoiceSwaps(structs.Empty) returns (structs.InvoiceSwapsList) {
option (auth_type) = "Admin";
option (http_method) = "post";
option (http_route) = "/api/admin/swap/invoice/list";
option (nostr) = true;
}
rpc PayAdminInvoiceSwap(structs.PayAdminInvoiceSwapRequest) returns (structs.AdminInvoiceSwapResponse) {
option (auth_type) = "Admin";
option (http_method) = "post";
option (http_route) = "/api/admin/swap/invoice/pay";
option (nostr) = true;
}
rpc RefundAdminInvoiceSwap(structs.RefundAdminInvoiceSwapRequest) returns (structs.AdminInvoiceSwapResponse) {
option (auth_type) = "Admin";
option (http_method) = "post";
option (http_route) = "/api/admin/swap/invoice/refund";
option (nostr) = true;
}
rpc GetAdminTransactionSwapQuotes(structs.TransactionSwapRequest) returns (structs.TransactionSwapQuoteList) {
option (auth_type) = "Admin";
option (http_method) = "post";
@ -182,17 +210,17 @@ service LightningPub {
option (nostr) = true;
}
rpc PayAdminTransactionSwap(structs.PayAdminTransactionSwapRequest) returns (structs.AdminSwapResponse) {
rpc PayAdminTransactionSwap(structs.PayAdminTransactionSwapRequest) returns (structs.AdminTxSwapResponse) {
option (auth_type) = "Admin";
option (http_method) = "post";
option (http_route) = "/api/admin/swap/transaction/pay";
option (nostr) = true;
}
rpc ListAdminSwaps(structs.Empty) returns (structs.SwapsList) {
rpc ListAdminTxSwaps(structs.Empty) returns (structs.TxSwapsList) {
option (auth_type) = "Admin";
option (http_method) = "post";
option (http_route) = "/api/admin/swap/list";
option (http_route) = "/api/admin/swap/transaction/list";
option (nostr) = true;
}
@ -520,14 +548,14 @@ service LightningPub {
rpc GetTransactionSwapQuotes(structs.TransactionSwapRequest) returns (structs.TransactionSwapQuoteList){
option (auth_type) = "User";
option (http_method) = "post";
option (http_route) = "/api/user/swap/quote";
option (http_route) = "/api/user/swap/transaction/quote";
option (nostr) = true;
}
rpc ListSwaps(structs.Empty) returns (structs.SwapsList){
rpc ListTxSwaps(structs.Empty) returns (structs.TxSwapsList){
option (auth_type) = "User";
option (http_method) = "post";
option (http_route) = "/api/user/swap/list";
option (http_route) = "/api/user/swap/transaction/list";
option (nostr) = true;
}

View file

@ -833,6 +833,55 @@ message MessagingToken {
string firebase_messaging_token = 2;
}
message InvoiceSwapRequest {
int64 amount_sats = 1;
}
message InvoiceSwapQuote {
string swap_operation_id = 1;
string invoice = 2;
int64 invoice_amount_sats = 3;
string address = 4;
int64 transaction_amount_sats = 5;
int64 chain_fee_sats = 6;
int64 service_fee_sats = 7;
string service_url = 8;
int64 swap_fee_sats = 9;
string tx_id = 10;
}
message InvoiceSwapQuoteList {
repeated InvoiceSwapQuote quotes = 1;
}
message InvoiceSwapOperation {
string swap_operation_id = 1;
optional UserOperation operation_payment = 2;
optional string failure_reason = 3;
string invoice_paid = 4;
string tx_id = 5;
}
message InvoiceSwapsList {
repeated InvoiceSwapOperation swaps = 1;
repeated InvoiceSwapQuote quotes = 2;
}
message RefundAdminInvoiceSwapRequest {
string swap_operation_id = 1;
int64 sat_per_v_byte = 2;
}
message PayAdminInvoiceSwapRequest {
string swap_operation_id = 1;
int64 sat_per_v_byte = 2;
optional bool no_claim = 3;
}
message AdminInvoiceSwapResponse {
string tx_id = 1;
}
message TransactionSwapRequest {
int64 transaction_amount_sats = 2;
}
@ -857,20 +906,20 @@ message TransactionSwapQuoteList {
repeated TransactionSwapQuote quotes = 1;
}
message AdminSwapResponse {
message AdminTxSwapResponse {
string tx_id = 1;
int64 network_fee = 2;
}
message SwapOperation {
message TxSwapOperation {
string swap_operation_id = 1;
optional UserOperation operation_payment = 2;
optional string failure_reason = 3;
string address_paid = 4;
}
message SwapsList {
repeated SwapOperation swaps = 1;
message TxSwapsList {
repeated TxSwapOperation swaps = 1;
repeated TransactionSwapQuote quotes = 2;
}

View file

@ -23,7 +23,7 @@ import { TxPointSettings } from '../storage/tlv/stateBundler.js';
import { WalletKitClient } from '../../../proto/lnd/walletkit.client.js';
import SettingsManager from '../main/settingsManager.js';
import { LndNodeSettings, LndSettings } from '../main/settings.js';
import { ListAddressesResponse } from '../../../proto/lnd/walletkit.js';
import { ListAddressesResponse, PublishResponse } from '../../../proto/lnd/walletkit.js';
const DeadLineMetadata = (deadline = 10 * 1000) => ({ deadline: Date.now() + deadline })
const deadLndRetrySeconds = 20
@ -157,6 +157,13 @@ export default class {
})
}
async PublishTransaction(txHex: string): Promise<PublishResponse> {
const res = await this.walletKit.publishTransaction({
txHex: Buffer.from(txHex, 'hex'), label: ""
}, DeadLineMetadata())
return res.response
}
async GetInfo(): Promise<NodeInfo> {
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
// Return dummy info when bypass is enabled

View file

@ -1,657 +0,0 @@
import zkpInit from '@vulpemventures/secp256k1-zkp';
import axios from 'axios';
import { crypto, initEccLib, Transaction, address, Network } from 'bitcoinjs-lib';
// import bolt11 from 'bolt11';
import {
Musig, SwapTreeSerializer, TaprootUtils, detectSwap,
constructClaimTransaction, targetFee, OutputType,
Networks,
} from 'boltz-core';
import { randomBytes, createHash } from 'crypto';
import { ECPairFactory, ECPairInterface } from 'ecpair';
import * as ecc from 'tiny-secp256k1';
import ws from 'ws';
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';
import { UserInvoicePayment } from '../storage/entity/UserInvoicePayment.js';
type InvoiceSwapResponse = { id: string, claimPublicKey: string, swapTree: string }
type InvoiceSwapInfo = { paymentHash: string, keys: ECPairInterface }
type InvoiceSwapData = { createdResponse: InvoiceSwapResponse, info: InvoiceSwapInfo }
type TransactionSwapFees = {
percentage: number,
minerFees: {
claim: number,
lockup: number,
}
}
type TransactionSwapFeesRes = {
BTC?: {
BTC?: {
fees: TransactionSwapFees
}
}
}
type TransactionSwapResponse = {
id: string, refundPublicKey: string, swapTree: string,
timeoutBlockHeight: number, lockupAddress: string, invoice: string,
onchainAmount?: number
}
type TransactionSwapInfo = { destinationAddress: string, preimage: Buffer, keys: ECPairInterface, chainFee: number }
export type TransactionSwapData = { createdResponse: TransactionSwapResponse, info: TransactionSwapInfo }
export class Swaps {
settings: SettingsManager
revSwappers: Record<string, ReverseSwaps>
// submarineSwaps: SubmarineSwaps
storage: Storage
lnd: LND
log = getLogger({ component: 'swaps' })
constructor(settings: SettingsManager, storage: Storage) {
this.settings = settings
this.revSwappers = {}
const network = settings.getSettings().lndSettings.network
const { boltzHttpUrl, boltzWebSocketUrl, boltsHttpUrlAlt, boltsWebSocketUrlAlt } = settings.getSettings().swapsSettings
if (boltzHttpUrl && boltzWebSocketUrl) {
this.revSwappers[boltzHttpUrl] = new ReverseSwaps({ httpUrl: boltzHttpUrl, wsUrl: boltzWebSocketUrl, network })
}
if (boltsHttpUrlAlt && boltsWebSocketUrlAlt) {
this.revSwappers[boltsHttpUrlAlt] = new ReverseSwaps({ httpUrl: boltsHttpUrlAlt, wsUrl: boltsWebSocketUrlAlt, network })
}
this.storage = storage
}
SetLnd = (lnd: LND) => {
this.lnd = lnd
}
Stop = () => { }
GetKeys = (privateKey: string) => {
const keys = ECPairFactory(ecc).fromPrivateKey(Buffer.from(privateKey, 'hex'))
return keys
}
ListSwaps = async (appUserId: string, payments: UserInvoicePayment[], newOp: (p: UserInvoicePayment) => Types.UserOperation | undefined, getServiceFee: (amt: number) => number): Promise<Types.SwapsList> => {
const completedSwaps = await this.storage.paymentStorage.ListCompletedSwaps(appUserId, payments)
const pendingSwaps = await this.storage.paymentStorage.ListPendingTransactionSwaps(appUserId)
return {
swaps: completedSwaps.map(s => {
const p = s.payment
const op = p ? newOp(p) : undefined
return {
operation_payment: op,
swap_operation_id: s.swap.swap_operation_id,
address_paid: s.swap.address_paid,
failure_reason: s.swap.failure_reason,
}
}),
quotes: pendingSwaps.map(s => {
const serviceFee = getServiceFee(s.invoice_amount)
return {
swap_operation_id: s.swap_operation_id,
invoice_amount_sats: s.invoice_amount,
transaction_amount_sats: s.transaction_amount,
chain_fee_sats: s.chain_fee_sats,
service_fee_sats: serviceFee,
swap_fee_sats: s.swap_fee_sats,
service_url: s.service_url,
}
})
}
}
GetTxSwapQuotes = async (appUserId: string, amt: number, getServiceFee: (decodedAmt: number) => number): Promise<Types.TransactionSwapQuote[]> => {
if (!this.settings.getSettings().swapsSettings.enableSwaps) {
throw new Error("Swaps are not enabled")
}
const swappers = Object.values(this.revSwappers)
if (swappers.length === 0) {
throw new Error("No swap services available")
}
const res = await Promise.allSettled(swappers.map(sw => this.getTxSwapQuote(sw, appUserId, amt, getServiceFee)))
const failures: string[] = []
const success: Types.TransactionSwapQuote[] = []
for (const r of res) {
if (r.status === 'fulfilled') {
success.push(r.value)
} else {
failures.push(r.reason.message ? r.reason.message : r.reason.toString())
}
}
if (success.length === 0) {
throw new Error(failures.join("\n"))
}
return success
}
private async getTxSwapQuote(swapper: ReverseSwaps, appUserId: string, amt: number, getServiceFee: (decodedAmt: number) => number): Promise<Types.TransactionSwapQuote> {
this.log("getting transaction swap quote")
const feesRes = await swapper.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 swapper.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,
service_url: swapper.getHttpUrl(),
})
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,
service_url: swapper.getHttpUrl(),
}
}
async PayAddrWithSwap(appUserId: string, swapOpId: string, address: string, payInvoice: (invoice: string, amt: number) => Promise<void>) {
if (!this.settings.getSettings().swapsSettings.enableSwaps) {
throw new Error("Swaps are not enabled")
}
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 swapper = this.revSwappers[txSwap.service_url]
if (!swapper) {
throw new Error("swapper service not found")
}
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 }
swapper.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 ReverseSwaps {
// settings: SettingsManager
private httpUrl: string
private wsUrl: string
log: PubLogger
private network: BTCNetwork
constructor({ httpUrl, wsUrl, network }: { httpUrl: string, wsUrl: string, network: BTCNetwork }) {
this.httpUrl = httpUrl
this.wsUrl = wsUrl
this.network = network
this.log = getLogger({ component: 'ReverseSwaps' })
initEccLib(ecc)
}
getHttpUrl = () => {
return this.httpUrl
}
getWsUrl = () => {
return this.wsUrl
}
calculateFees = (fees: TransactionSwapFees, receiveAmount: number) => {
const pct = fees.percentage / 100
const minerFee = fees.minerFees.claim + fees.minerFees.lockup
const preFee = receiveAmount + minerFee
const fee = Math.ceil(preFee * pct)
const total = preFee + fee
return { total, fee, minerFee }
}
GetFees = async (): Promise<{ ok: true, fees: TransactionSwapFees, } | { ok: false, error: string }> => {
const url = `${this.httpUrl}/v2/swap/reverse`
const feesRes = await loggedGet<TransactionSwapFeesRes>(this.log, url)
if (!feesRes.ok) {
return { ok: false, error: feesRes.error }
}
if (!feesRes.data.BTC?.BTC?.fees) {
return { ok: false, error: 'No fees found for BTC to BTC swap' }
}
return { ok: true, fees: feesRes.data.BTC.BTC.fees }
}
SwapTransaction = async (txAmount: number): Promise<{ ok: true, createdResponse: TransactionSwapResponse, preimage: string, pubkey: string, privKey: string } | { ok: false, error: string }> => {
const preimage = randomBytes(32);
const keys = ECPairFactory(ecc).makeRandom()
if (!keys.privateKey) {
return { ok: false, error: 'Failed to generate keys' }
}
const url = `${this.httpUrl}/v2/swap/reverse`
const req: any = {
onchainAmount: txAmount,
to: 'BTC',
from: 'BTC',
claimPublicKey: Buffer.from(keys.publicKey).toString('hex'),
preimageHash: createHash('sha256').update(preimage).digest('hex'),
}
const createdResponseRes = await loggedPost<TransactionSwapResponse>(this.log, url, req)
if (!createdResponseRes.ok) {
return createdResponseRes
}
const createdResponse = createdResponseRes.data
this.log('Created transaction swap');
this.log(createdResponse);
return {
ok: true, createdResponse,
preimage: Buffer.from(preimage).toString('hex'),
pubkey: Buffer.from(keys.publicKey).toString('hex'),
privKey: Buffer.from(keys.privateKey).toString('hex')
}
}
SubscribeToTransactionSwap = async (data: TransactionSwapData, swapDone: (result: { ok: true, txId: string } | { ok: false, error: string }) => void) => {
const webSocket = new ws(`${this.wsUrl}/v2/ws`)
const subReq = { op: 'subscribe', channel: 'swap.update', args: [data.createdResponse.id] }
webSocket.on('open', () => {
webSocket.send(JSON.stringify(subReq))
})
let txId = "", isDone = false
const done = () => {
isDone = true
webSocket.close()
swapDone({ ok: true, txId })
}
webSocket.on('error', (err) => {
this.log(ERROR, 'Error in WebSocket', err.message)
})
webSocket.on('close', () => {
if (!isDone) {
this.log(ERROR, 'WebSocket closed before swap was done');
swapDone({ ok: false, error: 'WebSocket closed before swap was done' })
}
})
webSocket.on('message', async (rawMsg) => {
try {
const result = await this.handleSwapTransactionMessage(rawMsg, data, done)
if (result) {
txId = result
}
} catch (err: any) {
this.log(ERROR, 'Error handling transaction WebSocket message', err.message)
isDone = true
webSocket.close()
swapDone({ ok: false, error: err.message })
return
}
})
}
handleSwapTransactionMessage = async (rawMsg: ws.RawData, data: TransactionSwapData, done: () => void) => {
const msg = JSON.parse(rawMsg.toString('utf-8'));
if (msg.event !== 'update') {
return;
}
this.log('Got WebSocket update');
this.log(msg);
switch (msg.args[0].status) {
// "swap.created" means Boltz is waiting for the invoice to be paid
case 'swap.created':
this.log('Waiting invoice to be paid');
return;
// "transaction.mempool" means that Boltz sent an onchain transaction
case 'transaction.mempool':
const txIdRes = await this.handleTransactionMempool(data, msg.args[0].transaction.hex)
if (!txIdRes.ok) {
throw new Error(txIdRes.error)
}
return txIdRes.txId
case 'invoice.settled':
this.log('Transaction swap successful');
done()
return;
}
}
handleTransactionMempool = async (data: TransactionSwapData, txHex: string): Promise<{ ok: true, txId: string } | { ok: false, error: string }> => {
this.log('Creating claim transaction');
const { createdResponse, info } = data
const { destinationAddress, keys, preimage, chainFee } = info
const boltzPublicKey = Buffer.from(
createdResponse.refundPublicKey,
'hex',
);
// Create a musig signing session and tweak it with the Taptree of the swap scripts
const musig = new Musig(await zkpInit(), keys, randomBytes(32), [
boltzPublicKey,
Buffer.from(keys.publicKey),
]);
const tweakedKey = TaprootUtils.tweakMusig(
musig,
// swap tree can either be a string or an object
SwapTreeSerializer.deserializeSwapTree(createdResponse.swapTree).tree,
);
// Parse the lockup transaction and find the output relevant for the swap
const lockupTx = Transaction.fromHex(txHex);
const swapOutput = detectSwap(tweakedKey, lockupTx);
if (swapOutput === undefined) {
this.log(ERROR, 'No swap output found in lockup transaction');
return { ok: false, error: 'No swap output found in lockup transaction' }
}
const network = getNetwork(this.network)
// Create a claim transaction to be signed cooperatively via a key path spend
const claimTx = constructClaimTransaction(
[
{
...swapOutput,
keys,
preimage,
cooperative: true,
type: OutputType.Taproot,
txHash: lockupTx.getHash(),
},
],
address.toOutputScript(destinationAddress, network),
chainFee,
)
// Get the partial signature from Boltz
const claimUrl = `${this.httpUrl}/v2/swap/reverse/${createdResponse.id}/claim`
const claimReq = {
index: 0,
transaction: claimTx.toHex(),
preimage: preimage.toString('hex'),
pubNonce: Buffer.from(musig.getPublicNonce()).toString('hex'),
}
const boltzSigRes = await loggedPost<{ pubNonce: string, partialSignature: string }>(this.log, claimUrl, claimReq)
if (!boltzSigRes.ok) {
return boltzSigRes
}
const boltzSig = boltzSigRes.data
// Aggregate the nonces
musig.aggregateNonces([
[boltzPublicKey, Buffer.from(boltzSig.pubNonce, 'hex')],
]);
// Initialize the session to sign the claim transaction
musig.initializeSession(
claimTx.hashForWitnessV1(
0,
[swapOutput.script],
[swapOutput.value],
Transaction.SIGHASH_DEFAULT,
),
);
// Add the partial signature from Boltz
musig.addPartial(
boltzPublicKey,
Buffer.from(boltzSig.partialSignature, 'hex'),
);
// Create our partial signature
musig.signPartial();
// Witness of the input to the aggregated signature
claimTx.ins[0].witness = [musig.aggregatePartials()];
// Broadcast the finalized transaction
const broadcastUrl = `${this.httpUrl}/v2/chain/BTC/transaction`
const broadcastReq = {
hex: claimTx.toHex(),
}
const broadcastResponse = await loggedPost<any>(this.log, broadcastUrl, broadcastReq)
if (!broadcastResponse.ok) {
return broadcastResponse
}
this.log('Transaction broadcasted', broadcastResponse.data)
const txId = claimTx.getId()
return { ok: true, txId }
}
}
const loggedPost = async <T>(log: PubLogger, url: string, req: any): Promise<{ ok: true, data: T } | { ok: false, error: string }> => {
try {
const { data } = await axios.post(url, req)
return { ok: true, data: data as T }
} catch (err: any) {
if (err.response?.data) {
log(ERROR, 'Error sending request', err.response.data)
return { ok: false, error: JSON.stringify(err.response.data) }
}
log(ERROR, 'Error sending request', err.message)
return { ok: false, error: err.message }
}
}
const loggedGet = async <T>(log: PubLogger, url: string): Promise<{ ok: true, data: T } | { ok: false, error: string }> => {
try {
const { data } = await axios.get(url)
return { ok: true, data: data as T }
} catch (err: any) {
if (err.response?.data) {
log(ERROR, 'Error getting request', err.response.data)
return { ok: false, error: err.response.data }
}
log(ERROR, 'Error getting request', err.message)
return { ok: false, error: err.message }
}
}
const getNetwork = (network: BTCNetwork): Network => {
switch (network) {
case 'mainnet':
return Networks.bitcoinMainnet
case 'testnet':
return Networks.bitcoinTestnet
case 'regtest':
return Networks.bitcoinRegtest
default:
throw new Error(`Invalid network: ${network}`)
}
}
// Submarine swaps currently not supported, keeping the code for future reference
/*
export class SubmarineSwaps {
settings: SettingsManager
log: PubLogger
constructor(settings: SettingsManager) {
this.settings = settings
this.log = getLogger({ component: 'SubmarineSwaps' })
}
SwapInvoice = async (invoice: string, paymentHash: string) => {
if (!this.settings.getSettings().swapsSettings.enableSwaps) {
this.log(ERROR, 'Swaps are not enabled');
return;
}
const keys = ECPairFactory(ecc).makeRandom()
const refundPublicKey = Buffer.from(keys.publicKey).toString('hex')
const req = { invoice, to: 'BTC', from: 'BTC', refundPublicKey }
const url = `${this.settings.getSettings().swapsSettings.boltzHttpUrl}/v2/swap/submarine`
this.log('Sending invoice swap request to', url);
const createdResponseRes = await loggedPost<InvoiceSwapResponse>(this.log, url, req)
if (!createdResponseRes.ok) {
return createdResponseRes
}
const createdResponse = createdResponseRes.data
this.log('Created invoice swap');
this.log(createdResponse);
const webSocket = new ws(`${this.settings.getSettings().swapsSettings.boltzWebSocketUrl}/v2/ws`)
const subReq = { op: 'subscribe', channel: 'swap.update', args: [createdResponse.id] }
webSocket.on('open', () => {
webSocket.send(JSON.stringify(subReq))
})
webSocket.on('message', async (rawMsg) => {
try {
await this.handleSwapInvoiceMessage(rawMsg, { createdResponse, info: { paymentHash, keys } }, () => webSocket.close())
} catch (err: any) {
this.log(ERROR, 'Error handling invoice WebSocket message', err.message)
webSocket.close()
return
}
});
}
handleSwapInvoiceMessage = async (rawMsg: ws.RawData, data: InvoiceSwapData, closeWebSocket: () => void) => {
const msg = JSON.parse(rawMsg.toString('utf-8'));
if (msg.event !== 'update') {
return;
}
this.log('Got invoice WebSocket update');
this.log(msg);
switch (msg.args[0].status) {
// "invoice.set" means Boltz is waiting for an onchain transaction to be sent
case 'invoice.set':
this.log('Waiting for onchain transaction');
return;
// Create a partial signature to allow Boltz to do a key path spend to claim the mainchain coins
case 'transaction.claim.pending':
await this.handleInvoiceClaimPending(data)
return;
case 'transaction.claimed':
this.log('Invoice swap successful');
closeWebSocket()
return;
}
}
handleInvoiceClaimPending = async (data: InvoiceSwapData) => {
this.log('Creating cooperative claim transaction');
const { createdResponse, info } = data
const { paymentHash, keys } = info
const { boltzHttpUrl } = this.settings.getSettings().swapsSettings
// Get the information request to create a partial signature
const url = `${boltzHttpUrl}/v2/swap/submarine/${createdResponse.id}/claim`
const claimTxDetailsRes = await loggedGet<{ preimage: string, transactionHash: string, pubNonce: string }>(this.log, url)
if (!claimTxDetailsRes.ok) {
return claimTxDetailsRes
}
const claimTxDetails = claimTxDetailsRes.data
// Verify that Boltz actually paid the invoice by comparing the preimage hash
// of the invoice to the SHA256 hash of the preimage from the response
const claimTxPreimageHash = createHash('sha256').update(Buffer.from(claimTxDetails.preimage, 'hex')).digest()
const invoicePreimageHash = Buffer.from(paymentHash, 'hex')
if (!claimTxPreimageHash.equals(invoicePreimageHash)) {
this.log(ERROR, 'Boltz provided invalid preimage');
return;
}
const boltzPublicKey = Buffer.from(createdResponse.claimPublicKey, 'hex')
// Create a musig signing instance
const musig = new Musig(await zkpInit(), keys, randomBytes(32), [
boltzPublicKey,
Buffer.from(keys.publicKey),
]);
// Tweak that musig with the Taptree of the swap scripts
TaprootUtils.tweakMusig(
musig,
SwapTreeSerializer.deserializeSwapTree(createdResponse.swapTree).tree,
);
// Aggregate the nonces
musig.aggregateNonces([
[boltzPublicKey, Buffer.from(claimTxDetails.pubNonce, 'hex')],
]);
// Initialize the session to sign the transaction hash from the response
musig.initializeSession(
Buffer.from(claimTxDetails.transactionHash, 'hex'),
);
// Give our public nonce and the partial signature to Boltz
const claimUrl = `${boltzHttpUrl}/v2/swap/submarine/${createdResponse.id}/claim`
const claimReq = {
pubNonce: Buffer.from(musig.getPublicNonce()).toString('hex'),
partialSignature: Buffer.from(musig.signPartial()).toString('hex'),
}
const claimResponseRes = await loggedPost<{ pubNonce: string, partialSignature: string }>(this.log, claimUrl, claimReq)
if (!claimResponseRes.ok) {
return claimResponseRes
}
const claimResponse = claimResponseRes.data
this.log('Claim response', claimResponse)
}
}
*/

View file

@ -0,0 +1,294 @@
import secp256k1ZkpModule from '@vulpemventures/secp256k1-zkp';
const zkpInit = (secp256k1ZkpModule as any).default || secp256k1ZkpModule;
import { initEccLib, Transaction, address } from 'bitcoinjs-lib';
// import bolt11 from 'bolt11';
import {
Musig, SwapTreeSerializer, TaprootUtils, detectSwap,
constructClaimTransaction, OutputType, constructRefundTransaction
} from 'boltz-core';
import { randomBytes, createHash } from 'crypto';
import { ECPairFactory, ECPairInterface } from 'ecpair';
import * as ecc from 'tiny-secp256k1';
import ws from 'ws';
import { getLogger, PubLogger, ERROR } from '../../helpers/logger.js';
import { BTCNetwork } from '../../main/settings.js';
import { loggedGet, loggedPost, getNetwork } from './swapHelpers.js';
type TransactionSwapFees = {
percentage: number,
minerFees: {
claim: number,
lockup: number,
}
}
type TransactionSwapFeesRes = {
BTC?: {
BTC?: {
fees: TransactionSwapFees
}
}
}
type TransactionSwapResponse = {
id: string, refundPublicKey: string, swapTree: string,
timeoutBlockHeight: number, lockupAddress: string, invoice: string,
onchainAmount?: number
}
type TransactionSwapInfo = { destinationAddress: string, preimage: Buffer, keys: ECPairInterface, chainFee: number }
export type TransactionSwapData = { createdResponse: TransactionSwapResponse, info: TransactionSwapInfo }
export class ReverseSwaps {
// settings: SettingsManager
private httpUrl: string
private wsUrl: string
log: PubLogger
private network: BTCNetwork
constructor({ httpUrl, wsUrl, network }: { httpUrl: string, wsUrl: string, network: BTCNetwork }) {
this.httpUrl = httpUrl
this.wsUrl = wsUrl
this.network = network
this.log = getLogger({ component: 'ReverseSwaps' })
initEccLib(ecc)
}
getHttpUrl = () => {
return this.httpUrl
}
getWsUrl = () => {
return this.wsUrl
}
calculateFees = (fees: TransactionSwapFees, receiveAmount: number) => {
const pct = fees.percentage / 100
const minerFee = fees.minerFees.claim + fees.minerFees.lockup
const preFee = receiveAmount + minerFee
const fee = Math.ceil(preFee * pct)
const total = preFee + fee
return { total, fee, minerFee }
}
GetFees = async (): Promise<{ ok: true, fees: TransactionSwapFees, } | { ok: false, error: string }> => {
const url = `${this.httpUrl}/v2/swap/reverse`
const feesRes = await loggedGet<TransactionSwapFeesRes>(this.log, url)
if (!feesRes.ok) {
return { ok: false, error: feesRes.error }
}
if (!feesRes.data.BTC?.BTC?.fees) {
return { ok: false, error: 'No fees found for BTC to BTC swap' }
}
return { ok: true, fees: feesRes.data.BTC.BTC.fees }
}
SwapTransaction = async (txAmount: number): Promise<{ ok: true, createdResponse: TransactionSwapResponse, preimage: string, pubkey: string, privKey: string } | { ok: false, error: string }> => {
const preimage = randomBytes(32);
const keys = ECPairFactory(ecc).makeRandom()
if (!keys.privateKey) {
return { ok: false, error: 'Failed to generate keys' }
}
const url = `${this.httpUrl}/v2/swap/reverse`
const req: any = {
onchainAmount: txAmount,
to: 'BTC',
from: 'BTC',
claimPublicKey: Buffer.from(keys.publicKey).toString('hex'),
preimageHash: createHash('sha256').update(preimage).digest('hex'),
}
const createdResponseRes = await loggedPost<TransactionSwapResponse>(this.log, url, req)
if (!createdResponseRes.ok) {
return createdResponseRes
}
const createdResponse = createdResponseRes.data
this.log('Created transaction swap');
this.log(createdResponse);
return {
ok: true, createdResponse,
preimage: Buffer.from(preimage).toString('hex'),
pubkey: Buffer.from(keys.publicKey).toString('hex'),
privKey: Buffer.from(keys.privateKey).toString('hex')
}
}
SubscribeToTransactionSwap = async (data: TransactionSwapData, swapDone: (result: { ok: true, txId: string } | { ok: false, error: string }) => void) => {
const webSocket = new ws(`${this.wsUrl}/v2/ws`)
const subReq = { op: 'subscribe', channel: 'swap.update', args: [data.createdResponse.id] }
webSocket.on('open', () => {
webSocket.send(JSON.stringify(subReq))
})
let txId = "", isDone = false
const done = (failureReason?: string) => {
isDone = true
webSocket.close()
if (failureReason) {
swapDone({ ok: false, error: failureReason })
} else {
swapDone({ ok: true, txId })
}
}
webSocket.on('error', (err) => {
this.log(ERROR, 'Error in WebSocket', err.message)
})
webSocket.on('close', () => {
if (!isDone) {
this.log(ERROR, 'WebSocket closed before swap was done');
done('WebSocket closed before swap was done')
}
})
webSocket.on('message', async (rawMsg) => {
try {
const result = await this.handleSwapTransactionMessage(rawMsg, data, done)
if (result) {
txId = result
}
} catch (err: any) {
this.log(ERROR, 'Error handling transaction WebSocket message', err.message)
isDone = true
webSocket.close()
swapDone({ ok: false, error: err.message })
return
}
})
}
handleSwapTransactionMessage = async (rawMsg: ws.RawData, data: TransactionSwapData, done: (failureReason?: string) => void) => {
const msg = JSON.parse(rawMsg.toString('utf-8'));
if (msg.event !== 'update') {
return;
}
this.log('Got WebSocket update');
this.log(msg);
switch (msg.args[0].status) {
// "swap.created" means Boltz is waiting for the invoice to be paid
case 'swap.created':
this.log('Waiting invoice to be paid');
return;
// "transaction.mempool" means that Boltz sent an onchain transaction
case 'transaction.mempool':
const txIdRes = await this.handleTransactionMempool(data, msg.args[0].transaction.hex)
if (!txIdRes.ok) {
throw new Error(txIdRes.error)
}
return txIdRes.txId
case 'invoice.settled':
this.log('Transaction swap successful');
done()
return;
case 'invoice.expired':
case 'swap.expired':
case 'transaction.failed':
done(`swap ${data.createdResponse.id} failed with status ${msg.args[0].status}`)
return;
default:
this.log('Unknown swap transaction WebSocket message', msg)
return;
}
}
handleTransactionMempool = async (data: TransactionSwapData, txHex: string): Promise<{ ok: true, txId: string } | { ok: false, error: string }> => {
this.log('Creating claim transaction');
const { createdResponse, info } = data
const { destinationAddress, keys, preimage, chainFee } = info
const boltzPublicKey = Buffer.from(
createdResponse.refundPublicKey,
'hex',
);
// Create a musig signing session and tweak it with the Taptree of the swap scripts
const musig = new Musig(await zkpInit(), keys, randomBytes(32), [
boltzPublicKey,
Buffer.from(keys.publicKey),
]);
const tweakedKey = TaprootUtils.tweakMusig(
musig,
// swap tree can either be a string or an object
SwapTreeSerializer.deserializeSwapTree(createdResponse.swapTree).tree,
);
// Parse the lockup transaction and find the output relevant for the swap
const lockupTx = Transaction.fromHex(txHex);
const swapOutput = detectSwap(tweakedKey, lockupTx);
if (swapOutput === undefined) {
this.log(ERROR, 'No swap output found in lockup transaction');
return { ok: false, error: 'No swap output found in lockup transaction' }
}
const network = getNetwork(this.network)
// Create a claim transaction to be signed cooperatively via a key path spend
const claimTx = constructClaimTransaction(
[
{
...swapOutput,
keys,
preimage,
cooperative: true,
type: OutputType.Taproot,
txHash: lockupTx.getHash(),
},
],
address.toOutputScript(destinationAddress, network),
chainFee,
)
// Get the partial signature from Boltz
const claimUrl = `${this.httpUrl}/v2/swap/reverse/${createdResponse.id}/claim`
const claimReq = {
index: 0,
transaction: claimTx.toHex(),
preimage: preimage.toString('hex'),
pubNonce: Buffer.from(musig.getPublicNonce()).toString('hex'),
}
const boltzSigRes = await loggedPost<{ pubNonce: string, partialSignature: string }>(this.log, claimUrl, claimReq)
if (!boltzSigRes.ok) {
return boltzSigRes
}
const boltzSig = boltzSigRes.data
// Aggregate the nonces
musig.aggregateNonces([
[boltzPublicKey, Buffer.from(boltzSig.pubNonce, 'hex')],
]);
// Initialize the session to sign the claim transaction
musig.initializeSession(
claimTx.hashForWitnessV1(
0,
[swapOutput.script],
[swapOutput.value],
Transaction.SIGHASH_DEFAULT,
),
);
// Add the partial signature from Boltz
musig.addPartial(
boltzPublicKey,
Buffer.from(boltzSig.partialSignature, 'hex'),
);
// Create our partial signature
musig.signPartial();
// Witness of the input to the aggregated signature
claimTx.ins[0].witness = [musig.aggregatePartials()];
// Broadcast the finalized transaction
const broadcastUrl = `${this.httpUrl}/v2/chain/BTC/transaction`
const broadcastReq = {
hex: claimTx.toHex(),
}
const broadcastResponse = await loggedPost<any>(this.log, broadcastUrl, broadcastReq)
if (!broadcastResponse.ok) {
return broadcastResponse
}
this.log('Transaction broadcasted', broadcastResponse.data)
const txId = claimTx.getId()
return { ok: true, txId }
}
}

View file

@ -0,0 +1,503 @@
import secp256k1ZkpModule from '@vulpemventures/secp256k1-zkp';
const zkpInit = (secp256k1ZkpModule as any).default || secp256k1ZkpModule;
// import bolt11 from 'bolt11';
import {
Musig, SwapTreeSerializer, TaprootUtils, constructRefundTransaction,
detectSwap, OutputType
} from 'boltz-core';
import { randomBytes, createHash } from 'crypto';
import { ECPairFactory, ECPairInterface } from 'ecpair';
import * as ecc from 'tiny-secp256k1';
import { Transaction, address } from 'bitcoinjs-lib';
import ws from 'ws';
import { getLogger, PubLogger, ERROR } from '../../helpers/logger.js';
import { loggedGet, loggedPost, getNetwork } from './swapHelpers.js';
import { BTCNetwork } from '../../main/settings.js';
/* type InvoiceSwapFees = {
hash: string,
rate: number,
limits: {
maximal: number,
minimal: number,
maximalZeroConf: number
},
fees: {
percentage: number,
minerFees: number,
}
} */
type InvoiceSwapFees = {
percentage: number,
minerFees: number,
}
type InvoiceSwapFeesRes = {
BTC?: {
BTC?: {
fees: InvoiceSwapFees
}
}
}
type InvoiceSwapResponse = {
id: string, claimPublicKey: string, swapTree: string, timeoutBlockHeight: number,
expectedAmount: number, address: string
}
type InvoiceSwapInfo = { paymentHash: string, keys: ECPairInterface }
export type InvoiceSwapData = { createdResponse: InvoiceSwapResponse, info: InvoiceSwapInfo }
export class SubmarineSwaps {
private httpUrl: string
private wsUrl: string
private network: BTCNetwork
log: PubLogger
constructor({ httpUrl, wsUrl, network }: { httpUrl: string, wsUrl: string, network: BTCNetwork }) {
this.httpUrl = httpUrl
this.wsUrl = wsUrl
this.network = network
this.log = getLogger({ component: 'SubmarineSwaps' })
}
getHttpUrl = () => {
return this.httpUrl
}
getWsUrl = () => {
return this.wsUrl
}
GetFees = async (): Promise<{ ok: true, fees: InvoiceSwapFees, } | { ok: false, error: string }> => {
const url = `${this.httpUrl}/v2/swap/submarine`
const feesRes = await loggedGet<InvoiceSwapFeesRes>(this.log, url)
if (!feesRes.ok) {
return { ok: false, error: feesRes.error }
}
if (!feesRes.data.BTC?.BTC?.fees) {
return { ok: false, error: 'No fees found for BTC to BTC swap' }
}
return { ok: true, fees: feesRes.data.BTC.BTC.fees }
}
SwapInvoice = async (invoice: string): Promise<{ ok: true, createdResponse: InvoiceSwapResponse, pubkey: string, privKey: string } | { ok: false, error: string }> => {
const keys = ECPairFactory(ecc).makeRandom()
if (!keys.privateKey) {
return { ok: false, error: 'Failed to generate keys' }
}
const refundPublicKey = Buffer.from(keys.publicKey).toString('hex')
const req = { invoice, to: 'BTC', from: 'BTC', refundPublicKey }
const url = `${this.httpUrl}/v2/swap/submarine`
this.log('Sending invoice swap request to', url);
const createdResponseRes = await loggedPost<InvoiceSwapResponse>(this.log, url, req)
if (!createdResponseRes.ok) {
return createdResponseRes
}
const createdResponse = createdResponseRes.data
this.log('Created invoice swap');
this.log(createdResponse);
return {
ok: true, createdResponse,
pubkey: refundPublicKey,
privKey: Buffer.from(keys.privateKey).toString('hex')
}
}
/**
* Get the lockup transaction for a swap from Boltz
*/
private getLockupTransaction = async (swapId: string): Promise<{ ok: true, data: { hex: string } } | { ok: false, error: string }> => {
const url = `${this.httpUrl}/v2/swap/submarine/${swapId}/transaction`
return await loggedGet<{ hex: string }>(this.log, url)
}
/**
* Get partial refund signature from Boltz for cooperative refund
*/
private getPartialRefundSignature = async (
swapId: string,
pubNonce: Buffer,
transaction: Transaction,
index: number
): Promise<{ ok: true, data: { pubNonce: string, partialSignature: string } } | { ok: false, error: string }> => {
const url = `${this.httpUrl}/v2/swap/submarine/${swapId}/refund`
const req = {
index,
pubNonce: pubNonce.toString('hex'),
transaction: transaction.toHex()
}
return await loggedPost<{ pubNonce: string, partialSignature: string }>(this.log, url, req)
}
/**
* Constructs a Taproot refund transaction (cooperative or uncooperative)
*/
private constructTaprootRefund = async (
swapId: string,
claimPublicKey: string,
swapTree: string,
timeoutBlockHeight: number,
lockupTx: Transaction,
privateKey: ECPairInterface,
refundAddress: string,
feePerVbyte: number,
cooperative: boolean = true
): Promise<{
ok: true,
transaction: Transaction,
cooperativeError?: string
} | {
ok: false,
error: string
}> => {
this.log(`Constructing ${cooperative ? 'cooperative' : 'uncooperative'} Taproot refund for swap ${swapId}`)
const boltzPublicKey = Buffer.from(claimPublicKey, 'hex')
const swapTreeDeserialized = SwapTreeSerializer.deserializeSwapTree(swapTree)
// Create musig and tweak it
let musig = new Musig(await zkpInit(), privateKey, randomBytes(32), [
boltzPublicKey,
Buffer.from(privateKey.publicKey),
])
const tweakedKey = TaprootUtils.tweakMusig(musig, swapTreeDeserialized.tree)
// Detect the swap output in the lockup transaction
const swapOutput = detectSwap(tweakedKey, lockupTx)
if (!swapOutput) {
return { ok: false, error: 'Could not detect swap output in lockup transaction' }
}
const network = getNetwork(this.network)
// const decodedAddress = address.fromBech32(refundAddress)
const details = [
{
...swapOutput,
keys: privateKey,
cooperative,
type: OutputType.Taproot,
txHash: lockupTx.getHash(),
swapTree: swapTreeDeserialized,
internalKey: musig.getAggregatedPublicKey(),
}
]
const outputScript = address.toOutputScript(refundAddress, network)
// Construct the refund transaction
const refundTx = constructRefundTransaction(
details,
outputScript,
cooperative ? 0 : timeoutBlockHeight,
feePerVbyte,
true
)
if (!cooperative) {
return { ok: true, transaction: refundTx }
}
// For cooperative refund, get Boltz's partial signature
try {
musig = new Musig(await zkpInit(), privateKey, randomBytes(32), [
boltzPublicKey,
Buffer.from(privateKey.publicKey),
])
// Get the partial signature from Boltz
const boltzSigRes = await this.getPartialRefundSignature(
swapId,
Buffer.from(musig.getPublicNonce()),
refundTx,
0
)
if (!boltzSigRes.ok) {
this.log(ERROR, 'Failed to get Boltz partial signature, falling back to uncooperative refund')
// Fallback to uncooperative refund
return await this.constructTaprootRefund(
swapId,
claimPublicKey,
swapTree,
timeoutBlockHeight,
lockupTx,
privateKey,
refundAddress,
feePerVbyte,
false
)
}
const boltzSig = boltzSigRes.data
// Aggregate nonces
musig.aggregateNonces([
[boltzPublicKey, Musig.parsePubNonce(boltzSig.pubNonce)],
])
// Tweak musig again after aggregating nonces
TaprootUtils.tweakMusig(musig, swapTreeDeserialized.tree)
// Initialize session and sign
musig.initializeSession(
TaprootUtils.hashForWitnessV1(
details,
refundTx,
0
)
)
musig.signPartial()
musig.addPartial(boltzPublicKey, Buffer.from(boltzSig.partialSignature, 'hex'))
// Set the witness to the aggregated signature
refundTx.ins[0].witness = [musig.aggregatePartials()]
return { ok: true, transaction: refundTx }
} catch (error: any) {
this.log(ERROR, 'Cooperative refund failed:', error.message)
// Fallback to uncooperative refund
return await this.constructTaprootRefund(
swapId,
claimPublicKey,
swapTree,
timeoutBlockHeight,
lockupTx,
privateKey,
refundAddress,
feePerVbyte,
false
)
}
}
/**
* Broadcasts a refund transaction
*/
private broadcastRefundTransaction = async (transaction: Transaction): Promise<{ ok: true, txId: string } | { ok: false, error: string }> => {
const url = `${this.httpUrl}/v2/chain/BTC/transaction`
const req = { hex: transaction.toHex() }
const result = await loggedPost<{ id: string }>(this.log, url, req)
if (!result.ok) {
return result
}
return { ok: true, txId: result.data.id }
}
/**
* Refund a submarine swap
* @param swapId - The swap ID
* @param claimPublicKey - Boltz's claim public key
* @param swapTree - The swap tree
* @param timeoutBlockHeight - The timeout block height
* @param privateKey - The refund private key (hex string)
* @param refundAddress - The address to refund to
* @param currentHeight - The current block height
* @param lockupTxHex - The lockup transaction hex (optional, will fetch from Boltz if not provided)
* @param feePerVbyte - Fee rate in sat/vbyte (optional, will use default if not provided)
*/
RefundSwap = async (params: {
swapId: string,
claimPublicKey: string,
swapTree: string,
timeoutBlockHeight: number,
privateKeyHex: string,
refundAddress: string,
currentHeight: number,
lockupTxHex?: string,
feePerVbyte?: number
}): Promise<{ ok: true, publish: { done: false, txHex: string, txId: string } | { done: true, txId: string } } | { ok: false, error: string }> => {
const { swapId, claimPublicKey, swapTree, timeoutBlockHeight, privateKeyHex, refundAddress, currentHeight, lockupTxHex, feePerVbyte = 2 } = params
this.log('Starting refund process for swap:', swapId)
// Get the lockup transaction (from parameter or fetch from Boltz)
let lockupTx: Transaction
if (lockupTxHex) {
this.log('Using provided lockup transaction hex')
lockupTx = Transaction.fromHex(lockupTxHex)
} else {
this.log('Fetching lockup transaction from Boltz')
const lockupTxRes = await this.getLockupTransaction(swapId)
if (!lockupTxRes.ok) {
return { ok: false, error: `Failed to get lockup transaction: ${lockupTxRes.error}` }
}
lockupTx = Transaction.fromHex(lockupTxRes.data.hex)
}
this.log('Lockup transaction retrieved:', lockupTx.getId())
// Check if swap has timed out
if (currentHeight < timeoutBlockHeight) {
return {
ok: false,
error: `Swap has not timed out yet. Current height: ${currentHeight}, timeout: ${timeoutBlockHeight}`
}
}
this.log(`Swap has timed out. Current height: ${currentHeight}, timeout: ${timeoutBlockHeight}`)
// Parse the private key
const privateKey = ECPairFactory(ecc).fromPrivateKey(Buffer.from(privateKeyHex, 'hex'))
// Construct the refund transaction (tries cooperative first, then falls back to uncooperative)
const refundTxRes = await this.constructTaprootRefund(
swapId,
claimPublicKey,
swapTree,
timeoutBlockHeight,
lockupTx,
privateKey,
refundAddress,
feePerVbyte,
true // Try cooperative first
)
if (!refundTxRes.ok) {
return { ok: false, error: refundTxRes.error }
}
const cooperative = !refundTxRes.cooperativeError
this.log(`Refund transaction constructed (${cooperative ? 'cooperative' : 'uncooperative'}):`, refundTxRes.transaction.getId())
if (!cooperative) {
return { ok: true, publish: { done: false, txHex: refundTxRes.transaction.toHex(), txId: refundTxRes.transaction.getId() } }
}
// Broadcast the refund transaction
const broadcastRes = await this.broadcastRefundTransaction(refundTxRes.transaction)
if (!broadcastRes.ok) {
return { ok: false, error: `Failed to broadcast refund transaction: ${broadcastRes.error}` }
}
this.log('Refund transaction broadcasted successfully:', broadcastRes.txId)
return { ok: true, publish: { done: true, txId: broadcastRes.txId } }
}
SubscribeToInvoiceSwap = (data: InvoiceSwapData, swapDone: (result: { ok: true } | { ok: false, error: string }) => void, waitingTx: () => void) => {
this.log("subscribing to invoice swap", { id: data.createdResponse.id })
const webSocket = new ws(`${this.wsUrl}/v2/ws`)
const subReq = { op: 'subscribe', channel: 'swap.update', args: [data.createdResponse.id] }
webSocket.on('open', () => {
webSocket.send(JSON.stringify(subReq))
})
let isDone = false
const done = (failureReason?: string) => {
isDone = true
webSocket.close()
if (failureReason) {
swapDone({ ok: false, error: failureReason })
} else {
swapDone({ ok: true })
}
}
webSocket.on('error', (err) => {
this.log(ERROR, 'Error in WebSocket', err.message)
})
webSocket.on('close', () => {
if (!isDone) {
this.log(ERROR, 'WebSocket closed before swap was done');
done('WebSocket closed before swap was done')
}
})
webSocket.on('message', async (rawMsg) => {
try {
await this.handleSwapInvoiceMessage(rawMsg, data, done, waitingTx)
} catch (err: any) {
this.log(ERROR, 'Error handling invoice WebSocket message', err.message)
webSocket.close()
return
}
});
return () => {
webSocket.close()
}
}
handleSwapInvoiceMessage = async (rawMsg: ws.RawData, data: InvoiceSwapData, closeWebSocket: (failureReason?: string) => void, waitingTx: () => void) => {
const msg = JSON.parse(rawMsg.toString('utf-8'));
if (msg.event !== 'update') {
return;
}
this.log('Got invoice WebSocket update');
this.log(msg);
switch (msg.args[0].status) {
// "invoice.set" means Boltz is waiting for an onchain transaction to be sent
case 'invoice.set':
this.log('Waiting for onchain transaction');
waitingTx()
return;
// Create a partial signature to allow Boltz to do a key path spend to claim the mainchain coins
case 'transaction.claim.pending':
await this.handleInvoiceClaimPending(data)
return;
case 'transaction.claimed':
this.log('Invoice swap successful');
closeWebSocket()
return;
case 'swap.expired':
case 'transaction.lockupFailed':
case 'invoice.failedToPay':
closeWebSocket(`swap ${data.createdResponse.id} failed with status ${msg.args[0].status}`)
return;
default:
this.log('Unknown swap invoice WebSocket message', msg)
return;
}
}
handleInvoiceClaimPending = async (data: InvoiceSwapData) => {
this.log('Creating cooperative claim transaction');
const { createdResponse, info } = data
const { paymentHash, keys } = info
// Get the information request to create a partial signature
const url = `${this.httpUrl}/v2/swap/submarine/${createdResponse.id}/claim`
const claimTxDetailsRes = await loggedGet<{ preimage: string, transactionHash: string, pubNonce: string }>(this.log, url)
if (!claimTxDetailsRes.ok) {
return claimTxDetailsRes
}
const claimTxDetails = claimTxDetailsRes.data
// Verify that Boltz actually paid the invoice by comparing the preimage hash
// of the invoice to the SHA256 hash of the preimage from the response
const claimTxPreimageHash = createHash('sha256').update(Buffer.from(claimTxDetails.preimage, 'hex')).digest()
const invoicePreimageHash = Buffer.from(paymentHash, 'hex')
if (!claimTxPreimageHash.equals(invoicePreimageHash)) {
this.log(ERROR, 'Boltz provided invalid preimage');
return;
}
const boltzPublicKey = Buffer.from(createdResponse.claimPublicKey, 'hex')
// Create a musig signing instance
const musig = new Musig(await zkpInit(), keys, randomBytes(32), [
boltzPublicKey,
Buffer.from(keys.publicKey),
]);
// Tweak that musig with the Taptree of the swap scripts
TaprootUtils.tweakMusig(
musig,
SwapTreeSerializer.deserializeSwapTree(createdResponse.swapTree).tree,
);
// Aggregate the nonces
musig.aggregateNonces([
[boltzPublicKey, Buffer.from(claimTxDetails.pubNonce, 'hex')],
]);
// Initialize the session to sign the transaction hash from the response
musig.initializeSession(
Buffer.from(claimTxDetails.transactionHash, 'hex'),
);
// Give our public nonce and the partial signature to Boltz
const claimUrl = `${this.httpUrl}/v2/swap/submarine/${createdResponse.id}/claim`
const claimReq = {
pubNonce: Buffer.from(musig.getPublicNonce()).toString('hex'),
partialSignature: Buffer.from(musig.signPartial()).toString('hex'),
}
const claimResponseRes = await loggedPost<{ pubNonce: string, partialSignature: string }>(this.log, claimUrl, claimReq)
if (!claimResponseRes.ok) {
return claimResponseRes
}
const claimResponse = claimResponseRes.data
this.log('Claim response', claimResponse)
}
}

View file

@ -0,0 +1,50 @@
import axios from 'axios';
import { Network } from 'bitcoinjs-lib';
// import bolt11 from 'bolt11';
import {
Networks,
} from 'boltz-core';
import { PubLogger, ERROR } from '../../helpers/logger.js';
import { BTCNetwork } from '../../main/settings.js';
export const loggedPost = async <T>(log: PubLogger, url: string, req: any): Promise<{ ok: true, data: T } | { ok: false, error: string }> => {
try {
const { data } = await axios.post(url, req)
return { ok: true, data: data as T }
} catch (err: any) {
if (err.response?.data) {
log(ERROR, 'Error sending request', err.response.data)
return { ok: false, error: JSON.stringify(err.response.data) }
}
log(ERROR, 'Error sending request', err.message)
return { ok: false, error: err.message }
}
}
export const loggedGet = async <T>(log: PubLogger, url: string): Promise<{ ok: true, data: T } | { ok: false, error: string }> => {
try {
const { data } = await axios.get(url)
return { ok: true, data: data as T }
} catch (err: any) {
if (err.response?.data) {
log(ERROR, 'Error getting request', err.response.data)
return { ok: false, error: err.response.data }
}
log(ERROR, 'Error getting request', err.message)
return { ok: false, error: err.message }
}
}
export const getNetwork = (network: BTCNetwork): Network => {
switch (network) {
case 'mainnet':
return Networks.bitcoinMainnet
case 'testnet':
return Networks.bitcoinTestnet
case 'regtest':
return Networks.bitcoinRegtest
default:
throw new Error(`Invalid network: ${network}`)
}
}

View file

@ -0,0 +1,431 @@
import { ECPairFactory } from 'ecpair';
import * as ecc from 'tiny-secp256k1';
import { getLogger } from '../../helpers/logger.js';
import SettingsManager from '../../main/settingsManager.js';
import * as Types from '../../../../proto/autogenerated/ts/types.js';
import Storage from '../../storage/index.js';
import LND from '../lnd.js';
import { UserInvoicePayment } from '../../storage/entity/UserInvoicePayment.js';
import { ReverseSwaps, TransactionSwapData } from './reverseSwaps.js';
import { SubmarineSwaps, InvoiceSwapData } from './submarineSwaps.js';
import { InvoiceSwap } from '../../storage/entity/InvoiceSwap.js';
export class Swaps {
settings: SettingsManager
revSwappers: Record<string, ReverseSwaps>
subSwappers: Record<string, SubmarineSwaps>
storage: Storage
lnd: LND
waitingSwaps: Record<string, boolean> = {}
log = getLogger({ component: 'swaps' })
constructor(settings: SettingsManager, storage: Storage) {
this.settings = settings
this.revSwappers = {}
this.subSwappers = {}
const network = settings.getSettings().lndSettings.network
const { boltzHttpUrl, boltzWebSocketUrl, boltsHttpUrlAlt, boltsWebSocketUrlAlt } = settings.getSettings().swapsSettings
if (boltzHttpUrl && boltzWebSocketUrl) {
this.revSwappers[boltzHttpUrl] = new ReverseSwaps({ httpUrl: boltzHttpUrl, wsUrl: boltzWebSocketUrl, network })
this.subSwappers[boltzHttpUrl] = new SubmarineSwaps({ httpUrl: boltzHttpUrl, wsUrl: boltzWebSocketUrl, network })
}
if (boltsHttpUrlAlt && boltsWebSocketUrlAlt) {
this.revSwappers[boltsHttpUrlAlt] = new ReverseSwaps({ httpUrl: boltsHttpUrlAlt, wsUrl: boltsWebSocketUrlAlt, network })
this.subSwappers[boltsHttpUrlAlt] = new SubmarineSwaps({ httpUrl: boltsHttpUrlAlt, wsUrl: boltsWebSocketUrlAlt, network })
}
this.storage = storage
}
SetLnd = (lnd: LND) => {
this.lnd = lnd
}
Stop = () => { }
GetKeys = (privateKey: string) => {
const keys = ECPairFactory(ecc).fromPrivateKey(Buffer.from(privateKey, 'hex'))
return keys
}
GetInvoiceSwapQuotes = async (appUserId: string, invoice: string): Promise<Types.InvoiceSwapQuote[]> => {
if (!this.settings.getSettings().swapsSettings.enableSwaps) {
throw new Error("Swaps are not enabled")
}
const swappers = Object.values(this.subSwappers)
if (swappers.length === 0) {
throw new Error("No swap services available")
}
const res = await Promise.allSettled(swappers.map(sw => this.getInvoiceSwapQuote(sw, appUserId, invoice)))
const failures: string[] = []
const success: Types.InvoiceSwapQuote[] = []
for (const r of res) {
if (r.status === 'fulfilled') {
success.push(r.value)
} else {
failures.push(r.reason.message ? r.reason.message : r.reason.toString())
}
}
if (success.length === 0) {
throw new Error(failures.join("\n"))
}
return success
}
ListInvoiceSwaps = async (appUserId: string): Promise<Types.InvoiceSwapsList> => {
const completedSwaps = await this.storage.paymentStorage.ListCompletedInvoiceSwaps(appUserId)
const pendingSwaps = await this.storage.paymentStorage.ListPendingInvoiceSwaps(appUserId)
return {
swaps: completedSwaps.map(s => {
return {
invoice_paid: s.invoice,
swap_operation_id: s.swap_operation_id,
failure_reason: s.failure_reason,
tx_id: s.tx_id,
}
}),
quotes: pendingSwaps.map(s => {
return {
swap_operation_id: s.swap_operation_id,
invoice: s.invoice,
invoice_amount_sats: s.invoice_amount,
address: s.address,
transaction_amount_sats: s.transaction_amount,
chain_fee_sats: s.chain_fee_sats,
service_fee_sats: 0,
service_url: s.service_url,
swap_fee_sats: s.swap_fee_sats,
tx_id: s.tx_id,
}
})
}
}
RefundInvoiceSwap = async (swapOperationId: string, satPerVByte: number, refundAddress: string, currentHeight: number): Promise<{ published: false, txHex: string, txId: string } | { published: true, txId: string }> => {
this.log("refunding invoice swap", { swapOperationId, satPerVByte, refundAddress, currentHeight })
const swap = await this.storage.paymentStorage.GetRefundableInvoiceSwap(swapOperationId)
if (!swap) {
throw new Error("Swap not found or already used")
}
const swapper = this.subSwappers[swap.service_url]
if (!swapper) {
throw new Error("swapper service not found")
}
const result = await swapper.RefundSwap({
swapId: swap.swap_quote_id,
claimPublicKey: swap.claim_public_key,
currentHeight,
privateKeyHex: swap.ephemeral_private_key,
refundAddress,
swapTree: swap.swap_tree,
timeoutBlockHeight: swap.timeout_block_height,
feePerVbyte: satPerVByte,
lockupTxHex: swap.lockup_tx_hex,
})
if (!result.ok) {
throw new Error(result.error)
}
if (result.publish.done) {
return { published: true, txId: result.publish.txId }
}
return { published: false, txHex: result.publish.txHex, txId: result.publish.txId }
}
PayInvoiceSwap = async (appUserId: string, swapOpId: string, satPerVByte: number, payAddress: (address: string, amt: number) => Promise<{ txId: string }>): Promise<void> => {
this.log("paying invoice swap", { appUserId, swapOpId, satPerVByte })
if (!this.settings.getSettings().swapsSettings.enableSwaps) {
throw new Error("Swaps are not enabled")
}
if (!swapOpId) {
throw new Error("swap operation id is required")
}
if (!satPerVByte) {
throw new Error("sat per v byte is required")
}
const swap = await this.storage.paymentStorage.GetInvoiceSwap(swapOpId, appUserId)
if (!swap) {
throw new Error("swap not found")
}
const swapper = this.subSwappers[swap.service_url]
if (!swapper) {
throw new Error("swapper service not found")
}
if (this.waitingSwaps[swapOpId]) {
throw new Error("swap already in progress")
}
this.waitingSwaps[swapOpId] = true
const data = this.getInvoiceSwapData(swap)
let txId = ""
const close = swapper.SubscribeToInvoiceSwap(data, async (result) => {
if (result.ok) {
await this.storage.paymentStorage.FinalizeInvoiceSwap(swapOpId)
this.log("invoice swap completed", { swapOpId, txId })
} else {
await this.storage.paymentStorage.FailInvoiceSwap(swapOpId, result.error, txId)
this.log("invoice swap failed", { swapOpId, error: result.error })
}
}, () => payAddress(swap.address, swap.transaction_amount)
.then(res => { txId = res.txId })
.catch(err => { close(); this.log("error paying address", err.message || err) }))
}
ResumeInvoiceSwaps = async () => {
this.log("resuming invoice swaps")
const swaps = await this.storage.paymentStorage.ListUnfinishedInvoiceSwaps()
this.log("resuming", swaps.length, "invoice swaps")
for (const swap of swaps) {
try {
this.resumeInvoiceSwap(swap)
} catch (err: any) {
this.log("error resuming invoice swap", err.message || err)
}
}
}
private resumeInvoiceSwap = (swap: InvoiceSwap) => {
// const swap = await this.storage.paymentStorage.GetInvoiceSwap(swapOpId, appUserId)
if (!swap || !swap.tx_id || swap.used) {
throw new Error("swap to resume not found, or does not have a tx id")
}
const swapper = this.subSwappers[swap.service_url]
if (!swapper) {
throw new Error("swapper service not found")
}
const data = this.getInvoiceSwapData(swap)
swapper.SubscribeToInvoiceSwap(data, async (result) => {
if (result.ok) {
await this.storage.paymentStorage.FinalizeInvoiceSwap(swap.swap_operation_id)
this.log("invoice swap completed", { swapOpId: swap.swap_operation_id, txId: swap.tx_id })
} else {
await this.storage.paymentStorage.FailInvoiceSwap(swap.swap_operation_id, result.error)
this.log("invoice swap failed", { swapOpId: swap.swap_operation_id, error: result.error })
}
}, () => { throw new Error("swap tx already paid") })
}
private getInvoiceSwapData = (swap: InvoiceSwap) => {
return {
createdResponse: {
address: swap.address,
claimPublicKey: swap.claim_public_key,
id: swap.swap_quote_id,
swapTree: swap.swap_tree,
timeoutBlockHeight: swap.timeout_block_height,
expectedAmount: swap.transaction_amount,
},
info: {
keys: this.GetKeys(swap.ephemeral_private_key),
paymentHash: swap.payment_hash,
}
}
}
private async getInvoiceSwapQuote(swapper: SubmarineSwaps, appUserId: string, invoice: string): Promise<Types.InvoiceSwapQuote> {
const feesRes = await swapper.GetFees()
if (!feesRes.ok) {
throw new Error(feesRes.error)
}
const decoded = await this.lnd.DecodeInvoice(invoice)
const amt = decoded.numSatoshis
const fee = Math.ceil((feesRes.fees.percentage / 100) * amt) + feesRes.fees.minerFees
const res = await swapper.SwapInvoice(invoice)
if (!res.ok) {
throw new Error(res.error)
}
const newSwap = await this.storage.paymentStorage.AddInvoiceSwap({
app_user_id: appUserId,
swap_quote_id: res.createdResponse.id,
swap_tree: JSON.stringify(res.createdResponse.swapTree),
timeout_block_height: res.createdResponse.timeoutBlockHeight,
ephemeral_public_key: res.pubkey,
ephemeral_private_key: res.privKey,
invoice: invoice,
invoice_amount: amt,
transaction_amount: res.createdResponse.expectedAmount,
swap_fee_sats: fee,
chain_fee_sats: 0,
service_url: swapper.getHttpUrl(),
address: res.createdResponse.address,
claim_public_key: res.createdResponse.claimPublicKey,
payment_hash: decoded.paymentHash,
})
return {
swap_operation_id: newSwap.swap_operation_id,
invoice: invoice,
invoice_amount_sats: amt,
address: res.createdResponse.address,
transaction_amount_sats: res.createdResponse.expectedAmount,
chain_fee_sats: 0,
service_fee_sats: 0,
service_url: swapper.getHttpUrl(),
swap_fee_sats: fee,
tx_id: newSwap.tx_id,
}
}
ListTxSwaps = async (appUserId: string, payments: UserInvoicePayment[], newOp: (p: UserInvoicePayment) => Types.UserOperation | undefined, getServiceFee: (amt: number) => number): Promise<Types.TxSwapsList> => {
const completedSwaps = await this.storage.paymentStorage.ListCompletedTxSwaps(appUserId, payments)
const pendingSwaps = await this.storage.paymentStorage.ListPendingTransactionSwaps(appUserId)
return {
swaps: completedSwaps.map(s => {
const p = s.payment
const op = p ? newOp(p) : undefined
return {
operation_payment: op,
swap_operation_id: s.swap.swap_operation_id,
address_paid: s.swap.address_paid,
failure_reason: s.swap.failure_reason,
}
}),
quotes: pendingSwaps.map(s => {
const serviceFee = getServiceFee(s.invoice_amount)
return {
swap_operation_id: s.swap_operation_id,
invoice_amount_sats: s.invoice_amount,
transaction_amount_sats: s.transaction_amount,
chain_fee_sats: s.chain_fee_sats,
service_fee_sats: serviceFee,
swap_fee_sats: s.swap_fee_sats,
service_url: s.service_url,
}
})
}
}
GetTxSwapQuotes = async (appUserId: string, amt: number, getServiceFee: (decodedAmt: number) => number): Promise<Types.TransactionSwapQuote[]> => {
if (!this.settings.getSettings().swapsSettings.enableSwaps) {
throw new Error("Swaps are not enabled")
}
const swappers = Object.values(this.revSwappers)
if (swappers.length === 0) {
throw new Error("No swap services available")
}
const res = await Promise.allSettled(swappers.map(sw => this.getTxSwapQuote(sw, appUserId, amt, getServiceFee)))
const failures: string[] = []
const success: Types.TransactionSwapQuote[] = []
for (const r of res) {
if (r.status === 'fulfilled') {
success.push(r.value)
} else {
failures.push(r.reason.message ? r.reason.message : r.reason.toString())
}
}
if (success.length === 0) {
throw new Error(failures.join("\n"))
}
return success
}
private async getTxSwapQuote(swapper: ReverseSwaps, appUserId: string, amt: number, getServiceFee: (decodedAmt: number) => number): Promise<Types.TransactionSwapQuote> {
this.log("getting transaction swap quote")
const feesRes = await swapper.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 swapper.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,
service_url: swapper.getHttpUrl(),
})
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,
service_url: swapper.getHttpUrl(),
}
}
async PayAddrWithSwap(appUserId: string, swapOpId: string, address: string, payInvoice: (invoice: string, amt: number) => Promise<void>) {
if (!this.settings.getSettings().swapsSettings.enableSwaps) {
throw new Error("Swaps are not enabled")
}
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 swapper = this.revSwappers[txSwap.service_url]
if (!swapper) {
throw new Error("swapper service not found")
}
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 }
swapper.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
}
}
}

View file

@ -5,7 +5,8 @@ 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";
import { Swaps } from "../lnd/swaps/swaps.js";
import { defaultInvoiceExpiry } from "../storage/paymentStorage.js";
export class AdminManager {
settings: SettingsManager
storage: Storage
@ -260,15 +261,62 @@ export class AdminManager {
}
}
async ListAdminSwaps(): Promise<Types.SwapsList> {
return this.swaps.ListSwaps("admin", [], p => undefined, amt => 0)
async ListAdminInvoiceSwaps(): Promise<Types.InvoiceSwapsList> {
return this.swaps.ListInvoiceSwaps("admin")
}
async GetAdminInvoiceSwapQuotes(req: Types.InvoiceSwapRequest): Promise<Types.InvoiceSwapQuoteList> {
const invoice = await this.lnd.NewInvoice(req.amount_sats, "Admin Swap", defaultInvoiceExpiry, { useProvider: false, from: 'system' })
const quotes = await this.swaps.GetInvoiceSwapQuotes("admin", invoice.payRequest)
return { quotes }
}
async PayAdminInvoiceSwap(req: Types.PayAdminInvoiceSwapRequest): Promise<Types.AdminInvoiceSwapResponse> {
const txId = await new Promise<string>(res => {
this.swaps.PayInvoiceSwap("admin", req.swap_operation_id, req.sat_per_v_byte, async (addr, amt) => {
const tx = await this.lnd.PayAddress(addr, amt, req.sat_per_v_byte, "", { useProvider: false, from: 'system' })
this.log("paid admin invoice swap", { swapOpId: req.swap_operation_id, txId: tx.txid })
await this.storage.metricsStorage.AddRootOperation("chain_payment", txId, amt)
// Fetch the full transaction hex for potential refunds
let lockupTxHex: string | undefined
try {
const txDetails = await this.lnd.GetTx(tx.txid)
lockupTxHex = txDetails.rawTxHex
} catch (err: any) {
this.log("Warning: Could not fetch transaction hex for refund purposes:", err.message)
}
await this.storage.paymentStorage.SetInvoiceSwapTxId(req.swap_operation_id, txId, lockupTxHex)
this.log("saved admin swap txid", { swapOpId: req.swap_operation_id, txId })
res(tx.txid)
return { txId: tx.txid }
})
})
return { tx_id: txId }
}
async RefundAdminInvoiceSwap(req: Types.RefundAdminInvoiceSwapRequest): Promise<Types.AdminInvoiceSwapResponse> {
const info = await this.lnd.GetInfo()
const currentHeight = info.blockHeight
const address = await this.lnd.NewAddress(Types.AddressType.WITNESS_PUBKEY_HASH, { useProvider: false, from: 'system' })
const result = await this.swaps.RefundInvoiceSwap(req.swap_operation_id, req.sat_per_v_byte, address.address, currentHeight)
if (result.published) {
return { tx_id: result.txId }
}
await this.lnd.PublishTransaction(result.txHex)
return { tx_id: result.txId }
}
async ListAdminTxSwaps(): Promise<Types.TxSwapsList> {
return this.swaps.ListTxSwaps("admin", [], p => undefined, amt => 0)
}
async GetAdminTransactionSwapQuotes(req: Types.TransactionSwapRequest): Promise<Types.TransactionSwapQuoteList> {
const quotes = await this.swaps.GetTxSwapQuotes("admin", req.transaction_amount_sats, () => 0)
return { quotes }
}
async PayAdminTransactionSwap(req: Types.PayAdminTransactionSwapRequest): Promise<Types.AdminSwapResponse> {
async PayAdminTransactionSwap(req: Types.PayAdminTransactionSwapRequest): Promise<Types.AdminTxSwapResponse> {
const routingFloor = this.settings.getSettings().lndSettings.routingFeeFloor
const routingLimit = this.settings.getSettings().lndSettings.routingFeeLimitBps / 10000

View file

@ -163,6 +163,8 @@ export default class {
let log = getLogger({})
this.storage.paymentStorage.DeleteExpiredTransactionSwaps(height)
.catch(err => log(ERROR, "failed to delete expired transaction swaps", err.message || err))
this.storage.paymentStorage.DeleteExpiredInvoiceSwaps(height)
.catch(err => log(ERROR, "failed to delete expired invoice swaps", err.message || err))
try {
const balanceEvents = await this.paymentManager.GetLndBalance()
if (!skipMetrics) {

View file

@ -11,7 +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"
import { Swaps } from "../lnd/swaps/swaps.js"
export type AppData = {
privateKey: string;
publicKey: string;
@ -79,6 +79,7 @@ export const initMainHandler = async (log: PubLogger, settingsManager: SettingsM
await mainHandler.paymentManager.CleanupOldUnpaidInvoices()
await mainHandler.appUserManager.CleanupInactiveUsers()
await mainHandler.appUserManager.CleanupNeverActiveUsers()
await swaps.ResumeInvoiceSwaps()
await mainHandler.paymentManager.watchDog.Start()
return { mainHandler, apps, localProviderClient, wizard, adminManager }
}

View file

@ -18,7 +18,7 @@ import { LiquidityManager } from './liquidityManager.js'
import { Utils } from '../helpers/utilsWrapper.js'
import { UserInvoicePayment } from '../storage/entity/UserInvoicePayment.js'
import SettingsManager from './settingsManager.js'
import { Swaps, TransactionSwapData } from '../lnd/swaps.js'
import { Swaps } from '../lnd/swaps/swaps.js'
import { Transaction, OutputDetail } from '../../../proto/lnd/lightning.js'
import { LndAddress } from '../lnd/lnd.js'
import Metrics from '../metrics/index.js'
@ -634,11 +634,11 @@ export default class {
}
}
async ListSwaps(ctx: Types.UserContext): Promise<Types.SwapsList> {
const payments = await this.storage.paymentStorage.ListSwapPayments(ctx.app_user_id)
async ListTxSwaps(ctx: Types.UserContext): Promise<Types.TxSwapsList> {
const payments = await this.storage.paymentStorage.ListTxSwapPayments(ctx.app_user_id)
const app = await this.storage.applicationStorage.GetApplication(ctx.app_id)
const isManagedUser = ctx.user_id !== app.owner.user_id
return this.swaps.ListSwaps(ctx.app_user_id, payments, p => {
return this.swaps.ListTxSwaps(ctx.app_user_id, payments, p => {
const opId = `${Types.UserOperationType.OUTGOING_TX}-${p.serial_id}`
return this.newInvoicePaymentOperation({ amount: p.paid_amount, confirmed: p.paid_at_unix !== 0, invoice: p.invoice, opId, networkFee: p.routing_fees, serviceFee: p.service_fees, paidAtUnix: p.paid_at_unix })
}, amt => this.getSendServiceFee(Types.UserOperationType.OUTGOING_INVOICE, amt, isManagedUser))

View file

@ -106,6 +106,33 @@ export default (mainHandler: Main): Types.ServerMethods => {
if (err != null) throw new Error(err.message)
return mainHandler.adminManager.PayAdminTransactionSwap(req)
},
ListAdminTxSwaps: async ({ ctx }) => {
return mainHandler.adminManager.ListAdminTxSwaps()
},
GetAdminInvoiceSwapQuotes: async ({ ctx, req }) => {
const err = Types.InvoiceSwapRequestValidate(req, {
amount_sats_CustomCheck: amt => amt > 0
})
if (err != null) throw new Error(err.message)
return mainHandler.adminManager.GetAdminInvoiceSwapQuotes(req)
},
RefundAdminInvoiceSwap: async ({ ctx, req }) => {
const err = Types.RefundAdminInvoiceSwapRequestValidate(req, {
swap_operation_id_CustomCheck: id => id !== '',
})
if (err != null) throw new Error(err.message)
return mainHandler.adminManager.RefundAdminInvoiceSwap(req)
},
ListAdminInvoiceSwaps: async ({ ctx }) => {
return mainHandler.adminManager.ListAdminInvoiceSwaps()
},
PayAdminInvoiceSwap: async ({ ctx, req }) => {
const err = Types.PayAdminInvoiceSwapRequestValidate(req, {
swap_operation_id_CustomCheck: id => id !== '',
})
if (err != null) throw new Error(err.message)
return mainHandler.adminManager.PayAdminInvoiceSwap(req)
},
GetProvidersDisruption: async () => {
return mainHandler.metricsManager.GetProvidersDisruption()
},
@ -145,9 +172,7 @@ export default (mainHandler: Main): Types.ServerMethods => {
GetUserOperations: async ({ ctx, req }) => {
return mainHandler.paymentManager.GetUserOperations(ctx.user_id, req)
},
ListAdminSwaps: async ({ ctx }) => {
return mainHandler.adminManager.ListAdminSwaps()
},
GetPaymentState: async ({ ctx, req }) => {
const err = Types.GetPaymentStateRequestValidate(req, {
invoice_CustomCheck: invoice => invoice !== ""
@ -165,8 +190,8 @@ export default (mainHandler: Main): Types.ServerMethods => {
if (err != null) throw new Error(err.message)
return mainHandler.paymentManager.PayAddress(ctx, req)
},
ListSwaps: async ({ ctx }) => {
return mainHandler.paymentManager.ListSwaps(ctx)
ListTxSwaps: async ({ ctx }) => {
return mainHandler.paymentManager.ListTxSwaps(ctx)
},
GetTransactionSwapQuotes: async ({ ctx, req }) => {
return mainHandler.paymentManager.GetTransactionSwapQuotes(ctx, req)

View file

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

View file

@ -0,0 +1,88 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, ManyToOne, JoinColumn, UpdateDateColumn } from "typeorm";
import { User } from "./User";
@Entity()
export class InvoiceSwap {
@PrimaryGeneratedColumn('uuid')
swap_operation_id: string
@Column()
app_user_id: string
@Column()
swap_quote_id: string
@Column()
swap_tree: string
@Column()
claim_public_key: string
@Column()
payment_hash: string
/* @Column()
lockup_address: string */
/* @Column()
refund_public_key: string */
@Column()
timeout_block_height: number
@Column()
invoice: string
@Column()
invoice_amount: number
@Column()
transaction_amount: number
@Column()
swap_fee_sats: number
@Column()
chain_fee_sats: number
@Column()
ephemeral_public_key: string
@Column()
address: string
// the private key is used on to perform a swap, it does not hold any funds once the swap is completed
// the swap should only last a few seconds, so it is not a security risk to store the private key in the database
// the key is stored here mostly for recovery purposes, in case something goes wrong with the swap
@Column()
ephemeral_private_key: string
@Column({ default: false })
used: boolean
@Column({ default: "" })
preimage: string
@Column({ default: "" })
failure_reason: string
@Column({ default: "" })
tx_id: string
@Column({ default: "", type: "text" })
lockup_tx_hex: string
/* @Column({ default: "" })
address_paid: string */
@Column({ default: "" })
service_url: string
@CreateDateColumn()
created_at: Date
@UpdateDateColumn()
updated_at: Date
}

View file

@ -0,0 +1,14 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class InvoiceSwaps1769529793283 implements MigrationInterface {
name = 'InvoiceSwaps1769529793283'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "invoice_swap" ("swap_operation_id" varchar PRIMARY KEY NOT NULL, "app_user_id" varchar NOT NULL, "swap_quote_id" varchar NOT NULL, "swap_tree" varchar NOT NULL, "claim_public_key" varchar NOT NULL, "payment_hash" varchar NOT NULL, "timeout_block_height" integer NOT NULL, "invoice" varchar NOT NULL, "invoice_amount" integer NOT NULL, "transaction_amount" integer NOT NULL, "swap_fee_sats" integer NOT NULL, "chain_fee_sats" integer NOT NULL, "ephemeral_public_key" varchar NOT NULL, "address" varchar NOT NULL, "ephemeral_private_key" varchar NOT NULL, "used" boolean NOT NULL DEFAULT (0), "preimage" varchar NOT NULL DEFAULT (''), "failure_reason" varchar NOT NULL DEFAULT (''), "tx_id" varchar NOT NULL DEFAULT (''), "service_url" varchar NOT NULL DEFAULT (''), "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')))`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE "invoice_swap"`);
}
}

View file

@ -0,0 +1,21 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class InvoiceSwapsFixes1769805357459 implements MigrationInterface {
name = 'InvoiceSwapsFixes1769805357459'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "temporary_invoice_swap" ("swap_operation_id" varchar PRIMARY KEY NOT NULL, "app_user_id" varchar NOT NULL, "swap_quote_id" varchar NOT NULL, "swap_tree" varchar NOT NULL, "claim_public_key" varchar NOT NULL, "payment_hash" varchar NOT NULL, "timeout_block_height" integer NOT NULL, "invoice" varchar NOT NULL, "invoice_amount" integer NOT NULL, "transaction_amount" integer NOT NULL, "swap_fee_sats" integer NOT NULL, "chain_fee_sats" integer NOT NULL, "ephemeral_public_key" varchar NOT NULL, "address" varchar NOT NULL, "ephemeral_private_key" varchar NOT NULL, "used" boolean NOT NULL DEFAULT (0), "preimage" varchar NOT NULL DEFAULT (''), "failure_reason" varchar NOT NULL DEFAULT (''), "tx_id" varchar NOT NULL DEFAULT (''), "service_url" varchar NOT NULL DEFAULT (''), "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')), "lockup_tx_hex" text NOT NULL DEFAULT (''))`);
await queryRunner.query(`INSERT INTO "temporary_invoice_swap"("swap_operation_id", "app_user_id", "swap_quote_id", "swap_tree", "claim_public_key", "payment_hash", "timeout_block_height", "invoice", "invoice_amount", "transaction_amount", "swap_fee_sats", "chain_fee_sats", "ephemeral_public_key", "address", "ephemeral_private_key", "used", "preimage", "failure_reason", "tx_id", "service_url", "created_at", "updated_at") SELECT "swap_operation_id", "app_user_id", "swap_quote_id", "swap_tree", "claim_public_key", "payment_hash", "timeout_block_height", "invoice", "invoice_amount", "transaction_amount", "swap_fee_sats", "chain_fee_sats", "ephemeral_public_key", "address", "ephemeral_private_key", "used", "preimage", "failure_reason", "tx_id", "service_url", "created_at", "updated_at" FROM "invoice_swap"`);
await queryRunner.query(`DROP TABLE "invoice_swap"`);
await queryRunner.query(`ALTER TABLE "temporary_invoice_swap" RENAME TO "invoice_swap"`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "invoice_swap" RENAME TO "temporary_invoice_swap"`);
await queryRunner.query(`CREATE TABLE "invoice_swap" ("swap_operation_id" varchar PRIMARY KEY NOT NULL, "app_user_id" varchar NOT NULL, "swap_quote_id" varchar NOT NULL, "swap_tree" varchar NOT NULL, "claim_public_key" varchar NOT NULL, "payment_hash" varchar NOT NULL, "timeout_block_height" integer NOT NULL, "invoice" varchar NOT NULL, "invoice_amount" integer NOT NULL, "transaction_amount" integer NOT NULL, "swap_fee_sats" integer NOT NULL, "chain_fee_sats" integer NOT NULL, "ephemeral_public_key" varchar NOT NULL, "address" varchar NOT NULL, "ephemeral_private_key" varchar NOT NULL, "used" boolean NOT NULL DEFAULT (0), "preimage" varchar NOT NULL DEFAULT (''), "failure_reason" varchar NOT NULL DEFAULT (''), "tx_id" varchar NOT NULL DEFAULT (''), "service_url" varchar NOT NULL DEFAULT (''), "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')))`);
await queryRunner.query(`INSERT INTO "invoice_swap"("swap_operation_id", "app_user_id", "swap_quote_id", "swap_tree", "claim_public_key", "payment_hash", "timeout_block_height", "invoice", "invoice_amount", "transaction_amount", "swap_fee_sats", "chain_fee_sats", "ephemeral_public_key", "address", "ephemeral_private_key", "used", "preimage", "failure_reason", "tx_id", "service_url", "created_at", "updated_at") SELECT "swap_operation_id", "app_user_id", "swap_quote_id", "swap_tree", "claim_public_key", "payment_hash", "timeout_block_height", "invoice", "invoice_amount", "transaction_amount", "swap_fee_sats", "chain_fee_sats", "ephemeral_public_key", "address", "ephemeral_private_key", "used", "preimage", "failure_reason", "tx_id", "service_url", "created_at", "updated_at" FROM "temporary_invoice_swap"`);
await queryRunner.query(`DROP TABLE "temporary_invoice_swap"`);
}
}

View file

@ -32,14 +32,15 @@ import { TxSwapAddress1764779178945 } from './1764779178945-tx_swap_address.js'
import { ClinkRequester1765497600000 } from './1765497600000-clink_requester.js'
import { TrackedProviderHeight1766504040000 } from './1766504040000-tracked_provider_height.js'
import { SwapsServiceUrl1768413055036 } from './1768413055036-swaps_service_url.js'
import { InvoiceSwaps1769529793283 } from './1769529793283-invoice_swaps.js'
import { InvoiceSwapsFixes1769805357459 } from './1769805357459-invoice_swaps_fixes.js'
export const allMigrations = [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189,
TrackedProvider1720814323679, CreateInviteTokenTable1721751414878, PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264,
DebitToPub1727105758354, UserCbUrl1727112281043, UserOffer1733502626042, ManagementGrant1751307732346, ManagementGrantBanned1751989251513,
InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611, AppUserDevice1753285173175,
UserAccess1759426050669, AddBlindToUserOffer1760000000000, ApplicationAvatarUrl1761000001000, AdminSettings1761683639419, TxSwap1762890527098,
TxSwapAddress1764779178945, ClinkRequester1765497600000, TrackedProviderHeight1766504040000, SwapsServiceUrl1768413055036]
TxSwapAddress1764779178945, ClinkRequester1765497600000, TrackedProviderHeight1766504040000, SwapsServiceUrl1768413055036, InvoiceSwaps1769529793283, InvoiceSwapsFixes1769805357459]
export const allMetricsMigrations = [LndMetrics1703170330183, ChannelRouting1709316653538, HtlcCount1724266887195, BalanceEvents1724860966825, RootOps1732566440447, RootOpsTime1745428134124, ChannelEvents1750777346411]
/* export const TypeOrmMigrationRunner = async (log: PubLogger, storageManager: Storage, settings: DbSettings, arg: string | undefined): Promise<boolean> => {

View file

@ -15,6 +15,7 @@ import TransactionsQueue from "./db/transactionsQueue.js";
import { LoggedEvent } from './eventsLog.js';
import { StorageInterface } from './db/storageInterface.js';
import { TransactionSwap } from './entity/TransactionSwap.js';
import { InvoiceSwap } from './entity/InvoiceSwap.js';
export type InboundOptionals = { product?: Product, callbackUrl?: string, expiry: number, expectedPayer?: User, linkedApplication?: Application, zapInfo?: ZapInfo, offerId?: string, payerData?: Record<string, string>, rejectUnauthorized?: boolean, token?: string, blind?: boolean, clinkRequesterPub?: string, clinkRequesterEventId?: string }
export const defaultInvoiceExpiry = 60 * 60
export default class {
@ -472,20 +473,20 @@ export default class {
return this.dbs.FindOne<TransactionSwap>('TransactionSwap', { where: { swap_operation_id: swapOperationId, used: false, app_user_id: appUserId } }, txId)
}
async FinalizeTransactionSwap(swapOperationId: string, address: string, txId: string) {
async FinalizeTransactionSwap(swapOperationId: string, address: string, chainTxId: string, txId?: string) {
return this.dbs.Update<TransactionSwap>('TransactionSwap', { swap_operation_id: swapOperationId }, {
used: true,
tx_id: txId,
tx_id: chainTxId,
address_paid: address,
})
}, txId)
}
async FailTransactionSwap(swapOperationId: string, address: string, failureReason: string) {
async FailTransactionSwap(swapOperationId: string, address: string, failureReason: string, txId?: string) {
return this.dbs.Update<TransactionSwap>('TransactionSwap', { swap_operation_id: swapOperationId }, {
used: true,
failure_reason: failureReason,
address_paid: address,
})
}, txId)
}
async DeleteTransactionSwap(swapOperationId: string, txId?: string) {
@ -500,11 +501,11 @@ export default class {
return this.dbs.Find<TransactionSwap>('TransactionSwap', { where: { used: false, app_user_id: appUserId } }, txId)
}
async ListSwapPayments(userId: string, txId?: string) {
async ListTxSwapPayments(userId: string, txId?: string) {
return this.dbs.Find<UserInvoicePayment>('UserInvoicePayment', { where: { swap_operation_id: Not(IsNull()), user: { user_id: userId } } }, txId)
}
async ListCompletedSwaps(appUserId: string, payments: UserInvoicePayment[], txId?: string) {
async ListCompletedTxSwaps(appUserId: string, payments: UserInvoicePayment[], txId?: string) {
const completed = await this.dbs.Find<TransactionSwap>('TransactionSwap', { where: { used: true, app_user_id: appUserId } }, txId)
// const payments = await this.dbs.Find<UserInvoicePayment>('UserInvoicePayment', { where: { swap_operation_id: Not(IsNull()), } }, txId)
const paymentsMap = new Map<string, UserInvoicePayment>()
@ -515,6 +516,78 @@ export default class {
swap: c, payment: paymentsMap.get(c.swap_operation_id)
}))
}
async AddInvoiceSwap(swap: Partial<InvoiceSwap>) {
return this.dbs.CreateAndSave<InvoiceSwap>('InvoiceSwap', swap)
}
async GetInvoiceSwap(swapOperationId: string, appUserId: string, txId?: string) {
const swap = await this.dbs.FindOne<InvoiceSwap>('InvoiceSwap', { where: { swap_operation_id: swapOperationId, used: false, app_user_id: appUserId } }, txId)
if (!swap || swap.tx_id) {
return null
}
return swap
}
async FinalizeInvoiceSwap(swapOperationId: string, txId?: string) {
return this.dbs.Update<InvoiceSwap>('InvoiceSwap', { swap_operation_id: swapOperationId }, {
used: true,
}, txId)
}
async UpdateInvoiceSwap(swapOperationId: string, update: Partial<InvoiceSwap>, txId?: string) {
return this.dbs.Update<InvoiceSwap>('InvoiceSwap', { swap_operation_id: swapOperationId }, update, txId)
}
async SetInvoiceSwapTxId(swapOperationId: string, chainTxId: string, lockupTxHex?: string, txId?: string) {
const update: Partial<InvoiceSwap> = {
tx_id: chainTxId,
}
if (lockupTxHex) {
update.lockup_tx_hex = lockupTxHex
}
return this.dbs.Update<InvoiceSwap>('InvoiceSwap', { swap_operation_id: swapOperationId }, update, txId)
}
async FailInvoiceSwap(swapOperationId: string, failureReason: string, txId?: string) {
return this.dbs.Update<InvoiceSwap>('InvoiceSwap', { swap_operation_id: swapOperationId }, {
used: true,
failure_reason: failureReason,
}, txId)
}
async DeleteInvoiceSwap(swapOperationId: string, txId?: string) {
return this.dbs.Delete<InvoiceSwap>('InvoiceSwap', { swap_operation_id: swapOperationId }, txId)
}
async DeleteExpiredInvoiceSwaps(currentHeight: number, txId?: string) {
return this.dbs.Delete<InvoiceSwap>('InvoiceSwap', { timeout_block_height: LessThan(currentHeight) }, txId)
}
async ListCompletedInvoiceSwaps(appUserId: string, txId?: string) {
return this.dbs.Find<InvoiceSwap>('InvoiceSwap', { where: { used: true, app_user_id: appUserId } }, txId)
}
async ListPendingInvoiceSwaps(appUserId: string, txId?: string) {
return this.dbs.Find<InvoiceSwap>('InvoiceSwap', { where: { used: false, app_user_id: appUserId } }, txId)
}
async ListUnfinishedInvoiceSwaps(txId?: string) {
const swaps = await this.dbs.Find<InvoiceSwap>('InvoiceSwap', { where: { used: false } }, txId)
return swaps.filter(s => !!s.tx_id)
}
async GetRefundableInvoiceSwap(swapOperationId: string, txId?: string) {
const swap = await this.dbs.FindOne<InvoiceSwap>('InvoiceSwap', { where: { swap_operation_id: swapOperationId } }, txId)
if (!swap || !swap.tx_id) {
return null
}
if (swap.used && !swap.failure_reason) {
return null
}
return swap
}
}
const orFail = async <T>(resultPromise: Promise<T | null>) => {