Add timeouts to all wallet API calls (#1722)
* Add timeout to all wallet API calls * Pass timeout signal to wallet API * Fix timeout error message not shown on timeout * Fix cross-fetch throws generic error message on abort * Fix wrong method in error message * Always use FetchTimeoutError * Catch NDK timeout error to replace with custom timeout error * Also use 15s for NWC connect timeout * Add timeout delay
This commit is contained in:
parent
819d382494
commit
62a922247d
|
@ -8,7 +8,8 @@ import { SELECT, itemQueryWithMeta } from './item'
|
|||
import { formatMsats, msatsToSats, msatsToSatsDecimal, satsToMsats } from '@/lib/format'
|
||||
import {
|
||||
USER_ID, INVOICE_RETENTION_DAYS,
|
||||
PAID_ACTION_PAYMENT_METHODS
|
||||
PAID_ACTION_PAYMENT_METHODS,
|
||||
WALLET_CREATE_INVOICE_TIMEOUT_MS
|
||||
} from '@/lib/constants'
|
||||
import { amountSchema, validateSchema, withdrawlSchema, lnAddrSchema } from '@/lib/validate'
|
||||
import assertGofacYourself from './ofac'
|
||||
|
@ -24,6 +25,7 @@ import validateWallet from '@/wallets/validate'
|
|||
import { canReceive } from '@/wallets/common'
|
||||
import performPaidAction from '../paidAction'
|
||||
import performPayingAction from '../payingAction'
|
||||
import { timeoutSignal, withTimeout } from '@/lib/time'
|
||||
|
||||
function injectResolvers (resolvers) {
|
||||
console.group('injected GraphQL resolvers:')
|
||||
|
@ -65,7 +67,12 @@ function injectResolvers (resolvers) {
|
|||
wallet,
|
||||
testCreateInvoice:
|
||||
walletDef.testCreateInvoice && validateLightning && canReceive({ def: walletDef, config: data })
|
||||
? (data) => walletDef.testCreateInvoice(data, { logger })
|
||||
? (data) => withTimeout(
|
||||
walletDef.testCreateInvoice(data, {
|
||||
logger,
|
||||
signal: timeoutSignal(WALLET_CREATE_INVOICE_TIMEOUT_MS)
|
||||
}),
|
||||
WALLET_CREATE_INVOICE_TIMEOUT_MS)
|
||||
: null
|
||||
}, {
|
||||
settings,
|
||||
|
|
52
lib/cln.js
52
lib/cln.js
|
@ -2,30 +2,44 @@ import fetch from 'cross-fetch'
|
|||
import crypto from 'crypto'
|
||||
import { getAgent } from '@/lib/proxy'
|
||||
import { assertContentTypeJson, assertResponseOk } from './url'
|
||||
import { FetchTimeoutError } from './fetch'
|
||||
import { WALLET_CREATE_INVOICE_TIMEOUT_MS } from './constants'
|
||||
|
||||
export const createInvoice = async ({ msats, description, expiry }, { socket, rune, cert }) => {
|
||||
export const createInvoice = async ({ msats, description, expiry }, { socket, rune, cert }, { signal }) => {
|
||||
const agent = getAgent({ hostname: socket, cert })
|
||||
|
||||
const url = `${agent.protocol}//${socket}/v1/invoice`
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Rune: rune,
|
||||
// can be any node id, only required for CLN v23.08 and below
|
||||
// see https://docs.corelightning.org/docs/rest#server
|
||||
nodeId: '02cb2e2d5a6c5b17fa67b1a883e2973c82e328fb9bd08b2b156a9e23820c87a490'
|
||||
},
|
||||
agent,
|
||||
body: JSON.stringify({
|
||||
// CLN requires a unique label for every invoice
|
||||
// see https://docs.corelightning.org/reference/lightning-invoice
|
||||
label: crypto.randomBytes(16).toString('hex'),
|
||||
description,
|
||||
amount_msat: msats,
|
||||
expiry
|
||||
|
||||
let res
|
||||
try {
|
||||
res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Rune: rune,
|
||||
// can be any node id, only required for CLN v23.08 and below
|
||||
// see https://docs.corelightning.org/docs/rest#server
|
||||
nodeId: '02cb2e2d5a6c5b17fa67b1a883e2973c82e328fb9bd08b2b156a9e23820c87a490'
|
||||
},
|
||||
agent,
|
||||
body: JSON.stringify({
|
||||
// CLN requires a unique label for every invoice
|
||||
// see https://docs.corelightning.org/reference/lightning-invoice
|
||||
label: crypto.randomBytes(16).toString('hex'),
|
||||
description,
|
||||
amount_msat: msats,
|
||||
expiry
|
||||
}),
|
||||
signal
|
||||
})
|
||||
})
|
||||
} catch (err) {
|
||||
if (err.name === 'AbortError') {
|
||||
// XXX node-fetch doesn't throw our custom TimeoutError but throws a generic error so we have to handle that manually.
|
||||
// see https://github.com/node-fetch/node-fetch/issues/1462
|
||||
throw new FetchTimeoutError('POST', url, WALLET_CREATE_INVOICE_TIMEOUT_MS)
|
||||
}
|
||||
throw err
|
||||
}
|
||||
|
||||
assertResponseOk(res)
|
||||
assertContentTypeJson(res)
|
||||
|
|
|
@ -191,3 +191,6 @@ export const LONG_POLL_INTERVAL = Number(process.env.NEXT_PUBLIC_LONG_POLL_INTER
|
|||
export const EXTRA_LONG_POLL_INTERVAL = Number(process.env.NEXT_PUBLIC_EXTRA_LONG_POLL_INTERVAL)
|
||||
|
||||
export const ZAP_UNDO_DELAY_MS = 5_000
|
||||
|
||||
export const WALLET_SEND_PAYMENT_TIMEOUT_MS = 15_000
|
||||
export const WALLET_CREATE_INVOICE_TIMEOUT_MS = 15_000
|
||||
|
|
14
lib/fetch.js
14
lib/fetch.js
|
@ -1,23 +1,25 @@
|
|||
import { TimeoutError } from '@/lib/time'
|
||||
import { TimeoutError, timeoutSignal } from '@/lib/time'
|
||||
|
||||
class FetchTimeoutError extends TimeoutError {
|
||||
export class FetchTimeoutError extends TimeoutError {
|
||||
constructor (method, url, timeout) {
|
||||
super(timeout)
|
||||
this.name = 'FetchTimeoutError'
|
||||
this.message = `${method} ${url}: timeout after ${timeout / 1000}s`
|
||||
this.message = timeout
|
||||
? `${method} ${url}: timeout after ${timeout / 1000}s`
|
||||
: `${method} ${url}: timeout`
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchWithTimeout (resource, { timeout = 1000, ...options } = {}) {
|
||||
export async function fetchWithTimeout (resource, { signal, timeout = 1000, ...options } = {}) {
|
||||
try {
|
||||
return await fetch(resource, {
|
||||
...options,
|
||||
signal: AbortSignal.timeout(timeout)
|
||||
signal: signal ?? timeoutSignal(timeout)
|
||||
})
|
||||
} catch (err) {
|
||||
if (err.name === 'TimeoutError') {
|
||||
// use custom error message
|
||||
throw new FetchTimeoutError('GET', resource, timeout)
|
||||
throw new FetchTimeoutError(options.method ?? 'GET', resource, err.timeout)
|
||||
}
|
||||
throw err
|
||||
}
|
||||
|
|
12
lib/lnurl.js
12
lib/lnurl.js
|
@ -1,6 +1,8 @@
|
|||
import { createHash } from 'crypto'
|
||||
import { bech32 } from 'bech32'
|
||||
import { lnAddrSchema } from './validate'
|
||||
import { FetchTimeoutError } from '@/lib/fetch'
|
||||
import { WALLET_CREATE_INVOICE_TIMEOUT_MS } from './constants'
|
||||
|
||||
export function encodeLNUrl (url) {
|
||||
const words = bech32.toWords(Buffer.from(url.toString(), 'utf8'))
|
||||
|
@ -25,7 +27,7 @@ export function lnurlPayDescriptionHash (data) {
|
|||
return createHash('sha256').update(data).digest('hex')
|
||||
}
|
||||
|
||||
export async function lnAddrOptions (addr) {
|
||||
export async function lnAddrOptions (addr, { signal } = {}) {
|
||||
await lnAddrSchema().fields.addr.validate(addr)
|
||||
const [name, domain] = addr.split('@')
|
||||
let protocol = 'https'
|
||||
|
@ -35,12 +37,16 @@ export async function lnAddrOptions (addr) {
|
|||
}
|
||||
const unexpectedErrorMessage = `An unexpected error occurred fetching the Lightning Address metadata for ${addr}. Check the address and try again.`
|
||||
let res
|
||||
const url = `${protocol}://${domain}/.well-known/lnurlp/${name}`
|
||||
try {
|
||||
const req = await fetch(`${protocol}://${domain}/.well-known/lnurlp/${name}`)
|
||||
const req = await fetch(url, { signal })
|
||||
res = await req.json()
|
||||
} catch (err) {
|
||||
// If `fetch` fails, or if `req.json` fails, catch it here and surface a reasonable error
|
||||
console.log('Error fetching lnurlp', err)
|
||||
if (err.name === 'TimeoutError') {
|
||||
throw new FetchTimeoutError('GET', url, WALLET_CREATE_INVOICE_TIMEOUT_MS)
|
||||
}
|
||||
// If `fetch` fails, or if `req.json` fails, catch it here and surface a reasonable error
|
||||
throw new Error(unexpectedErrorMessage)
|
||||
}
|
||||
if (res.status === 'ERROR') {
|
||||
|
|
18
lib/time.js
18
lib/time.js
|
@ -132,6 +132,7 @@ export class TimeoutError extends Error {
|
|||
constructor (timeout) {
|
||||
super(`timeout after ${timeout / 1000}s`)
|
||||
this.name = 'TimeoutError'
|
||||
this.timeout = timeout
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -140,7 +141,9 @@ function timeoutPromise (timeout) {
|
|||
// if no timeout is specified, never settle
|
||||
if (!timeout) return
|
||||
|
||||
setTimeout(() => reject(new TimeoutError(timeout)), timeout)
|
||||
// delay timeout by 100ms so any parallel promise with same timeout will throw first
|
||||
const delay = 100
|
||||
setTimeout(() => reject(new TimeoutError(timeout)), timeout + delay)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -151,3 +154,16 @@ export async function withTimeout (promise, timeout) {
|
|||
export async function callWithTimeout (fn, timeout) {
|
||||
return await Promise.race([fn(), timeoutPromise(timeout)])
|
||||
}
|
||||
|
||||
// AbortSignal.timeout with our custom timeout error message
|
||||
export function timeoutSignal (timeout) {
|
||||
const controller = new AbortController()
|
||||
|
||||
if (timeout) {
|
||||
setTimeout(() => {
|
||||
controller.abort(new TimeoutError(timeout))
|
||||
}, timeout)
|
||||
}
|
||||
|
||||
return controller.signal
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { getScopes, SCOPE_READ, SCOPE_WRITE, getWallet, request } from '@/wallets/blink/common'
|
||||
export * from '@/wallets/blink'
|
||||
|
||||
export async function testSendPayment ({ apiKey, currency }, { logger }) {
|
||||
export async function testSendPayment ({ apiKey, currency }, { logger, signal }) {
|
||||
logger.info('trying to fetch ' + currency + ' wallet')
|
||||
|
||||
const scopes = await getScopes({ apiKey })
|
||||
const scopes = await getScopes({ apiKey }, { signal })
|
||||
if (!scopes.includes(SCOPE_READ)) {
|
||||
throw new Error('missing READ scope')
|
||||
}
|
||||
|
@ -13,17 +13,17 @@ export async function testSendPayment ({ apiKey, currency }, { logger }) {
|
|||
}
|
||||
|
||||
currency = currency ? currency.toUpperCase() : 'BTC'
|
||||
await getWallet({ apiKey, currency })
|
||||
await getWallet({ apiKey, currency }, { signal })
|
||||
|
||||
logger.ok(currency + ' wallet found')
|
||||
}
|
||||
|
||||
export async function sendPayment (bolt11, { apiKey, currency }) {
|
||||
const wallet = await getWallet({ apiKey, currency })
|
||||
return await payInvoice(bolt11, { apiKey, wallet })
|
||||
export async function sendPayment (bolt11, { apiKey, currency }, { signal }) {
|
||||
const wallet = await getWallet({ apiKey, currency }, { signal })
|
||||
return await payInvoice(bolt11, { apiKey, wallet }, { signal })
|
||||
}
|
||||
|
||||
async function payInvoice (bolt11, { apiKey, wallet }) {
|
||||
async function payInvoice (bolt11, { apiKey, wallet }, { signal }) {
|
||||
const out = await request({
|
||||
apiKey,
|
||||
query: `
|
||||
|
@ -53,7 +53,7 @@ async function payInvoice (bolt11, { apiKey, wallet }) {
|
|||
walletId: wallet.id
|
||||
}
|
||||
}
|
||||
})
|
||||
}, { signal })
|
||||
|
||||
const status = out.data.lnInvoicePaymentSend.status
|
||||
const errors = out.data.lnInvoicePaymentSend.errors
|
||||
|
@ -79,7 +79,7 @@ async function payInvoice (bolt11, { apiKey, wallet }) {
|
|||
// at some point it should either be settled or fail on the backend, so the loop will exit
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
|
||||
const txInfo = await getTxInfo(bolt11, { apiKey, wallet })
|
||||
const txInfo = await getTxInfo(bolt11, { apiKey, wallet }, { signal })
|
||||
// settled
|
||||
if (txInfo.status === 'SUCCESS') {
|
||||
if (!txInfo.preImage) throw new Error('no preimage')
|
||||
|
@ -98,7 +98,7 @@ async function payInvoice (bolt11, { apiKey, wallet }) {
|
|||
throw new Error('unexpected error')
|
||||
}
|
||||
|
||||
async function getTxInfo (bolt11, { apiKey, wallet }) {
|
||||
async function getTxInfo (bolt11, { apiKey, wallet }, { signal }) {
|
||||
let out
|
||||
try {
|
||||
out = await request({
|
||||
|
@ -128,7 +128,7 @@ async function getTxInfo (bolt11, { apiKey, wallet }) {
|
|||
paymentRequest: bolt11,
|
||||
walletId: wallet.Id
|
||||
}
|
||||
})
|
||||
}, { signal })
|
||||
} catch (e) {
|
||||
// something went wrong during the query,
|
||||
// maybe the connection was lost, so we just return
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { fetchWithTimeout } from '@/lib/fetch'
|
||||
import { assertContentTypeJson, assertResponseOk } from '@/lib/url'
|
||||
|
||||
export const galoyBlinkUrl = 'https://api.blink.sv/graphql'
|
||||
|
@ -7,7 +8,7 @@ export const SCOPE_READ = 'READ'
|
|||
export const SCOPE_WRITE = 'WRITE'
|
||||
export const SCOPE_RECEIVE = 'RECEIVE'
|
||||
|
||||
export async function getWallet ({ apiKey, currency }) {
|
||||
export async function getWallet ({ apiKey, currency }, { signal }) {
|
||||
const out = await request({
|
||||
apiKey,
|
||||
query: `
|
||||
|
@ -21,7 +22,7 @@ export async function getWallet ({ apiKey, currency }) {
|
|||
}
|
||||
}
|
||||
}`
|
||||
})
|
||||
}, { signal })
|
||||
|
||||
const wallets = out.data.me.defaultAccount.wallets
|
||||
for (const wallet of wallets) {
|
||||
|
@ -33,14 +34,15 @@ export async function getWallet ({ apiKey, currency }) {
|
|||
throw new Error(`wallet ${currency} not found`)
|
||||
}
|
||||
|
||||
export async function request ({ apiKey, query, variables = {} }) {
|
||||
const res = await fetch(galoyBlinkUrl, {
|
||||
export async function request ({ apiKey, query, variables = {} }, { signal }) {
|
||||
const res = await fetchWithTimeout(galoyBlinkUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-API-KEY': apiKey
|
||||
},
|
||||
body: JSON.stringify({ query, variables })
|
||||
body: JSON.stringify({ query, variables }),
|
||||
signal
|
||||
})
|
||||
|
||||
assertResponseOk(res)
|
||||
|
@ -49,7 +51,7 @@ export async function request ({ apiKey, query, variables = {} }) {
|
|||
return res.json()
|
||||
}
|
||||
|
||||
export async function getScopes ({ apiKey }) {
|
||||
export async function getScopes ({ apiKey }, { signal }) {
|
||||
const out = await request({
|
||||
apiKey,
|
||||
query: `
|
||||
|
@ -58,7 +60,7 @@ export async function getScopes ({ apiKey }) {
|
|||
scopes
|
||||
}
|
||||
}`
|
||||
})
|
||||
}, { signal })
|
||||
const scopes = out?.data?.authorization?.scopes
|
||||
return scopes || []
|
||||
}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import { withTimeout } from '@/lib/time'
|
||||
import { getScopes, SCOPE_READ, SCOPE_RECEIVE, SCOPE_WRITE, getWallet, request } from '@/wallets/blink/common'
|
||||
import { msatsToSats } from '@/lib/format'
|
||||
export * from '@/wallets/blink'
|
||||
|
||||
export async function testCreateInvoice ({ apiKeyRecv, currencyRecv }) {
|
||||
const scopes = await getScopes({ apiKey: apiKeyRecv })
|
||||
export async function testCreateInvoice ({ apiKeyRecv, currencyRecv }, { signal }) {
|
||||
const scopes = await getScopes({ apiKey: apiKeyRecv }, { signal })
|
||||
if (!scopes.includes(SCOPE_READ)) {
|
||||
throw new Error('missing READ scope')
|
||||
}
|
||||
|
@ -15,17 +14,17 @@ export async function testCreateInvoice ({ apiKeyRecv, currencyRecv }) {
|
|||
throw new Error('missing RECEIVE scope')
|
||||
}
|
||||
|
||||
const timeout = 15_000
|
||||
currencyRecv = currencyRecv ? currencyRecv.toUpperCase() : 'BTC'
|
||||
return await withTimeout(createInvoice({ msats: 1000, expiry: 1 }, { apiKeyRecv, currencyRecv }), timeout)
|
||||
return await createInvoice({ msats: 1000, expiry: 1 }, { apiKeyRecv, currencyRecv }, { signal })
|
||||
}
|
||||
|
||||
export async function createInvoice (
|
||||
{ msats, description, expiry },
|
||||
{ apiKeyRecv: apiKey, currencyRecv: currency }) {
|
||||
{ apiKeyRecv: apiKey, currencyRecv: currency },
|
||||
{ signal }) {
|
||||
currency = currency ? currency.toUpperCase() : 'BTC'
|
||||
|
||||
const wallet = await getWallet({ apiKey, currency })
|
||||
const wallet = await getWallet({ apiKey, currency }, { signal })
|
||||
|
||||
if (currency !== 'BTC') {
|
||||
throw new Error('unsupported currency ' + currency)
|
||||
|
@ -52,7 +51,7 @@ export async function createInvoice (
|
|||
walletId: wallet.id
|
||||
}
|
||||
}
|
||||
})
|
||||
}, { signal })
|
||||
|
||||
const res = out.data.lnInvoiceCreate
|
||||
const errors = res.errors
|
||||
|
|
|
@ -2,14 +2,14 @@ import { createInvoice as clnCreateInvoice } from '@/lib/cln'
|
|||
|
||||
export * from '@/wallets/cln'
|
||||
|
||||
export const testCreateInvoice = async ({ socket, rune, cert }) => {
|
||||
return await createInvoice({ msats: 1000, expiry: 1, description: '' }, { socket, rune, cert })
|
||||
export const testCreateInvoice = async ({ socket, rune, cert }, { signal }) => {
|
||||
return await createInvoice({ msats: 1000, expiry: 1, description: '' }, { socket, rune, cert }, { signal })
|
||||
}
|
||||
|
||||
export const createInvoice = async (
|
||||
{ msats, description, expiry },
|
||||
{ socket, rune, cert }
|
||||
) => {
|
||||
{ socket, rune, cert },
|
||||
{ signal }) => {
|
||||
const inv = await clnCreateInvoice(
|
||||
{
|
||||
msats,
|
||||
|
@ -20,7 +20,8 @@ export const createInvoice = async (
|
|||
socket,
|
||||
rune,
|
||||
cert
|
||||
})
|
||||
},
|
||||
{ signal })
|
||||
|
||||
return inv.bolt11
|
||||
}
|
||||
|
|
|
@ -8,6 +8,8 @@ import { REMOVE_WALLET } from '@/fragments/wallet'
|
|||
import { useWalletLogger } from '@/wallets/logger'
|
||||
import { useWallets } from '.'
|
||||
import validateWallet from './validate'
|
||||
import { WALLET_SEND_PAYMENT_TIMEOUT_MS } from '@/lib/constants'
|
||||
import { timeoutSignal, withTimeout } from '@/lib/time'
|
||||
|
||||
export function useWalletConfigurator (wallet) {
|
||||
const { me } = useMe()
|
||||
|
@ -43,7 +45,13 @@ export function useWalletConfigurator (wallet) {
|
|||
clientConfig = Object.assign(clientConfig, transformedConfig)
|
||||
}
|
||||
if (wallet.def.testSendPayment && validateLightning) {
|
||||
transformedConfig = await wallet.def.testSendPayment(clientConfig, { logger })
|
||||
transformedConfig = await withTimeout(
|
||||
wallet.def.testSendPayment(clientConfig, {
|
||||
logger,
|
||||
signal: timeoutSignal(WALLET_SEND_PAYMENT_TIMEOUT_MS)
|
||||
}),
|
||||
WALLET_SEND_PAYMENT_TIMEOUT_MS
|
||||
)
|
||||
if (transformedConfig) {
|
||||
clientConfig = Object.assign(clientConfig, transformedConfig)
|
||||
}
|
||||
|
|
|
@ -1,18 +1,20 @@
|
|||
import { fetchWithTimeout } from '@/lib/fetch'
|
||||
import { msatsSatsFloor } from '@/lib/format'
|
||||
import { lnAddrOptions } from '@/lib/lnurl'
|
||||
import { assertContentTypeJson, assertResponseOk } from '@/lib/url'
|
||||
|
||||
export * from '@/wallets/lightning-address'
|
||||
|
||||
export const testCreateInvoice = async ({ address }) => {
|
||||
return await createInvoice({ msats: 1000 }, { address })
|
||||
export const testCreateInvoice = async ({ address }, { signal }) => {
|
||||
return await createInvoice({ msats: 1000 }, { address }, { signal })
|
||||
}
|
||||
|
||||
export const createInvoice = async (
|
||||
{ msats, description },
|
||||
{ address }
|
||||
{ address },
|
||||
{ signal }
|
||||
) => {
|
||||
const { callback, commentAllowed } = await lnAddrOptions(address)
|
||||
const { callback, commentAllowed } = await lnAddrOptions(address, { signal })
|
||||
const callbackUrl = new URL(callback)
|
||||
|
||||
// most lnurl providers suck nards so we have to floor to nearest sat
|
||||
|
@ -25,7 +27,7 @@ export const createInvoice = async (
|
|||
}
|
||||
|
||||
// call callback with amount and conditionally comment
|
||||
const res = await fetch(callbackUrl.toString())
|
||||
const res = await fetchWithTimeout(callbackUrl.toString(), { signal })
|
||||
|
||||
assertResponseOk(res)
|
||||
assertContentTypeJson(res)
|
||||
|
|
|
@ -1,22 +1,23 @@
|
|||
import { fetchWithTimeout } from '@/lib/fetch'
|
||||
import { assertContentTypeJson } from '@/lib/url'
|
||||
|
||||
export * from '@/wallets/lnbits'
|
||||
|
||||
export async function testSendPayment ({ url, adminKey, invoiceKey }, { logger }) {
|
||||
export async function testSendPayment ({ url, adminKey, invoiceKey }, { signal, logger }) {
|
||||
logger.info('trying to fetch wallet')
|
||||
|
||||
url = url.replace(/\/+$/, '')
|
||||
await getWallet({ url, adminKey, invoiceKey })
|
||||
await getWallet({ url, adminKey, invoiceKey }, { signal })
|
||||
|
||||
logger.ok('wallet found')
|
||||
}
|
||||
|
||||
export async function sendPayment (bolt11, { url, adminKey }) {
|
||||
export async function sendPayment (bolt11, { url, adminKey }, { signal }) {
|
||||
url = url.replace(/\/+$/, '')
|
||||
|
||||
const response = await postPayment(bolt11, { url, adminKey })
|
||||
const response = await postPayment(bolt11, { url, adminKey }, { signal })
|
||||
|
||||
const checkResponse = await getPayment(response.payment_hash, { url, adminKey })
|
||||
const checkResponse = await getPayment(response.payment_hash, { url, adminKey }, { signal })
|
||||
if (!checkResponse.preimage) {
|
||||
throw new Error('No preimage')
|
||||
}
|
||||
|
@ -24,7 +25,7 @@ export async function sendPayment (bolt11, { url, adminKey }) {
|
|||
return checkResponse.preimage
|
||||
}
|
||||
|
||||
async function getWallet ({ url, adminKey, invoiceKey }) {
|
||||
async function getWallet ({ url, adminKey, invoiceKey }, { signal }) {
|
||||
const path = '/api/v1/wallet'
|
||||
|
||||
const headers = new Headers()
|
||||
|
@ -32,7 +33,7 @@ async function getWallet ({ url, adminKey, invoiceKey }) {
|
|||
headers.append('Content-Type', 'application/json')
|
||||
headers.append('X-Api-Key', adminKey || invoiceKey)
|
||||
|
||||
const res = await fetch(url + path, { method: 'GET', headers })
|
||||
const res = await fetchWithTimeout(url + path, { method: 'GET', headers, signal })
|
||||
|
||||
assertContentTypeJson(res)
|
||||
if (!res.ok) {
|
||||
|
@ -44,7 +45,7 @@ async function getWallet ({ url, adminKey, invoiceKey }) {
|
|||
return wallet
|
||||
}
|
||||
|
||||
async function postPayment (bolt11, { url, adminKey }) {
|
||||
async function postPayment (bolt11, { url, adminKey }, { signal }) {
|
||||
const path = '/api/v1/payments'
|
||||
|
||||
const headers = new Headers()
|
||||
|
@ -54,7 +55,7 @@ async function postPayment (bolt11, { url, adminKey }) {
|
|||
|
||||
const body = JSON.stringify({ bolt11, out: true })
|
||||
|
||||
const res = await fetch(url + path, { method: 'POST', headers, body })
|
||||
const res = await fetchWithTimeout(url + path, { method: 'POST', headers, body, signal })
|
||||
|
||||
assertContentTypeJson(res)
|
||||
if (!res.ok) {
|
||||
|
@ -66,7 +67,7 @@ async function postPayment (bolt11, { url, adminKey }) {
|
|||
return payment
|
||||
}
|
||||
|
||||
async function getPayment (paymentHash, { url, adminKey }) {
|
||||
async function getPayment (paymentHash, { url, adminKey }, { signal }) {
|
||||
const path = `/api/v1/payments/${paymentHash}`
|
||||
|
||||
const headers = new Headers()
|
||||
|
@ -74,7 +75,7 @@ async function getPayment (paymentHash, { url, adminKey }) {
|
|||
headers.append('Content-Type', 'application/json')
|
||||
headers.append('X-Api-Key', adminKey)
|
||||
|
||||
const res = await fetch(url + path, { method: 'GET', headers })
|
||||
const res = await fetchWithTimeout(url + path, { method: 'GET', headers, signal })
|
||||
|
||||
assertContentTypeJson(res)
|
||||
if (!res.ok) {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { WALLET_CREATE_INVOICE_TIMEOUT_MS } from '@/lib/constants'
|
||||
import { FetchTimeoutError } from '@/lib/fetch'
|
||||
import { msatsToSats } from '@/lib/format'
|
||||
import { getAgent } from '@/lib/proxy'
|
||||
import { assertContentTypeJson } from '@/lib/url'
|
||||
|
@ -5,13 +7,14 @@ import fetch from 'cross-fetch'
|
|||
|
||||
export * from '@/wallets/lnbits'
|
||||
|
||||
export async function testCreateInvoice ({ url, invoiceKey }) {
|
||||
return await createInvoice({ msats: 1000, expiry: 1 }, { url, invoiceKey })
|
||||
export async function testCreateInvoice ({ url, invoiceKey }, { signal }) {
|
||||
return await createInvoice({ msats: 1000, expiry: 1 }, { url, invoiceKey }, { signal })
|
||||
}
|
||||
|
||||
export async function createInvoice (
|
||||
{ msats, description, descriptionHash, expiry },
|
||||
{ url, invoiceKey }) {
|
||||
{ url, invoiceKey },
|
||||
{ signal }) {
|
||||
const path = '/api/v1/payments'
|
||||
|
||||
const headers = new Headers()
|
||||
|
@ -38,12 +41,23 @@ export async function createInvoice (
|
|||
hostname = 'lnbits:5000'
|
||||
}
|
||||
|
||||
const res = await fetch(`${agent.protocol}//${hostname}${path}`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
agent,
|
||||
body
|
||||
})
|
||||
let res
|
||||
try {
|
||||
res = await fetch(`${agent.protocol}//${hostname}${path}`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
agent,
|
||||
body,
|
||||
signal
|
||||
})
|
||||
} catch (err) {
|
||||
if (err.name === 'AbortError') {
|
||||
// XXX node-fetch doesn't throw our custom TimeoutError but throws a generic error so we have to handle that manually.
|
||||
// see https://github.com/node-fetch/node-fetch/issues/1462
|
||||
throw new FetchTimeoutError('POST', url, WALLET_CREATE_INVOICE_TIMEOUT_MS)
|
||||
}
|
||||
throw err
|
||||
}
|
||||
|
||||
assertContentTypeJson(res)
|
||||
if (!res.ok) {
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
import { getNwc, supportedMethods, nwcTryRun } from '@/wallets/nwc'
|
||||
export * from '@/wallets/nwc'
|
||||
|
||||
export async function testSendPayment ({ nwcUrl }) {
|
||||
const timeout = 15_000
|
||||
|
||||
const supported = await supportedMethods(nwcUrl, { timeout })
|
||||
export async function testSendPayment ({ nwcUrl }, { signal }) {
|
||||
const supported = await supportedMethods(nwcUrl, { signal })
|
||||
if (!supported.includes('pay_invoice')) {
|
||||
throw new Error('pay_invoice not supported')
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendPayment (bolt11, { nwcUrl }) {
|
||||
const nwc = await getNwc(nwcUrl)
|
||||
export async function sendPayment (bolt11, { nwcUrl }, { signal }) {
|
||||
const nwc = await getNwc(nwcUrl, { signal })
|
||||
// TODO: support AbortSignal
|
||||
const result = await nwcTryRun(() => nwc.payInvoice(bolt11))
|
||||
return result.preimage
|
||||
}
|
||||
|
|
|
@ -2,6 +2,9 @@ import Nostr from '@/lib/nostr'
|
|||
import { string } from '@/lib/yup'
|
||||
import { parseNwcUrl } from '@/lib/url'
|
||||
import { NDKNwc } from '@nostr-dev-kit/ndk'
|
||||
import { TimeoutError } from '@/lib/time'
|
||||
|
||||
const NWC_CONNECT_TIMEOUT_MS = 15_000
|
||||
|
||||
export const name = 'nwc'
|
||||
export const walletType = 'NWC'
|
||||
|
@ -33,7 +36,7 @@ export const card = {
|
|||
subtitle: 'use Nostr Wallet Connect for payments'
|
||||
}
|
||||
|
||||
export async function getNwc (nwcUrl, { timeout = 5e4 } = {}) {
|
||||
export async function getNwc (nwcUrl, { signal }) {
|
||||
const ndk = Nostr.ndk
|
||||
const { walletPubkey, secret, relayUrls } = parseNwcUrl(nwcUrl)
|
||||
const nwc = new NDKNwc({
|
||||
|
@ -42,7 +45,17 @@ export async function getNwc (nwcUrl, { timeout = 5e4 } = {}) {
|
|||
relayUrls,
|
||||
secret
|
||||
})
|
||||
await nwc.blockUntilReady(timeout)
|
||||
|
||||
// TODO: support AbortSignal
|
||||
try {
|
||||
await nwc.blockUntilReady(NWC_CONNECT_TIMEOUT_MS)
|
||||
} catch (err) {
|
||||
if (err.message === 'Timeout') {
|
||||
throw new TimeoutError(NWC_CONNECT_TIMEOUT_MS)
|
||||
}
|
||||
throw err
|
||||
}
|
||||
|
||||
return nwc
|
||||
}
|
||||
|
||||
|
@ -63,8 +76,9 @@ export async function nwcTryRun (fun) {
|
|||
}
|
||||
}
|
||||
|
||||
export async function supportedMethods (nwcUrl, { timeout } = {}) {
|
||||
const nwc = await getNwc(nwcUrl, { timeout })
|
||||
export async function supportedMethods (nwcUrl, { signal }) {
|
||||
const nwc = await getNwc(nwcUrl, { signal })
|
||||
// TODO: support AbortSignal
|
||||
const result = await nwcTryRun(() => nwc.getInfo())
|
||||
return result.methods
|
||||
}
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
import { withTimeout } from '@/lib/time'
|
||||
import { getNwc, supportedMethods, nwcTryRun } from '@/wallets/nwc'
|
||||
export * from '@/wallets/nwc'
|
||||
|
||||
export async function testCreateInvoice ({ nwcUrlRecv }) {
|
||||
const timeout = 15_000
|
||||
|
||||
const supported = await supportedMethods(nwcUrlRecv, { timeout })
|
||||
export async function testCreateInvoice ({ nwcUrlRecv }, { signal }) {
|
||||
const supported = await supportedMethods(nwcUrlRecv, { signal })
|
||||
|
||||
const supports = (method) => supported.includes(method)
|
||||
|
||||
|
@ -20,11 +17,12 @@ export async function testCreateInvoice ({ nwcUrlRecv }) {
|
|||
}
|
||||
}
|
||||
|
||||
return await withTimeout(createInvoice({ msats: 1000, expiry: 1 }, { nwcUrlRecv }), timeout)
|
||||
return await createInvoice({ msats: 1000, expiry: 1 }, { nwcUrlRecv }, { signal })
|
||||
}
|
||||
|
||||
export async function createInvoice ({ msats, description, expiry }, { nwcUrlRecv }) {
|
||||
const nwc = await getNwc(nwcUrlRecv)
|
||||
export async function createInvoice ({ msats, description, expiry }, { nwcUrlRecv }, { signal }) {
|
||||
const nwc = await getNwc(nwcUrlRecv, { signal })
|
||||
// TODO: support AbortSignal
|
||||
const result = await nwcTryRun(() => nwc.sendReq('make_invoice', { amount: msats, description, expiry }))
|
||||
return result.invoice
|
||||
}
|
||||
|
|
|
@ -2,13 +2,14 @@ import { useCallback } from 'react'
|
|||
import { useSendWallets } from '@/wallets'
|
||||
import { formatSats } from '@/lib/format'
|
||||
import useInvoice from '@/components/use-invoice'
|
||||
import { FAST_POLL_INTERVAL } from '@/lib/constants'
|
||||
import { FAST_POLL_INTERVAL, WALLET_SEND_PAYMENT_TIMEOUT_MS } from '@/lib/constants'
|
||||
import {
|
||||
WalletsNotAvailableError, WalletSenderError, WalletAggregateError, WalletPaymentAggregateError,
|
||||
WalletNotEnabledError, WalletSendNotConfiguredError, WalletPaymentError, WalletError, WalletReceiverError
|
||||
} from '@/wallets/errors'
|
||||
import { canSend } from './common'
|
||||
import { useWalletLoggerFactory } from './logger'
|
||||
import { timeoutSignal, withTimeout } from '@/lib/time'
|
||||
|
||||
export function useWalletPayment () {
|
||||
const wallets = useSendWallets()
|
||||
|
@ -152,7 +153,12 @@ function useSendPayment () {
|
|||
|
||||
logger.info(`↗ sending payment: ${formatSats(satsRequested)}`, { bolt11 })
|
||||
try {
|
||||
const preimage = await wallet.def.sendPayment(bolt11, wallet.config, { logger })
|
||||
const preimage = await withTimeout(
|
||||
wallet.def.sendPayment(bolt11, wallet.config, {
|
||||
logger,
|
||||
signal: timeoutSignal(WALLET_SEND_PAYMENT_TIMEOUT_MS)
|
||||
}),
|
||||
WALLET_SEND_PAYMENT_TIMEOUT_MS)
|
||||
logger.ok(`↗ payment sent: ${formatSats(satsRequested)}`, { bolt11, preimage })
|
||||
} catch (err) {
|
||||
// we don't log the error here since we want to handle receiver errors separately
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { fetchWithTimeout } from '@/lib/fetch'
|
||||
import { assertContentTypeJson, assertResponseOk } from '@/lib/url'
|
||||
|
||||
export * from '@/wallets/phoenixd'
|
||||
|
||||
export async function testSendPayment (config, { logger }) {
|
||||
export async function testSendPayment (config, { logger, signal }) {
|
||||
// TODO:
|
||||
// Not sure which endpoint to call to test primary password
|
||||
// see https://phoenix.acinq.co/server/api
|
||||
|
@ -10,7 +11,7 @@ export async function testSendPayment (config, { logger }) {
|
|||
|
||||
}
|
||||
|
||||
export async function sendPayment (bolt11, { url, primaryPassword }) {
|
||||
export async function sendPayment (bolt11, { url, primaryPassword }, { signal }) {
|
||||
// https://phoenix.acinq.co/server/api#pay-bolt11-invoice
|
||||
const path = '/payinvoice'
|
||||
|
||||
|
@ -21,10 +22,11 @@ export async function sendPayment (bolt11, { url, primaryPassword }) {
|
|||
const body = new URLSearchParams()
|
||||
body.append('invoice', bolt11)
|
||||
|
||||
const res = await fetch(url + path, {
|
||||
const res = await fetchWithTimeout(url + path, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body
|
||||
body,
|
||||
signal
|
||||
})
|
||||
|
||||
assertResponseOk(res)
|
||||
|
|
|
@ -1,17 +1,20 @@
|
|||
import { fetchWithTimeout } from '@/lib/fetch'
|
||||
import { msatsToSats } from '@/lib/format'
|
||||
import { assertContentTypeJson, assertResponseOk } from '@/lib/url'
|
||||
|
||||
export * from '@/wallets/phoenixd'
|
||||
|
||||
export async function testCreateInvoice ({ url, secondaryPassword }) {
|
||||
export async function testCreateInvoice ({ url, secondaryPassword }, { signal }) {
|
||||
return await createInvoice(
|
||||
{ msats: 1000, description: 'SN test invoice', expiry: 1 },
|
||||
{ url, secondaryPassword })
|
||||
{ url, secondaryPassword },
|
||||
{ signal })
|
||||
}
|
||||
|
||||
export async function createInvoice (
|
||||
{ msats, description, descriptionHash, expiry },
|
||||
{ url, secondaryPassword }
|
||||
{ url, secondaryPassword },
|
||||
{ signal }
|
||||
) {
|
||||
// https://phoenix.acinq.co/server/api#create-bolt11-invoice
|
||||
const path = '/createinvoice'
|
||||
|
@ -24,10 +27,11 @@ export async function createInvoice (
|
|||
body.append('description', description)
|
||||
body.append('amountSat', msatsToSats(msats))
|
||||
|
||||
const res = await fetch(url + path, {
|
||||
const res = await fetchWithTimeout(url + path, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body
|
||||
body,
|
||||
signal
|
||||
})
|
||||
|
||||
assertResponseOk(res)
|
||||
|
|
|
@ -15,8 +15,8 @@ import { walletLogger } from '@/api/resolvers/wallet'
|
|||
import walletDefs from '@/wallets/server'
|
||||
import { parsePaymentRequest } from 'ln-service'
|
||||
import { toPositiveBigInt, toPositiveNumber, formatMsats, formatSats, msatsToSats } from '@/lib/format'
|
||||
import { PAID_ACTION_TERMINAL_STATES } from '@/lib/constants'
|
||||
import { withTimeout } from '@/lib/time'
|
||||
import { PAID_ACTION_TERMINAL_STATES, WALLET_CREATE_INVOICE_TIMEOUT_MS } from '@/lib/constants'
|
||||
import { timeoutSignal, withTimeout } from '@/lib/time'
|
||||
import { canReceive } from './common'
|
||||
import wrapInvoice from './wrap'
|
||||
|
||||
|
@ -201,6 +201,9 @@ async function walletCreateInvoice ({ wallet, def }, {
|
|||
expiry
|
||||
},
|
||||
wallet.wallet,
|
||||
{ logger }
|
||||
), 10_000)
|
||||
{
|
||||
logger,
|
||||
signal: timeoutSignal(WALLET_CREATE_INVOICE_TIMEOUT_MS)
|
||||
}
|
||||
), WALLET_CREATE_INVOICE_TIMEOUT_MS)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue