swap refunds
This commit is contained in:
parent
e71f631993
commit
3255730ae2
21 changed files with 554 additions and 33 deletions
|
|
@ -49,6 +49,7 @@ import { TxSwapAddress1764779178945 } from './build/src/services/storage/migrati
|
||||||
import { ClinkRequester1765497600000 } from './build/src/services/storage/migrations/1765497600000-clink_requester.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 { 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 { 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({
|
export default new DataSource({
|
||||||
type: "better-sqlite3",
|
type: "better-sqlite3",
|
||||||
|
|
@ -58,11 +59,11 @@ export default new DataSource({
|
||||||
PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264, DebitToPub1727105758354, UserCbUrl1727112281043,
|
PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264, DebitToPub1727105758354, UserCbUrl1727112281043,
|
||||||
UserOffer1733502626042, ManagementGrant1751307732346, InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611,
|
UserOffer1733502626042, ManagementGrant1751307732346, InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611,
|
||||||
AppUserDevice1753285173175, UserAccess1759426050669, AddBlindToUserOffer1760000000000, ApplicationAvatarUrl1761000001000, AdminSettings1761683639419, TxSwap1762890527098,
|
AppUserDevice1753285173175, UserAccess1759426050669, AddBlindToUserOffer1760000000000, ApplicationAvatarUrl1761000001000, AdminSettings1761683639419, TxSwap1762890527098,
|
||||||
TxSwapAddress1764779178945, ClinkRequester1765497600000, TrackedProviderHeight1766504040000, SwapsServiceUrl1768413055036],
|
TxSwapAddress1764779178945, ClinkRequester1765497600000, TrackedProviderHeight1766504040000, SwapsServiceUrl1768413055036, InvoiceSwaps1769529793283],
|
||||||
|
|
||||||
entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment,
|
entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment,
|
||||||
UserBasicAuth, UserEphemeralKey, Product, UserToUserPayment, Application, ApplicationUser, UserToUserPayment, LspOrder, LndNodeInfo,
|
UserBasicAuth, UserEphemeralKey, Product, UserToUserPayment, Application, ApplicationUser, UserToUserPayment, LspOrder, LndNodeInfo,
|
||||||
TrackedProvider, InviteToken, DebitAccess, UserOffer, ManagementGrant, AppUserDevice, UserAccess, AdminSettings, TransactionSwap, InvoiceSwap],
|
TrackedProvider, InviteToken, DebitAccess, UserOffer, ManagementGrant, AppUserDevice, UserAccess, AdminSettings, TransactionSwap, InvoiceSwap],
|
||||||
// synchronize: true,
|
// synchronize: true,
|
||||||
})
|
})
|
||||||
//npx typeorm migration:generate ./src/services/storage/migrations/invoice_swaps -d ./datasource.js
|
//npx typeorm migration:generate ./src/services/storage/migrations/invoice_swaps_fixes -d ./datasource.js
|
||||||
|
|
@ -320,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__ __request__ body
|
||||||
- This methods has an __empty__ __response__ body
|
- This methods has an __empty__ __response__ body
|
||||||
|
|
||||||
|
- RefundAdminInvoiceSwap
|
||||||
|
- auth type: __Admin__
|
||||||
|
- input: [RefundAdminInvoiceSwapRequest](#RefundAdminInvoiceSwapRequest)
|
||||||
|
- output: [AdminInvoiceSwapResponse](#AdminInvoiceSwapResponse)
|
||||||
|
|
||||||
- ResetDebit
|
- ResetDebit
|
||||||
- auth type: __User__
|
- auth type: __User__
|
||||||
- input: [DebitOperation](#DebitOperation)
|
- input: [DebitOperation](#DebitOperation)
|
||||||
|
|
@ -963,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__ __request__ body
|
||||||
- This methods has an __empty__ __response__ 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
|
- RequestNPubLinkingToken
|
||||||
- auth type: __App__
|
- auth type: __App__
|
||||||
- http method: __post__
|
- http method: __post__
|
||||||
|
|
@ -1603,6 +1615,7 @@ The nostr server will send back a message response, and inside the body there wi
|
||||||
- __txId__: _string_
|
- __txId__: _string_
|
||||||
|
|
||||||
### PayAdminInvoiceSwapRequest
|
### PayAdminInvoiceSwapRequest
|
||||||
|
- __no_claim__: _boolean_ *this field is optional
|
||||||
- __sat_per_v_byte__: _number_
|
- __sat_per_v_byte__: _number_
|
||||||
- __swap_operation_id__: _string_
|
- __swap_operation_id__: _string_
|
||||||
|
|
||||||
|
|
@ -1656,6 +1669,10 @@ The nostr server will send back a message response, and inside the body there wi
|
||||||
### ProvidersDisruption
|
### ProvidersDisruption
|
||||||
- __disruptions__: ARRAY of: _[ProviderDisruption](#ProviderDisruption)_
|
- __disruptions__: ARRAY of: _[ProviderDisruption](#ProviderDisruption)_
|
||||||
|
|
||||||
|
### RefundAdminInvoiceSwapRequest
|
||||||
|
- __sat_per_v_byte__: _number_
|
||||||
|
- __swap_operation_id__: _string_
|
||||||
|
|
||||||
### RelaysMigration
|
### RelaysMigration
|
||||||
- __relays__: ARRAY of: _string_
|
- __relays__: ARRAY of: _string_
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -130,6 +130,7 @@ type Client struct {
|
||||||
PayAppUserInvoice func(req PayAppUserInvoiceRequest) (*PayInvoiceResponse, error)
|
PayAppUserInvoice func(req PayAppUserInvoiceRequest) (*PayInvoiceResponse, error)
|
||||||
PayInvoice func(req PayInvoiceRequest) (*PayInvoiceResponse, error)
|
PayInvoice func(req PayInvoiceRequest) (*PayInvoiceResponse, error)
|
||||||
PingSubProcesses func() error
|
PingSubProcesses func() error
|
||||||
|
RefundAdminInvoiceSwap func(req RefundAdminInvoiceSwapRequest) (*AdminInvoiceSwapResponse, error)
|
||||||
RequestNPubLinkingToken func(req RequestNPubLinkingTokenRequest) (*RequestNPubLinkingTokenResponse, error)
|
RequestNPubLinkingToken func(req RequestNPubLinkingTokenRequest) (*RequestNPubLinkingTokenResponse, error)
|
||||||
ResetDebit func(req DebitOperation) error
|
ResetDebit func(req DebitOperation) error
|
||||||
ResetManage func(req ManageOperation) error
|
ResetManage func(req ManageOperation) error
|
||||||
|
|
@ -2087,6 +2088,35 @@ func NewClient(params ClientParams) *Client {
|
||||||
}
|
}
|
||||||
return nil
|
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) {
|
RequestNPubLinkingToken: func(req RequestNPubLinkingTokenRequest) (*RequestNPubLinkingTokenResponse, error) {
|
||||||
auth, err := params.RetrieveAppAuth()
|
auth, err := params.RetrieveAppAuth()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -592,6 +592,7 @@ type PayAddressResponse struct {
|
||||||
Txid string `json:"txId"`
|
Txid string `json:"txId"`
|
||||||
}
|
}
|
||||||
type PayAdminInvoiceSwapRequest struct {
|
type PayAdminInvoiceSwapRequest struct {
|
||||||
|
No_claim bool `json:"no_claim"`
|
||||||
Sat_per_v_byte int64 `json:"sat_per_v_byte"`
|
Sat_per_v_byte int64 `json:"sat_per_v_byte"`
|
||||||
Swap_operation_id string `json:"swap_operation_id"`
|
Swap_operation_id string `json:"swap_operation_id"`
|
||||||
}
|
}
|
||||||
|
|
@ -645,6 +646,10 @@ type ProviderDisruption struct {
|
||||||
type ProvidersDisruption struct {
|
type ProvidersDisruption struct {
|
||||||
Disruptions []ProviderDisruption `json:"disruptions"`
|
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 {
|
type RelaysMigration struct {
|
||||||
Relays []string `json:"relays"`
|
Relays []string `json:"relays"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1941,6 +1941,28 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => {
|
||||||
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
|
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
|
||||||
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
|
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
|
||||||
})
|
})
|
||||||
|
if (!opts.allowNotImplementedMethods && !methods.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')
|
if (!opts.allowNotImplementedMethods && !methods.RequestNPubLinkingToken) throw new Error('method: RequestNPubLinkingToken is not implemented')
|
||||||
app.post('/api/app/user/npub/token', async (req, res) => {
|
app.post('/api/app/user/npub/token', async (req, res) => {
|
||||||
const info: Types.RequestInfo = { rpcName: 'RequestNPubLinkingToken', batch: false, nostr: false, batchSize: 0}
|
const info: Types.RequestInfo = { rpcName: 'RequestNPubLinkingToken', batch: false, nostr: false, batchSize: 0}
|
||||||
|
|
|
||||||
|
|
@ -1004,6 +1004,20 @@ export default (params: ClientParams) => ({
|
||||||
}
|
}
|
||||||
return { status: 'ERROR', reason: 'invalid response' }
|
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)> => {
|
RequestNPubLinkingToken: async (request: Types.RequestNPubLinkingTokenRequest): Promise<ResultError | ({ status: 'OK' }& Types.RequestNPubLinkingTokenResponse)> => {
|
||||||
const auth = await params.retrieveAppAuth()
|
const auth = await params.retrieveAppAuth()
|
||||||
if (auth === null) throw new Error('retrieveAppAuth() returned null')
|
if (auth === null) throw new Error('retrieveAppAuth() returned null')
|
||||||
|
|
|
||||||
|
|
@ -883,6 +883,21 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ
|
||||||
}
|
}
|
||||||
return { status: 'ERROR', reason: 'invalid response' }
|
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' })> => {
|
ResetDebit: async (request: Types.DebitOperation): Promise<ResultError | ({ status: 'OK' })> => {
|
||||||
const auth = await params.retrieveNostrUserAuth()
|
const auth = await params.retrieveNostrUserAuth()
|
||||||
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
|
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
|
||||||
|
|
|
||||||
|
|
@ -1344,6 +1344,22 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => {
|
||||||
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
|
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
|
||||||
}catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
|
}catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
|
||||||
break
|
break
|
||||||
|
case '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':
|
case 'ResetDebit':
|
||||||
try {
|
try {
|
||||||
if (!methods.ResetDebit) throw new Error('method: ResetDebit is not implemented')
|
if (!methods.ResetDebit) throw new Error('method: ResetDebit is not implemented')
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ export type RequestMetric = AuthContext & RequestInfo & RequestStats & { error?:
|
||||||
export type AdminContext = {
|
export type AdminContext = {
|
||||||
admin_id: string
|
admin_id: string
|
||||||
}
|
}
|
||||||
export type AdminMethodInputs = AddApp_Input | AddPeer_Input | AuthApp_Input | BanUser_Input | CloseChannel_Input | CreateOneTimeInviteLink_Input | GetAdminInvoiceSwapQuotes_Input | GetAdminTransactionSwapQuotes_Input | GetInviteLinkState_Input | GetSeed_Input | ListAdminInvoiceSwaps_Input | ListAdminTxSwaps_Input | ListChannels_Input | LndGetInfo_Input | OpenChannel_Input | PayAdminInvoiceSwap_Input | PayAdminTransactionSwap_Input | UpdateChannelPolicy_Input
|
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 | UpdateChannelPolicy_Output
|
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 = {
|
export type AppContext = {
|
||||||
app_id: string
|
app_id: string
|
||||||
}
|
}
|
||||||
|
|
@ -289,6 +289,9 @@ export type PayInvoice_Output = ResultError | ({ status: 'OK' } & PayInvoiceResp
|
||||||
export type PingSubProcesses_Input = {rpcName:'PingSubProcesses'}
|
export type PingSubProcesses_Input = {rpcName:'PingSubProcesses'}
|
||||||
export type PingSubProcesses_Output = ResultError | { status: 'OK' }
|
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_Input = {rpcName:'RequestNPubLinkingToken', req: RequestNPubLinkingTokenRequest}
|
||||||
export type RequestNPubLinkingToken_Output = ResultError | ({ status: 'OK' } & RequestNPubLinkingTokenResponse)
|
export type RequestNPubLinkingToken_Output = ResultError | ({ status: 'OK' } & RequestNPubLinkingTokenResponse)
|
||||||
|
|
||||||
|
|
@ -422,6 +425,7 @@ export type ServerMethods = {
|
||||||
PayAppUserInvoice?: (req: PayAppUserInvoice_Input & {ctx: AppContext }) => Promise<PayInvoiceResponse>
|
PayAppUserInvoice?: (req: PayAppUserInvoice_Input & {ctx: AppContext }) => Promise<PayInvoiceResponse>
|
||||||
PayInvoice?: (req: PayInvoice_Input & {ctx: UserContext }) => Promise<PayInvoiceResponse>
|
PayInvoice?: (req: PayInvoice_Input & {ctx: UserContext }) => Promise<PayInvoiceResponse>
|
||||||
PingSubProcesses?: (req: PingSubProcesses_Input & {ctx: MetricsContext }) => Promise<void>
|
PingSubProcesses?: (req: PingSubProcesses_Input & {ctx: MetricsContext }) => Promise<void>
|
||||||
|
RefundAdminInvoiceSwap?: (req: RefundAdminInvoiceSwap_Input & {ctx: AdminContext }) => Promise<AdminInvoiceSwapResponse>
|
||||||
RequestNPubLinkingToken?: (req: RequestNPubLinkingToken_Input & {ctx: AppContext }) => Promise<RequestNPubLinkingTokenResponse>
|
RequestNPubLinkingToken?: (req: RequestNPubLinkingToken_Input & {ctx: AppContext }) => Promise<RequestNPubLinkingTokenResponse>
|
||||||
ResetDebit?: (req: ResetDebit_Input & {ctx: UserContext }) => Promise<void>
|
ResetDebit?: (req: ResetDebit_Input & {ctx: UserContext }) => Promise<void>
|
||||||
ResetManage?: (req: ResetManage_Input & {ctx: UserContext }) => Promise<void>
|
ResetManage?: (req: ResetManage_Input & {ctx: UserContext }) => Promise<void>
|
||||||
|
|
@ -3518,12 +3522,15 @@ export const PayAddressResponseValidate = (o?: PayAddressResponse, opts: PayAddr
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PayAdminInvoiceSwapRequest = {
|
export type PayAdminInvoiceSwapRequest = {
|
||||||
|
no_claim?: boolean
|
||||||
sat_per_v_byte: number
|
sat_per_v_byte: number
|
||||||
swap_operation_id: string
|
swap_operation_id: string
|
||||||
}
|
}
|
||||||
export const PayAdminInvoiceSwapRequestOptionalFields: [] = []
|
export type PayAdminInvoiceSwapRequestOptionalField = 'no_claim'
|
||||||
|
export const PayAdminInvoiceSwapRequestOptionalFields: PayAdminInvoiceSwapRequestOptionalField[] = ['no_claim']
|
||||||
export type PayAdminInvoiceSwapRequestOptions = OptionsBaseMessage & {
|
export type PayAdminInvoiceSwapRequestOptions = OptionsBaseMessage & {
|
||||||
checkOptionalsAreSet?: []
|
checkOptionalsAreSet?: PayAdminInvoiceSwapRequestOptionalField[]
|
||||||
|
no_claim_CustomCheck?: (v?: boolean) => boolean
|
||||||
sat_per_v_byte_CustomCheck?: (v: number) => boolean
|
sat_per_v_byte_CustomCheck?: (v: number) => boolean
|
||||||
swap_operation_id_CustomCheck?: (v: string) => boolean
|
swap_operation_id_CustomCheck?: (v: string) => boolean
|
||||||
}
|
}
|
||||||
|
|
@ -3531,6 +3538,9 @@ export const PayAdminInvoiceSwapRequestValidate = (o?: PayAdminInvoiceSwapReques
|
||||||
if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message')
|
if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message')
|
||||||
if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null')
|
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 (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 (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`)
|
||||||
|
|
||||||
|
|
@ -3832,6 +3842,29 @@ export const ProvidersDisruptionValidate = (o?: ProvidersDisruption, opts: Provi
|
||||||
return null
|
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 = {
|
export type RelaysMigration = {
|
||||||
relays: string[]
|
relays: string[]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -196,6 +196,13 @@ service LightningPub {
|
||||||
option (nostr) = true;
|
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) {
|
rpc GetAdminTransactionSwapQuotes(structs.TransactionSwapRequest) returns (structs.TransactionSwapQuoteList) {
|
||||||
option (auth_type) = "Admin";
|
option (auth_type) = "Admin";
|
||||||
option (http_method) = "post";
|
option (http_method) = "post";
|
||||||
|
|
|
||||||
|
|
@ -867,9 +867,15 @@ message InvoiceSwapsList {
|
||||||
repeated InvoiceSwapQuote quotes = 2;
|
repeated InvoiceSwapQuote quotes = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message RefundAdminInvoiceSwapRequest {
|
||||||
|
string swap_operation_id = 1;
|
||||||
|
int64 sat_per_v_byte = 2;
|
||||||
|
}
|
||||||
|
|
||||||
message PayAdminInvoiceSwapRequest {
|
message PayAdminInvoiceSwapRequest {
|
||||||
string swap_operation_id = 1;
|
string swap_operation_id = 1;
|
||||||
int64 sat_per_v_byte = 2;
|
int64 sat_per_v_byte = 2;
|
||||||
|
optional bool no_claim = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message AdminInvoiceSwapResponse {
|
message AdminInvoiceSwapResponse {
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import { TxPointSettings } from '../storage/tlv/stateBundler.js';
|
||||||
import { WalletKitClient } from '../../../proto/lnd/walletkit.client.js';
|
import { WalletKitClient } from '../../../proto/lnd/walletkit.client.js';
|
||||||
import SettingsManager from '../main/settingsManager.js';
|
import SettingsManager from '../main/settingsManager.js';
|
||||||
import { LndNodeSettings, LndSettings } from '../main/settings.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 DeadLineMetadata = (deadline = 10 * 1000) => ({ deadline: Date.now() + deadline })
|
||||||
const deadLndRetrySeconds = 20
|
const deadLndRetrySeconds = 20
|
||||||
|
|
@ -156,6 +156,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> {
|
async GetInfo(): Promise<NodeInfo> {
|
||||||
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
|
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
|
||||||
// Return dummy info when bypass is enabled
|
// Return dummy info when bypass is enabled
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { initEccLib, Transaction, address } from 'bitcoinjs-lib';
|
||||||
// import bolt11 from 'bolt11';
|
// import bolt11 from 'bolt11';
|
||||||
import {
|
import {
|
||||||
Musig, SwapTreeSerializer, TaprootUtils, detectSwap,
|
Musig, SwapTreeSerializer, TaprootUtils, detectSwap,
|
||||||
constructClaimTransaction, OutputType,
|
constructClaimTransaction, OutputType, constructRefundTransaction
|
||||||
} from 'boltz-core';
|
} from 'boltz-core';
|
||||||
import { randomBytes, createHash } from 'crypto';
|
import { randomBytes, createHash } from 'crypto';
|
||||||
import { ECPairFactory, ECPairInterface } from 'ecpair';
|
import { ECPairFactory, ECPairInterface } from 'ecpair';
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,16 @@
|
||||||
import zkpInit from '@vulpemventures/secp256k1-zkp';
|
import zkpInit from '@vulpemventures/secp256k1-zkp';
|
||||||
// import bolt11 from 'bolt11';
|
// import bolt11 from 'bolt11';
|
||||||
import {
|
import {
|
||||||
Musig, SwapTreeSerializer, TaprootUtils
|
Musig, SwapTreeSerializer, TaprootUtils, constructRefundTransaction,
|
||||||
|
detectSwap, OutputType
|
||||||
} from 'boltz-core';
|
} from 'boltz-core';
|
||||||
import { randomBytes, createHash } from 'crypto';
|
import { randomBytes, createHash } from 'crypto';
|
||||||
import { ECPairFactory, ECPairInterface } from 'ecpair';
|
import { ECPairFactory, ECPairInterface } from 'ecpair';
|
||||||
import * as ecc from 'tiny-secp256k1';
|
import * as ecc from 'tiny-secp256k1';
|
||||||
|
import { Transaction, address } from 'bitcoinjs-lib';
|
||||||
import ws from 'ws';
|
import ws from 'ws';
|
||||||
import { getLogger, PubLogger, ERROR } from '../../helpers/logger.js';
|
import { getLogger, PubLogger, ERROR } from '../../helpers/logger.js';
|
||||||
import { loggedGet, loggedPost } from './swapHelpers.js';
|
import { loggedGet, loggedPost, getNetwork } from './swapHelpers.js';
|
||||||
import { BTCNetwork } from '../../main/settings.js';
|
import { BTCNetwork } from '../../main/settings.js';
|
||||||
|
|
||||||
/* type InvoiceSwapFees = {
|
/* type InvoiceSwapFees = {
|
||||||
|
|
@ -47,10 +49,12 @@ export type InvoiceSwapData = { createdResponse: InvoiceSwapResponse, info: Invo
|
||||||
export class SubmarineSwaps {
|
export class SubmarineSwaps {
|
||||||
private httpUrl: string
|
private httpUrl: string
|
||||||
private wsUrl: string
|
private wsUrl: string
|
||||||
|
private network: BTCNetwork
|
||||||
log: PubLogger
|
log: PubLogger
|
||||||
constructor({ httpUrl, wsUrl }: { httpUrl: string, wsUrl: string, network: BTCNetwork }) {
|
constructor({ httpUrl, wsUrl, network }: { httpUrl: string, wsUrl: string, network: BTCNetwork }) {
|
||||||
this.httpUrl = httpUrl
|
this.httpUrl = httpUrl
|
||||||
this.wsUrl = wsUrl
|
this.wsUrl = wsUrl
|
||||||
|
this.network = network
|
||||||
this.log = getLogger({ component: 'SubmarineSwaps' })
|
this.log = getLogger({ component: 'SubmarineSwaps' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -97,6 +101,273 @@ export class SubmarineSwaps {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) => {
|
SubscribeToInvoiceSwap = (data: InvoiceSwapData, swapDone: (result: { ok: true } | { ok: false, error: string }) => void, waitingTx: () => void) => {
|
||||||
const webSocket = new ws(`${this.wsUrl}/v2/ws`)
|
const webSocket = new ws(`${this.wsUrl}/v2/ws`)
|
||||||
const subReq = { op: 'subscribe', channel: 'swap.update', args: [data.createdResponse.id] }
|
const subReq = { op: 'subscribe', channel: 'swap.update', args: [data.createdResponse.id] }
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,36 @@ export class Swaps {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RefundInvoiceSwap = async (swapOperationId: string, satPerVByte: number, refundAddress: string, currentHeight: number): Promise<{ published: false, txHex: string, txId: string } | { published: true, txId: string }> => {
|
||||||
|
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> => {
|
PayInvoiceSwap = async (appUserId: string, swapOpId: string, satPerVByte: number, payAddress: (address: string, amt: number) => Promise<{ txId: string }>): Promise<void> => {
|
||||||
if (!this.settings.getSettings().swapsSettings.enableSwaps) {
|
if (!this.settings.getSettings().swapsSettings.enableSwaps) {
|
||||||
throw new Error("Swaps are not enabled")
|
throw new Error("Swaps are not enabled")
|
||||||
|
|
|
||||||
|
|
@ -274,6 +274,18 @@ export class AdminManager {
|
||||||
this.swaps.PayInvoiceSwap("admin", req.swap_operation_id, req.sat_per_v_byte, async (addr, amt) => {
|
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' })
|
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 })
|
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)
|
||||||
res(tx.txid)
|
res(tx.txid)
|
||||||
return { txId: tx.txid }
|
return { txId: tx.txid }
|
||||||
})
|
})
|
||||||
|
|
@ -281,6 +293,18 @@ export class AdminManager {
|
||||||
return { tx_id: 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> {
|
async ListAdminTxSwaps(): Promise<Types.TxSwapsList> {
|
||||||
return this.swaps.ListTxSwaps("admin", [], p => undefined, amt => 0)
|
return this.swaps.ListTxSwaps("admin", [], p => undefined, amt => 0)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,9 @@ export class InvoiceSwap {
|
||||||
@Column({ default: "" })
|
@Column({ default: "" })
|
||||||
tx_id: string
|
tx_id: string
|
||||||
|
|
||||||
|
@Column({ default: "", type: "text" })
|
||||||
|
lockup_tx_hex: string
|
||||||
|
|
||||||
/* @Column({ default: "" })
|
/* @Column({ default: "" })
|
||||||
address_paid: string */
|
address_paid: string */
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,25 +5,9 @@ export class InvoiceSwaps1769529793283 implements MigrationInterface {
|
||||||
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
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')))`);
|
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(`DROP INDEX "recv_invoice_paid_serial"`);
|
|
||||||
await queryRunner.query(`DROP INDEX "IDX_a131e6b58f084f1340538681b5"`);
|
|
||||||
await queryRunner.query(`CREATE TABLE "temporary_user_receiving_invoice" ("serial_id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "invoice" varchar NOT NULL, "expires_at_unix" integer NOT NULL, "paid_at_unix" integer NOT NULL DEFAULT (0), "internal" boolean NOT NULL DEFAULT (0), "paidByLnd" boolean NOT NULL DEFAULT (0), "callbackUrl" varchar NOT NULL DEFAULT (''), "paid_amount" integer NOT NULL DEFAULT (0), "service_fee" integer NOT NULL DEFAULT (0), "zap_info" text, "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')), "userSerialId" integer, "productProductId" varchar, "payerSerialId" integer, "linkedApplicationSerialId" integer, "liquidityProvider" varchar, "payer_data" text, "offer_id" varchar NOT NULL DEFAULT (''), "rejectUnauthorized" boolean NOT NULL DEFAULT (1), "bearer_token" varchar NOT NULL DEFAULT (''), "clink_requester_pub" varchar, "clink_requester_event_id" varchar, CONSTRAINT "FK_2c0dfb3483f3e5e7e3cdd5dc71f" FOREIGN KEY ("userSerialId") REFERENCES "user" ("serial_id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_5263bde2a519db9ea608b702ec8" FOREIGN KEY ("productProductId") REFERENCES "product" ("product_id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_d4bb1e4c60e8a869f1f43ca2e31" FOREIGN KEY ("payerSerialId") REFERENCES "user" ("serial_id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_714a8b7d4f89f8a802ca181b789" FOREIGN KEY ("linkedApplicationSerialId") REFERENCES "application" ("serial_id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
|
|
||||||
await queryRunner.query(`INSERT INTO "temporary_user_receiving_invoice"("serial_id", "invoice", "expires_at_unix", "paid_at_unix", "internal", "paidByLnd", "callbackUrl", "paid_amount", "service_fee", "zap_info", "created_at", "updated_at", "userSerialId", "productProductId", "payerSerialId", "linkedApplicationSerialId", "liquidityProvider", "payer_data", "offer_id", "rejectUnauthorized", "bearer_token", "clink_requester_pub", "clink_requester_event_id") SELECT "serial_id", "invoice", "expires_at_unix", "paid_at_unix", "internal", "paidByLnd", "callbackUrl", "paid_amount", "service_fee", "zap_info", "created_at", "updated_at", "userSerialId", "productProductId", "payerSerialId", "linkedApplicationSerialId", "liquidityProvider", "payer_data", "offer_id", "rejectUnauthorized", "bearer_token", "clink_requester_pub", "clink_requester_event_id" FROM "user_receiving_invoice"`);
|
|
||||||
await queryRunner.query(`DROP TABLE "user_receiving_invoice"`);
|
|
||||||
await queryRunner.query(`ALTER TABLE "temporary_user_receiving_invoice" RENAME TO "user_receiving_invoice"`);
|
|
||||||
await queryRunner.query(`CREATE INDEX "recv_invoice_paid_serial" ON "user_receiving_invoice" ("userSerialId", "paid_at_unix", "serial_id") WHERE paid_at_unix > 0`);
|
|
||||||
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_a131e6b58f084f1340538681b5" ON "user_receiving_invoice" ("invoice") `);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
await queryRunner.query(`DROP INDEX "IDX_a131e6b58f084f1340538681b5"`);
|
|
||||||
await queryRunner.query(`DROP INDEX "recv_invoice_paid_serial"`);
|
|
||||||
await queryRunner.query(`ALTER TABLE "user_receiving_invoice" RENAME TO "temporary_user_receiving_invoice"`);
|
|
||||||
await queryRunner.query(`CREATE TABLE "user_receiving_invoice" ("serial_id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "invoice" varchar NOT NULL, "expires_at_unix" integer NOT NULL, "paid_at_unix" integer NOT NULL DEFAULT (0), "internal" boolean NOT NULL DEFAULT (0), "paidByLnd" boolean NOT NULL DEFAULT (0), "callbackUrl" varchar NOT NULL DEFAULT (''), "paid_amount" integer NOT NULL DEFAULT (0), "service_fee" integer NOT NULL DEFAULT (0), "zap_info" text, "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')), "userSerialId" integer, "productProductId" varchar, "payerSerialId" integer, "linkedApplicationSerialId" integer, "liquidityProvider" varchar, "payer_data" text, "offer_id" varchar NOT NULL DEFAULT (''), "rejectUnauthorized" boolean NOT NULL DEFAULT (1), "bearer_token" varchar NOT NULL DEFAULT (''), "clink_requester_pub" varchar(64), "clink_requester_event_id" varchar(64), CONSTRAINT "FK_2c0dfb3483f3e5e7e3cdd5dc71f" FOREIGN KEY ("userSerialId") REFERENCES "user" ("serial_id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_5263bde2a519db9ea608b702ec8" FOREIGN KEY ("productProductId") REFERENCES "product" ("product_id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_d4bb1e4c60e8a869f1f43ca2e31" FOREIGN KEY ("payerSerialId") REFERENCES "user" ("serial_id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_714a8b7d4f89f8a802ca181b789" FOREIGN KEY ("linkedApplicationSerialId") REFERENCES "application" ("serial_id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
|
|
||||||
await queryRunner.query(`INSERT INTO "user_receiving_invoice"("serial_id", "invoice", "expires_at_unix", "paid_at_unix", "internal", "paidByLnd", "callbackUrl", "paid_amount", "service_fee", "zap_info", "created_at", "updated_at", "userSerialId", "productProductId", "payerSerialId", "linkedApplicationSerialId", "liquidityProvider", "payer_data", "offer_id", "rejectUnauthorized", "bearer_token", "clink_requester_pub", "clink_requester_event_id") SELECT "serial_id", "invoice", "expires_at_unix", "paid_at_unix", "internal", "paidByLnd", "callbackUrl", "paid_amount", "service_fee", "zap_info", "created_at", "updated_at", "userSerialId", "productProductId", "payerSerialId", "linkedApplicationSerialId", "liquidityProvider", "payer_data", "offer_id", "rejectUnauthorized", "bearer_token", "clink_requester_pub", "clink_requester_event_id" FROM "temporary_user_receiving_invoice"`);
|
|
||||||
await queryRunner.query(`DROP TABLE "temporary_user_receiving_invoice"`);
|
|
||||||
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_a131e6b58f084f1340538681b5" ON "user_receiving_invoice" ("invoice") `);
|
|
||||||
await queryRunner.query(`CREATE INDEX "recv_invoice_paid_serial" ON "user_receiving_invoice" ("userSerialId", "paid_at_unix", "serial_id") WHERE paid_at_unix > 0`);
|
|
||||||
await queryRunner.query(`DROP TABLE "invoice_swap"`);
|
await queryRunner.query(`DROP TABLE "invoice_swap"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -33,14 +33,14 @@ import { ClinkRequester1765497600000 } from './1765497600000-clink_requester.js'
|
||||||
import { TrackedProviderHeight1766504040000 } from './1766504040000-tracked_provider_height.js'
|
import { TrackedProviderHeight1766504040000 } from './1766504040000-tracked_provider_height.js'
|
||||||
import { SwapsServiceUrl1768413055036 } from './1768413055036-swaps_service_url.js'
|
import { SwapsServiceUrl1768413055036 } from './1768413055036-swaps_service_url.js'
|
||||||
import { InvoiceSwaps1769529793283 } from './1769529793283-invoice_swaps.js'
|
import { InvoiceSwaps1769529793283 } from './1769529793283-invoice_swaps.js'
|
||||||
|
import { InvoiceSwapsFixes1769805357459 } from './1769805357459-invoice_swaps_fixes.js'
|
||||||
|
|
||||||
export const allMigrations = [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189,
|
export const allMigrations = [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189,
|
||||||
TrackedProvider1720814323679, CreateInviteTokenTable1721751414878, PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264,
|
TrackedProvider1720814323679, CreateInviteTokenTable1721751414878, PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264,
|
||||||
DebitToPub1727105758354, UserCbUrl1727112281043, UserOffer1733502626042, ManagementGrant1751307732346, ManagementGrantBanned1751989251513,
|
DebitToPub1727105758354, UserCbUrl1727112281043, UserOffer1733502626042, ManagementGrant1751307732346, ManagementGrantBanned1751989251513,
|
||||||
InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611, AppUserDevice1753285173175,
|
InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611, AppUserDevice1753285173175,
|
||||||
UserAccess1759426050669, AddBlindToUserOffer1760000000000, ApplicationAvatarUrl1761000001000, AdminSettings1761683639419, TxSwap1762890527098,
|
UserAccess1759426050669, AddBlindToUserOffer1760000000000, ApplicationAvatarUrl1761000001000, AdminSettings1761683639419, TxSwap1762890527098,
|
||||||
TxSwapAddress1764779178945, ClinkRequester1765497600000, TrackedProviderHeight1766504040000, SwapsServiceUrl1768413055036, InvoiceSwaps1769529793283]
|
TxSwapAddress1764779178945, ClinkRequester1765497600000, TrackedProviderHeight1766504040000, SwapsServiceUrl1768413055036, InvoiceSwaps1769529793283, InvoiceSwapsFixes1769805357459]
|
||||||
|
|
||||||
export const allMetricsMigrations = [LndMetrics1703170330183, ChannelRouting1709316653538, HtlcCount1724266887195, BalanceEvents1724860966825, RootOps1732566440447, RootOpsTime1745428134124, ChannelEvents1750777346411]
|
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> => {
|
/* export const TypeOrmMigrationRunner = async (log: PubLogger, storageManager: Storage, settings: DbSettings, arg: string | undefined): Promise<boolean> => {
|
||||||
|
|
|
||||||
|
|
@ -535,10 +535,14 @@ export default class {
|
||||||
}, txId)
|
}, txId)
|
||||||
}
|
}
|
||||||
|
|
||||||
async SetInvoiceSwapTxId(swapOperationId: string, chainTxId: string, txId?: string) {
|
async SetInvoiceSwapTxId(swapOperationId: string, chainTxId: string, lockupTxHex?: string, txId?: string) {
|
||||||
return this.dbs.Update<InvoiceSwap>('InvoiceSwap', { swap_operation_id: swapOperationId }, {
|
const update: Partial<InvoiceSwap> = {
|
||||||
tx_id: chainTxId,
|
tx_id: chainTxId,
|
||||||
}, txId)
|
}
|
||||||
|
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) {
|
async FailInvoiceSwap(swapOperationId: string, failureReason: string, txId?: string) {
|
||||||
|
|
@ -566,7 +570,18 @@ export default class {
|
||||||
|
|
||||||
async ListUnfinishedInvoiceSwaps(txId?: string) {
|
async ListUnfinishedInvoiceSwaps(txId?: string) {
|
||||||
const swaps = await this.dbs.Find<InvoiceSwap>('InvoiceSwap', { where: { used: false } }, txId)
|
const swaps = await this.dbs.Find<InvoiceSwap>('InvoiceSwap', { where: { used: false } }, txId)
|
||||||
return swaps.filter(s => !s.tx_id)
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue