admin swaps

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

View file

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

View file

@ -74,6 +74,7 @@ type Client struct {
EncryptionExchange func(req EncryptionExchangeRequest) error
EnrollAdminToken func(req EnrollAdminTokenRequest) error
EnrollMessagingToken func(req MessagingToken) error
GetAdminTransactionSwapQuote func(req TransactionSwapRequest) (*TransactionSwapQuote, error)
GetApp func() (*Application, error)
GetAppUser func(req GetAppUserRequest) (*AppUser, error)
GetAppUserLNURLInfo func(req GetAppUserLNURLInfoRequest) (*LnurlPayInfoResponse, error)
@ -121,6 +122,7 @@ type Client struct {
NewProductInvoice func(query NewProductInvoice_Query) (*NewInvoiceResponse, error)
OpenChannel func(req OpenChannelRequest) (*OpenChannelResponse, error)
PayAddress func(req PayAddressRequest) (*PayAddressResponse, error)
PayAdminTransactionSwap func(req TransactionSwapQuoteRequest) (*AdminSwapResponse, error)
PayAppUserInvoice func(req PayAppUserInvoiceRequest) (*PayInvoiceResponse, error)
PayInvoice func(req PayInvoiceRequest) (*PayInvoiceResponse, error)
PingSubProcesses func() error
@ -664,6 +666,35 @@ func NewClient(params ClientParams) *Client {
}
return nil
},
GetAdminTransactionSwapQuote: func(req TransactionSwapRequest) (*TransactionSwapQuote, error) {
auth, err := params.RetrieveAdminAuth()
if err != nil {
return nil, err
}
finalRoute := "/api/admin/swap/transaction/quote"
body, err := json.Marshal(req)
if err != nil {
return nil, err
}
resBody, err := doPostRequest(params.BaseURL+finalRoute, body, auth)
if err != nil {
return nil, err
}
result := ResultError{}
err = json.Unmarshal(resBody, &result)
if err != nil {
return nil, err
}
if result.Status == "ERROR" {
return nil, fmt.Errorf(result.Reason)
}
res := TransactionSwapQuote{}
err = json.Unmarshal(resBody, &res)
if err != nil {
return nil, err
}
return &res, nil
},
GetApp: func() (*Application, error) {
auth, err := params.RetrieveAppAuth()
if err != nil {
@ -1834,6 +1865,35 @@ func NewClient(params ClientParams) *Client {
}
return &res, nil
},
PayAdminTransactionSwap: func(req TransactionSwapQuoteRequest) (*AdminSwapResponse, error) {
auth, err := params.RetrieveAdminAuth()
if err != nil {
return nil, err
}
finalRoute := "/api/admin/swap/transaction/pay"
body, err := json.Marshal(req)
if err != nil {
return nil, err
}
resBody, err := doPostRequest(params.BaseURL+finalRoute, body, auth)
if err != nil {
return nil, err
}
result := ResultError{}
err = json.Unmarshal(resBody, &result)
if err != nil {
return nil, err
}
if result.Status == "ERROR" {
return nil, fmt.Errorf(result.Reason)
}
res := AdminSwapResponse{}
err = json.Unmarshal(resBody, &res)
if err != nil {
return nil, err
}
return &res, nil
},
PayAppUserInvoice: func(req PayAppUserInvoiceRequest) (*PayInvoiceResponse, error) {
auth, err := params.RetrieveAppAuth()
if err != nil {

View file

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

View file

@ -869,6 +869,28 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => {
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
})
if (!opts.allowNotImplementedMethods && !methods.GetAdminTransactionSwapQuote) throw new Error('method: GetAdminTransactionSwapQuote is not implemented')
app.post('/api/admin/swap/transaction/quote', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'GetAdminTransactionSwapQuote', batch: false, nostr: false, batchSize: 0}
const stats: Types.RequestStats = { startMs:req.startTimeMs || 0, start:req.startTime || 0n, parse: process.hrtime.bigint(), guard: 0n, validate: 0n, handle: 0n }
let authCtx: Types.AuthContext = {}
try {
if (!methods.GetAdminTransactionSwapQuote) throw new Error('method: GetAdminTransactionSwapQuote is not implemented')
const authContext = await opts.AdminAuthGuard(req.headers['authorization'])
authCtx = authContext
stats.guard = process.hrtime.bigint()
const request = req.body
const error = Types.TransactionSwapRequestValidate(request)
stats.validate = process.hrtime.bigint()
if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authContext }, opts.metricsCallback)
const query = req.query
const params = req.params
const response = await methods.GetAdminTransactionSwapQuote({rpcName:'GetAdminTransactionSwapQuote', ctx:authContext , req: request})
stats.handle = process.hrtime.bigint()
res.json({status: 'OK', ...response})
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
})
if (!opts.allowNotImplementedMethods && !methods.GetApp) throw new Error('method: GetApp is not implemented')
app.post('/api/app/get', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'GetApp', batch: false, nostr: false, batchSize: 0}
@ -1752,6 +1774,28 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => {
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
})
if (!opts.allowNotImplementedMethods && !methods.PayAdminTransactionSwap) throw new Error('method: PayAdminTransactionSwap is not implemented')
app.post('/api/admin/swap/transaction/pay', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'PayAdminTransactionSwap', batch: false, nostr: false, batchSize: 0}
const stats: Types.RequestStats = { startMs:req.startTimeMs || 0, start:req.startTime || 0n, parse: process.hrtime.bigint(), guard: 0n, validate: 0n, handle: 0n }
let authCtx: Types.AuthContext = {}
try {
if (!methods.PayAdminTransactionSwap) throw new Error('method: PayAdminTransactionSwap is not implemented')
const authContext = await opts.AdminAuthGuard(req.headers['authorization'])
authCtx = authContext
stats.guard = process.hrtime.bigint()
const request = req.body
const error = Types.TransactionSwapQuoteRequestValidate(request)
stats.validate = process.hrtime.bigint()
if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authContext }, opts.metricsCallback)
const query = req.query
const params = req.params
const response = await methods.PayAdminTransactionSwap({rpcName:'PayAdminTransactionSwap', ctx:authContext , req: request})
stats.handle = process.hrtime.bigint()
res.json({status: 'OK', ...response})
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
})
if (!opts.allowNotImplementedMethods && !methods.PayAppUserInvoice) throw new Error('method: PayAppUserInvoice is not implemented')
app.post('/api/app/invoice/pay', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'PayAppUserInvoice', batch: false, nostr: false, batchSize: 0}

View file

@ -273,6 +273,20 @@ export default (params: ClientParams) => ({
}
return { status: 'ERROR', reason: 'invalid response' }
},
GetAdminTransactionSwapQuote: async (request: Types.TransactionSwapRequest): Promise<ResultError | ({ status: 'OK' }& Types.TransactionSwapQuote)> => {
const auth = await params.retrieveAdminAuth()
if (auth === null) throw new Error('retrieveAdminAuth() returned null')
let finalRoute = '/api/admin/swap/transaction/quote'
const { data } = await axios.post(params.baseUrl + finalRoute, request, { headers: { 'authorization': auth } })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') {
const result = data
if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.TransactionSwapQuoteValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
GetApp: async (): Promise<ResultError | ({ status: 'OK' }& Types.Application)> => {
const auth = await params.retrieveAppAuth()
if (auth === null) throw new Error('retrieveAppAuth() returned null')
@ -881,6 +895,20 @@ export default (params: ClientParams) => ({
}
return { status: 'ERROR', reason: 'invalid response' }
},
PayAdminTransactionSwap: async (request: Types.TransactionSwapQuoteRequest): Promise<ResultError | ({ status: 'OK' }& Types.AdminSwapResponse)> => {
const auth = await params.retrieveAdminAuth()
if (auth === null) throw new Error('retrieveAdminAuth() returned null')
let finalRoute = '/api/admin/swap/transaction/pay'
const { data } = await axios.post(params.baseUrl + finalRoute, request, { headers: { 'authorization': auth } })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') {
const result = data
if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.AdminSwapResponseValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
PayAppUserInvoice: async (request: Types.PayAppUserInvoiceRequest): Promise<ResultError | ({ status: 'OK' }& Types.PayInvoiceResponse)> => {
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' }
},
GetAdminTransactionSwapQuote: async (request: Types.TransactionSwapRequest): Promise<ResultError | ({ status: 'OK' }& Types.TransactionSwapQuote)> => {
const auth = await params.retrieveNostrAdminAuth()
if (auth === null) throw new Error('retrieveNostrAdminAuth() returned null')
const nostrRequest: NostrRequest = {}
nostrRequest.body = request
const data = await send(params.pubDestination, {rpcName:'GetAdminTransactionSwapQuote',authIdentifier:auth, ...nostrRequest })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') {
const result = data
if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.TransactionSwapQuoteValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
GetAppsMetrics: async (request: Types.AppsMetricsRequest): Promise<ResultError | ({ status: 'OK' }& Types.AppsMetrics)> => {
const auth = await params.retrieveNostrMetricsAuth()
if (auth === null) throw new Error('retrieveNostrMetricsAuth() returned null')
@ -769,6 +784,21 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ
}
return { status: 'ERROR', reason: 'invalid response' }
},
PayAdminTransactionSwap: async (request: Types.TransactionSwapQuoteRequest): Promise<ResultError | ({ status: 'OK' }& Types.AdminSwapResponse)> => {
const auth = await params.retrieveNostrAdminAuth()
if (auth === null) throw new Error('retrieveNostrAdminAuth() returned null')
const nostrRequest: NostrRequest = {}
nostrRequest.body = request
const data = await send(params.pubDestination, {rpcName:'PayAdminTransactionSwap',authIdentifier:auth, ...nostrRequest })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') {
const result = data
if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.AdminSwapResponseValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
PayInvoice: async (request: Types.PayInvoiceRequest): Promise<ResultError | ({ status: 'OK' }& Types.PayInvoiceResponse)> => {
const auth = await params.retrieveNostrUserAuth()
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')

View file

@ -687,6 +687,22 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => {
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
}catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
break
case 'GetAdminTransactionSwapQuote':
try {
if (!methods.GetAdminTransactionSwapQuote) throw new Error('method: GetAdminTransactionSwapQuote is not implemented')
const authContext = await opts.NostrAdminAuthGuard(req.appId, req.authIdentifier)
stats.guard = process.hrtime.bigint()
authCtx = authContext
const request = req.body
const error = Types.TransactionSwapRequestValidate(request)
stats.validate = process.hrtime.bigint()
if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback)
const response = await methods.GetAdminTransactionSwapQuote({rpcName:'GetAdminTransactionSwapQuote', ctx:authContext , req: request})
stats.handle = process.hrtime.bigint()
res({status: 'OK', ...response})
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
}catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
break
case 'GetAppsMetrics':
try {
if (!methods.GetAppsMetrics) throw new Error('method: GetAppsMetrics is not implemented')
@ -1225,6 +1241,22 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => {
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
}catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
break
case 'PayAdminTransactionSwap':
try {
if (!methods.PayAdminTransactionSwap) throw new Error('method: PayAdminTransactionSwap is not implemented')
const authContext = await opts.NostrAdminAuthGuard(req.appId, req.authIdentifier)
stats.guard = process.hrtime.bigint()
authCtx = authContext
const request = req.body
const error = Types.TransactionSwapQuoteRequestValidate(request)
stats.validate = process.hrtime.bigint()
if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback)
const response = await methods.PayAdminTransactionSwap({rpcName:'PayAdminTransactionSwap', ctx:authContext , req: request})
stats.handle = process.hrtime.bigint()
res({status: 'OK', ...response})
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
}catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
break
case 'PayInvoice':
try {
if (!methods.PayInvoice) throw new Error('method: PayInvoice is not implemented')

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -61,7 +61,7 @@ export default class {
swaps: Swaps
invoiceLock: InvoiceLock
metrics: Metrics
constructor(storage: Storage, metrics: Metrics, lnd: LND, settings: SettingsManager, liquidityManager: LiquidityManager, utils: Utils, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb) {
constructor(storage: Storage, metrics: Metrics, lnd: LND, swaps: Swaps, settings: SettingsManager, liquidityManager: LiquidityManager, utils: Utils, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb) {
this.storage = storage
this.metrics = metrics
this.settings = settings
@ -69,7 +69,7 @@ export default class {
this.liquidityManager = liquidityManager
this.utils = utils
this.watchDog = new Watchdog(settings, this.liquidityManager, this.lnd, this.storage, this.utils, this.liquidityManager.rugPullTracker)
this.swaps = new Swaps(settings)
this.swaps = swaps
this.addressPaidCb = addressPaidCb
this.invoicePaidCb = invoicePaidCb
this.invoiceLock = new InvoiceLock()
@ -539,53 +539,15 @@ export default class {
}
}
async GetTransactionSwapQuote(ctx: Types.UserContext, req: Types.TransactionSwapRequest): Promise<Types.TransactionSwapQuote> {
const feesRes = await this.swaps.reverseSwaps.GetFees()
if (!feesRes.ok) {
throw new Error(feesRes.error)
}
const { claim, lockup } = feesRes.fees.minerFees
const minerFee = claim + lockup
const chainTotal = req.transaction_amount_sats + minerFee
const res = await this.swaps.reverseSwaps.SwapTransaction(chainTotal)
if (!res.ok) {
throw new Error(res.error)
}
const decoded = await this.lnd.DecodeInvoice(res.createdResponse.invoice)
const swapFee = decoded.numSatoshis - chainTotal
const app = await this.storage.applicationStorage.GetApplication(ctx.app_id)
return this.swaps.GetTxSwapQuote(ctx.app_user_id, req.transaction_amount_sats, decodedAmt => {
const isManagedUser = ctx.user_id !== app.owner.user_id
const serviceFee = this.getSendServiceFee(Types.UserOperationType.OUTGOING_INVOICE, decoded.numSatoshis, isManagedUser)
const newSwap = await this.storage.paymentStorage.AddTransactionSwap({
app_user_id: ctx.app_user_id,
swap_quote_id: res.createdResponse.id,
swap_tree: JSON.stringify(res.createdResponse.swapTree),
lockup_address: res.createdResponse.lockupAddress,
refund_public_key: res.createdResponse.refundPublicKey,
timeout_block_height: res.createdResponse.timeoutBlockHeight,
invoice: res.createdResponse.invoice,
invoice_amount: decoded.numSatoshis,
transaction_amount: chainTotal,
swap_fee_sats: swapFee,
chain_fee_sats: minerFee,
preimage: res.preimage,
ephemeral_private_key: res.privKey,
ephemeral_public_key: res.pubkey,
return this.getSendServiceFee(Types.UserOperationType.OUTGOING_INVOICE, decodedAmt, isManagedUser)
})
return {
swap_operation_id: newSwap.swap_operation_id,
swap_fee_sats: swapFee,
invoice_amount_sats: decoded.numSatoshis,
transaction_amount_sats: req.transaction_amount_sats,
chain_fee_sats: minerFee,
service_fee_sats: serviceFee,
}
}
async PayAddress(ctx: Types.UserContext, req: Types.PayAddressRequest): Promise<Types.PayAddressResponse> {
await this.watchDog.PaymentRequested()
@ -607,66 +569,18 @@ export default class {
throw new Error("request a swap quote before paying an external address")
}
const app = await this.storage.applicationStorage.GetApplication(ctx.app_id)
const txSwap = await this.storage.paymentStorage.GetTransactionSwap(req.swap_operation_id, ctx.app_user_id)
if (!txSwap) {
throw new Error("swap quote not found")
}
const info = await this.lnd.GetInfo()
if (info.blockHeight >= txSwap.timeout_block_height) {
throw new Error("swap timeout")
}
const keys = this.swaps.GetKeys(txSwap.ephemeral_private_key)
const data: TransactionSwapData = {
createdResponse: {
id: txSwap.swap_quote_id,
invoice: txSwap.invoice,
lockupAddress: txSwap.lockup_address,
refundPublicKey: txSwap.refund_public_key,
swapTree: txSwap.swap_tree,
timeoutBlockHeight: txSwap.timeout_block_height,
onchainAmount: txSwap.transaction_amount,
},
info: {
destinationAddress: req.address,
keys,
chainFee: txSwap.chain_fee_sats,
preimage: Buffer.from(txSwap.preimage, 'hex'),
}
}
// the swap and the invoice payment are linked, swap will not start until the invoice payment is started, and will not complete once the invoice payment is completed
let swapResult = { ok: false, error: "swap never completed" } as { ok: true, txId: string } | { ok: false, error: string }
this.swaps.reverseSwaps.SubscribeToTransactionSwap(data, result => {
swapResult = result
})
let payment: Types.PayInvoiceResponse
try {
const swap = await this.swaps.PayAddrWithSwap(ctx.app_user_id, req.swap_operation_id, req.address, async (invoice) => {
payment = await this.PayInvoice(ctx.user_id, {
amount: 0,
invoice: txSwap.invoice
invoice: invoice
}, app, { swapOperationId: req.swap_operation_id })
if (!swapResult.ok) {
this.log("invoice payment successful, but swap failed")
await this.storage.paymentStorage.FailTransactionSwap(req.swap_operation_id, req.address, swapResult.error)
throw new Error(swapResult.error)
}
this.log("swap completed successfully")
await this.storage.paymentStorage.FinalizeTransactionSwap(req.swap_operation_id, req.address, swapResult.txId)
} catch (err: any) {
if (swapResult.ok) {
this.log("failed to pay swap invoice, but swap completed successfully", swapResult.txId)
await this.storage.paymentStorage.FailTransactionSwap(req.swap_operation_id, req.address, err.message)
} else {
this.log("failed to pay swap invoice and swap failed", swapResult.error)
await this.storage.paymentStorage.FailTransactionSwap(req.swap_operation_id, req.address, swapResult.error)
}
throw err
}
const networkFeesTotal = txSwap.chain_fee_sats + txSwap.swap_fee_sats // + payment.network_fee
})
return {
txId: swapResult.txId,
network_fee: networkFeesTotal,
service_fee: payment.service_fee,
operation_id: payment.operation_id,
txId: swap.txId,
network_fee: swap.network_fee,
service_fee: payment!.service_fee,
operation_id: payment!.operation_id,
}
}

View file

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