Compare commits
40 Commits
080459cd21
...
6d4dfddae8
Author | SHA1 | Date | |
---|---|---|---|
|
6d4dfddae8 | ||
|
6098d39574 | ||
|
62a922247d | ||
|
819d382494 | ||
|
14de23b21d | ||
|
3cdfe620d0 | ||
|
77d22cfd77 | ||
|
bdd24130f9 | ||
|
285203889d | ||
|
a50a2c8bd1 | ||
|
d73f6323ff | ||
|
52734940a3 | ||
|
bf541aa643 | ||
|
06a3a71eb9 | ||
|
adfbdeddba | ||
|
fc4303658d | ||
|
e8434d07c5 | ||
|
959cd1f3f4 | ||
|
64ba7a56cb | ||
|
ec213907fc | ||
|
66b7352bf0 | ||
|
6918bcb452 | ||
|
98fae6c9ae | ||
|
0e765d4179 | ||
|
3fe5f4b435 | ||
|
c9439c33c6 | ||
|
8f092cdc66 | ||
|
4e6fb40c0b | ||
|
8cb89574ae | ||
|
756e75ed7c | ||
|
a46f81f1e1 | ||
|
e6c74c965b | ||
|
3ead4db8dc | ||
|
1a41760915 | ||
|
3e29e04b01 | ||
|
bf20cf8f56 | ||
|
52098a3e50 | ||
|
61fb1c445f | ||
|
d05a27a6c3 | ||
|
a5fa40aa1b |
@ -29,8 +29,8 @@ SLACK_BOT_TOKEN=
|
|||||||
SLACK_CHANNEL_ID=
|
SLACK_CHANNEL_ID=
|
||||||
|
|
||||||
# lnurl ... you'll need a tunnel to localhost:3000 for these
|
# lnurl ... you'll need a tunnel to localhost:3000 for these
|
||||||
LNAUTH_URL=
|
LNAUTH_URL=http://localhost:3000/api/lnauth
|
||||||
LNWITH_URL=
|
LNWITH_URL=http://localhost:3000/api/lnwith
|
||||||
|
|
||||||
########################################
|
########################################
|
||||||
# SNDEV STUFF WE PRESET #
|
# SNDEV STUFF WE PRESET #
|
||||||
@ -126,27 +126,42 @@ RPC_PORT=18443
|
|||||||
P2P_PORT=18444
|
P2P_PORT=18444
|
||||||
ZMQ_BLOCK_PORT=28334
|
ZMQ_BLOCK_PORT=28334
|
||||||
ZMQ_TX_PORT=28335
|
ZMQ_TX_PORT=28335
|
||||||
|
ZMQ_HASHBLOCK_PORT=29000
|
||||||
|
|
||||||
# sn lnd container stuff
|
# sn_lnd container stuff
|
||||||
LND_REST_PORT=8080
|
SN_LND_REST_PORT=8080
|
||||||
LND_GRPC_PORT=10009
|
SN_LND_GRPC_PORT=10009
|
||||||
LND_P2P_PORT=9735
|
SN_LND_P2P_PORT=9735
|
||||||
# docker exec -u lnd sn_lnd lncli newaddress p2wkh --unused
|
# docker exec -u lnd sn_lnd lncli newaddress p2wkh --unused
|
||||||
LND_ADDR=bcrt1q7q06n5st4vqq3lssn0rtkrn2qqypghv9xg2xnl
|
SN_LND_ADDR=bcrt1q7q06n5st4vqq3lssn0rtkrn2qqypghv9xg2xnl
|
||||||
LND_PUBKEY=02cb2e2d5a6c5b17fa67b1a883e2973c82e328fb9bd08b2b156a9e23820c87a490
|
SN_LND_PUBKEY=02cb2e2d5a6c5b17fa67b1a883e2973c82e328fb9bd08b2b156a9e23820c87a490
|
||||||
|
# sn_lndk stuff
|
||||||
|
SN_LNDK_GRPC_PORT=10012
|
||||||
|
|
||||||
# stacker lnd container stuff
|
# lnd container stuff
|
||||||
STACKER_LND_REST_PORT=8081
|
LND_REST_PORT=8081
|
||||||
STACKER_LND_GRPC_PORT=10010
|
LND_GRPC_PORT=10010
|
||||||
# docker exec -u lnd lnd lncli newaddress p2wkh --unused
|
# docker exec -u lnd lnd lncli newaddress p2wkh --unused
|
||||||
STACKER_LND_ADDR=bcrt1qfqau4ug9e6rtrvxrgclg58e0r93wshucumm9vu
|
LND_ADDR=bcrt1qfqau4ug9e6rtrvxrgclg58e0r93wshucumm9vu
|
||||||
STACKER_LND_PUBKEY=028093ae52e011d45b3e67f2e0f2cb6c3a1d7f88d2920d408f3ac6db3a56dc4b35
|
LND_PUBKEY=028093ae52e011d45b3e67f2e0f2cb6c3a1d7f88d2920d408f3ac6db3a56dc4b35
|
||||||
|
|
||||||
# stacker cln container stuff
|
# cln container stuff
|
||||||
STACKER_CLN_REST_PORT=9092
|
CLN_REST_PORT=9092
|
||||||
# docker exec -u clightning cln lightning-cli newaddr bech32
|
# docker exec -u clightning cln lightning-cli newaddr bech32
|
||||||
STACKER_CLN_ADDR=bcrt1q02sqd74l4pxedy24fg0qtjz4y2jq7x4lxlgzrx
|
CLN_ADDR=bcrt1q02sqd74l4pxedy24fg0qtjz4y2jq7x4lxlgzrx
|
||||||
STACKER_CLN_PUBKEY=03ca7acec181dbf5e427c682c4261a46a0dd9ea5f35d97acb094e399f727835b90
|
CLN_PUBKEY=03ca7acec181dbf5e427c682c4261a46a0dd9ea5f35d97acb094e399f727835b90
|
||||||
|
|
||||||
|
# sndev cli eclair getnewaddress
|
||||||
|
# sndev cli eclair getinfo
|
||||||
|
ECLAIR_ADDR="bcrt1qdus2yml69wsax3unz8pts9h979lc3s4tw0tpf6"
|
||||||
|
ECLAIR_PUBKEY="02268c74cc07837041131474881f97d497706b89a29f939555da6d094b65bd5af0"
|
||||||
|
|
||||||
|
# router lnd container stuff
|
||||||
|
ROUTER_LND_REST_PORT=8082
|
||||||
|
ROUTER_LND_GRPC_PORT=10011
|
||||||
|
# docker exec -u lnd router_lnd lncli newaddress p2wkh --unused
|
||||||
|
ROUTER_LND_ADDR=bcrt1qfkmwfpwgn6wt0dd36s79x04swz8vleyafsdpdr
|
||||||
|
ROUTER_LND_PUBKEY=02750991fbf62e57631888bc469fae69c5e658bd1d245d8ab95ed883517caa33c3
|
||||||
|
|
||||||
LNCLI_NETWORK=regtest
|
LNCLI_NETWORK=regtest
|
||||||
|
|
||||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -59,3 +59,6 @@ scripts/nwc-keys.json
|
|||||||
|
|
||||||
# lnbits
|
# lnbits
|
||||||
docker/lnbits/data
|
docker/lnbits/data
|
||||||
|
|
||||||
|
# lndk
|
||||||
|
!docker/lndk/tls-*.pem
|
@ -317,34 +317,39 @@ export async function retryPaidAction (actionType, args, incomingContext) {
|
|||||||
optimistic: actionOptimistic,
|
optimistic: actionOptimistic,
|
||||||
me: await models.user.findUnique({ where: { id: parseInt(me.id) } }),
|
me: await models.user.findUnique({ where: { id: parseInt(me.id) } }),
|
||||||
cost: BigInt(msatsRequested),
|
cost: BigInt(msatsRequested),
|
||||||
actionId
|
actionId,
|
||||||
|
predecessorId: failedInvoice.id
|
||||||
}
|
}
|
||||||
|
|
||||||
let invoiceArgs
|
let invoiceArgs
|
||||||
const invoiceForward = await models.invoiceForward.findUnique({
|
const invoiceForward = await models.invoiceForward.findUnique({
|
||||||
where: { invoiceId: failedInvoice.id },
|
where: {
|
||||||
|
invoiceId: failedInvoice.id
|
||||||
|
},
|
||||||
include: {
|
include: {
|
||||||
wallet: true,
|
wallet: true
|
||||||
invoice: true,
|
|
||||||
withdrawl: true
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// TODO: receiver fallbacks
|
|
||||||
// use next receiver wallet if forward failed (we currently immediately fallback to SN)
|
if (invoiceForward) {
|
||||||
const failedForward = invoiceForward?.withdrawl && invoiceForward.withdrawl.actionState !== 'CONFIRMED'
|
// this is a wrapped invoice, we need to retry it with receiver fallbacks
|
||||||
if (invoiceForward && !failedForward) {
|
try {
|
||||||
const { userId } = invoiceForward.wallet
|
const { userId } = invoiceForward.wallet
|
||||||
const { invoice: bolt11, wrappedInvoice: wrappedBolt11, wallet, maxFee } = await createWrappedInvoice(userId, {
|
// this will return an invoice from the first receiver wallet that didn't fail yet and throw if none is available
|
||||||
msats: failedInvoice.msatsRequested,
|
const { invoice: bolt11, wrappedInvoice: wrappedBolt11, wallet, maxFee } = await createWrappedInvoice(userId, {
|
||||||
feePercent: await action.getSybilFeePercent?.(actionArgs, retryContext),
|
msats: failedInvoice.msatsRequested,
|
||||||
description: await action.describe?.(actionArgs, retryContext),
|
feePercent: await action.getSybilFeePercent?.(actionArgs, retryContext),
|
||||||
expiry: INVOICE_EXPIRE_SECS
|
description: await action.describe?.(actionArgs, retryContext),
|
||||||
}, retryContext)
|
expiry: INVOICE_EXPIRE_SECS
|
||||||
invoiceArgs = { bolt11, wrappedBolt11, wallet, maxFee }
|
}, retryContext)
|
||||||
} else {
|
invoiceArgs = { bolt11, wrappedBolt11, wallet, maxFee }
|
||||||
invoiceArgs = await createSNInvoice(actionType, actionArgs, retryContext)
|
} catch (err) {
|
||||||
|
console.log('failed to retry wrapped invoice, falling back to SN:', err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
invoiceArgs ??= await createSNInvoice(actionType, actionArgs, retryContext)
|
||||||
|
|
||||||
return await models.$transaction(async tx => {
|
return await models.$transaction(async tx => {
|
||||||
const context = { ...retryContext, tx, invoiceArgs }
|
const context = { ...retryContext, tx, invoiceArgs }
|
||||||
|
|
||||||
@ -404,7 +409,7 @@ async function createSNInvoice (actionType, args, context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function createDbInvoice (actionType, args, context) {
|
async function createDbInvoice (actionType, args, context) {
|
||||||
const { me, models, tx, cost, optimistic, actionId, invoiceArgs } = context
|
const { me, models, tx, cost, optimistic, actionId, invoiceArgs, predecessorId } = context
|
||||||
const { bolt11, wrappedBolt11, preimage, wallet, maxFee } = invoiceArgs
|
const { bolt11, wrappedBolt11, preimage, wallet, maxFee } = invoiceArgs
|
||||||
|
|
||||||
const db = tx ?? models
|
const db = tx ?? models
|
||||||
@ -429,7 +434,8 @@ async function createDbInvoice (actionType, args, context) {
|
|||||||
actionOptimistic: optimistic,
|
actionOptimistic: optimistic,
|
||||||
actionArgs: args,
|
actionArgs: args,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
actionId
|
actionId,
|
||||||
|
predecessorId
|
||||||
}
|
}
|
||||||
|
|
||||||
let invoice
|
let invoice
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { LND_PATHFINDING_TIMEOUT_MS } from '@/lib/constants'
|
import { LND_PATHFINDING_TIME_PREF_PPM, LND_PATHFINDING_TIMEOUT_MS } from '@/lib/constants'
|
||||||
import { msatsToSats, satsToMsats, toPositiveBigInt } from '@/lib/format'
|
import { msatsToSats, satsToMsats, toPositiveBigInt } from '@/lib/format'
|
||||||
import { Prisma } from '@prisma/client'
|
import { Prisma } from '@prisma/client'
|
||||||
import { parsePaymentRequest, payViaPaymentRequest } from 'ln-service'
|
import { parsePaymentRequest, payViaPaymentRequest } from 'ln-service'
|
||||||
@ -44,7 +44,8 @@ export default async function performPayingAction ({ bolt11, maxFee, walletId },
|
|||||||
lnd,
|
lnd,
|
||||||
request: withdrawal.bolt11,
|
request: withdrawal.bolt11,
|
||||||
max_fee: msatsToSats(withdrawal.msatsFeePaying),
|
max_fee: msatsToSats(withdrawal.msatsFeePaying),
|
||||||
pathfinding_timeout: LND_PATHFINDING_TIMEOUT_MS
|
pathfinding_timeout: LND_PATHFINDING_TIMEOUT_MS,
|
||||||
|
confidence: LND_PATHFINDING_TIME_PREF_PPM
|
||||||
}).catch(console.error)
|
}).catch(console.error)
|
||||||
|
|
||||||
return withdrawal
|
return withdrawal
|
||||||
|
@ -8,7 +8,8 @@ import {
|
|||||||
COMMENT_DEPTH_LIMIT, COMMENT_TYPE_QUERY,
|
COMMENT_DEPTH_LIMIT, COMMENT_TYPE_QUERY,
|
||||||
USER_ID, POLL_COST, ADMIN_ITEMS, GLOBAL_SEED,
|
USER_ID, POLL_COST, ADMIN_ITEMS, GLOBAL_SEED,
|
||||||
NOFOLLOW_LIMIT, UNKNOWN_LINK_REL, SN_ADMIN_IDS,
|
NOFOLLOW_LIMIT, UNKNOWN_LINK_REL, SN_ADMIN_IDS,
|
||||||
BOOST_MULT
|
BOOST_MULT,
|
||||||
|
ITEM_EDIT_SECONDS
|
||||||
} from '@/lib/constants'
|
} from '@/lib/constants'
|
||||||
import { msatsToSats } from '@/lib/format'
|
import { msatsToSats } from '@/lib/format'
|
||||||
import { parse } from 'tldts'
|
import { parse } from 'tldts'
|
||||||
@ -1350,8 +1351,9 @@ export const updateItem = async (parent, { sub: subName, forward, hash, hmac, ..
|
|||||||
throw new GqlInputError('item is deleted')
|
throw new GqlInputError('item is deleted')
|
||||||
}
|
}
|
||||||
|
|
||||||
// author can edit their own item (except anon)
|
|
||||||
const meId = Number(me?.id ?? USER_ID.anon)
|
const meId = Number(me?.id ?? USER_ID.anon)
|
||||||
|
|
||||||
|
// author can edit their own item (except anon)
|
||||||
const authorEdit = !!me && Number(old.userId) === meId
|
const authorEdit = !!me && Number(old.userId) === meId
|
||||||
// admins can edit special items
|
// admins can edit special items
|
||||||
const adminEdit = ADMIN_ITEMS.includes(old.id) && SN_ADMIN_IDS.includes(meId)
|
const adminEdit = ADMIN_ITEMS.includes(old.id) && SN_ADMIN_IDS.includes(meId)
|
||||||
@ -1360,9 +1362,9 @@ export const updateItem = async (parent, { sub: subName, forward, hash, hmac, ..
|
|||||||
if (old.invoice?.hash && hash && hmac) {
|
if (old.invoice?.hash && hash && hmac) {
|
||||||
hmacEdit = old.invoice.hash === hash && verifyHmac(hash, hmac)
|
hmacEdit = old.invoice.hash === hash && verifyHmac(hash, hmac)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ownership permission check
|
// ownership permission check
|
||||||
if (!authorEdit && !adminEdit && !hmacEdit) {
|
const ownerEdit = authorEdit || adminEdit || hmacEdit
|
||||||
|
if (!ownerEdit) {
|
||||||
throw new GqlInputError('item does not belong to you')
|
throw new GqlInputError('item does not belong to you')
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1379,12 +1381,11 @@ export const updateItem = async (parent, { sub: subName, forward, hash, hmac, ..
|
|||||||
|
|
||||||
const user = await models.user.findUnique({ where: { id: meId } })
|
const user = await models.user.findUnique({ where: { id: meId } })
|
||||||
|
|
||||||
// prevent update if it's not explicitly allowed, not their bio, not their job and older than 10 minutes
|
// edits are only allowed for own items within 10 minutes but forever if it's their bio or a job
|
||||||
const myBio = user.bioId === old.id
|
const myBio = user.bioId === old.id
|
||||||
const timer = Date.now() < datePivot(new Date(old.invoicePaidAt ?? old.createdAt), { minutes: 10 })
|
const timer = Date.now() < datePivot(new Date(old.invoicePaidAt ?? old.createdAt), { seconds: ITEM_EDIT_SECONDS })
|
||||||
|
const canEdit = (timer && ownerEdit) || myBio || isJob(item)
|
||||||
// timer permission check
|
if (!canEdit) {
|
||||||
if (!adminEdit && !myBio && !timer && !isJob(item)) {
|
|
||||||
throw new GqlInputError('item can no longer be edited')
|
throw new GqlInputError('item can no longer be edited')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,7 +157,7 @@ export default {
|
|||||||
const [{ to, from }] = await models.$queryRaw`
|
const [{ to, from }] = await models.$queryRaw`
|
||||||
SELECT date_trunc('day', (now() AT TIME ZONE 'America/Chicago')) AT TIME ZONE 'America/Chicago' as from,
|
SELECT date_trunc('day', (now() AT TIME ZONE 'America/Chicago')) AT TIME ZONE 'America/Chicago' as from,
|
||||||
(date_trunc('day', (now() AT TIME ZONE 'America/Chicago')) AT TIME ZONE 'America/Chicago') + interval '1 day - 1 second' as to`
|
(date_trunc('day', (now() AT TIME ZONE 'America/Chicago')) AT TIME ZONE 'America/Chicago') + interval '1 day - 1 second' as to`
|
||||||
return await topUsers(parent, { when: 'custom', to: new Date(to).getTime().toString(), from: new Date(from).getTime().toString(), limit: 100 }, { models, ...context })
|
return await topUsers(parent, { when: 'custom', to: new Date(to).getTime().toString(), from: new Date(from).getTime().toString(), limit: 500 }, { models, ...context })
|
||||||
},
|
},
|
||||||
total: async (parent, args, { models }) => {
|
total: async (parent, args, { models }) => {
|
||||||
if (!parent.total) {
|
if (!parent.total) {
|
||||||
|
@ -66,11 +66,12 @@ export async function topUsers (parent, { cursor, when, by, from, to, limit = LI
|
|||||||
case 'comments': column = 'ncomments'; break
|
case 'comments': column = 'ncomments'; break
|
||||||
case 'referrals': column = 'referrals'; break
|
case 'referrals': column = 'referrals'; break
|
||||||
case 'stacking': column = 'stacked'; break
|
case 'stacking': column = 'stacked'; break
|
||||||
|
case 'value':
|
||||||
default: column = 'proportion'; break
|
default: column = 'proportion'; break
|
||||||
}
|
}
|
||||||
|
|
||||||
const users = (await models.$queryRawUnsafe(`
|
const users = (await models.$queryRawUnsafe(`
|
||||||
SELECT *
|
SELECT * ${column === 'proportion' ? ', proportion' : ''}
|
||||||
FROM
|
FROM
|
||||||
(SELECT users.*,
|
(SELECT users.*,
|
||||||
COALESCE(floor(sum(msats_spent)/1000), 0) as spent,
|
COALESCE(floor(sum(msats_spent)/1000), 0) as spent,
|
||||||
|
@ -8,7 +8,8 @@ import { SELECT, itemQueryWithMeta } from './item'
|
|||||||
import { formatMsats, msatsToSats, msatsToSatsDecimal, satsToMsats } from '@/lib/format'
|
import { formatMsats, msatsToSats, msatsToSatsDecimal, satsToMsats } from '@/lib/format'
|
||||||
import {
|
import {
|
||||||
USER_ID, INVOICE_RETENTION_DAYS,
|
USER_ID, INVOICE_RETENTION_DAYS,
|
||||||
PAID_ACTION_PAYMENT_METHODS
|
PAID_ACTION_PAYMENT_METHODS,
|
||||||
|
WALLET_CREATE_INVOICE_TIMEOUT_MS
|
||||||
} from '@/lib/constants'
|
} from '@/lib/constants'
|
||||||
import { amountSchema, validateSchema, withdrawlSchema, lnAddrSchema } from '@/lib/validate'
|
import { amountSchema, validateSchema, withdrawlSchema, lnAddrSchema } from '@/lib/validate'
|
||||||
import assertGofacYourself from './ofac'
|
import assertGofacYourself from './ofac'
|
||||||
@ -21,9 +22,10 @@ import { lnAddrOptions } from '@/lib/lnurl'
|
|||||||
import { GqlAuthenticationError, GqlAuthorizationError, GqlInputError } from '@/lib/error'
|
import { GqlAuthenticationError, GqlAuthorizationError, GqlInputError } from '@/lib/error'
|
||||||
import { getNodeSockets, getOurPubkey } from '../lnd'
|
import { getNodeSockets, getOurPubkey } from '../lnd'
|
||||||
import validateWallet from '@/wallets/validate'
|
import validateWallet from '@/wallets/validate'
|
||||||
import { canReceive } from '@/wallets/common'
|
import { canReceive, getWalletByType } from '@/wallets/common'
|
||||||
import performPaidAction from '../paidAction'
|
import performPaidAction from '../paidAction'
|
||||||
import performPayingAction from '../payingAction'
|
import performPayingAction from '../payingAction'
|
||||||
|
import { timeoutSignal, withTimeout } from '@/lib/time'
|
||||||
|
|
||||||
function injectResolvers (resolvers) {
|
function injectResolvers (resolvers) {
|
||||||
console.group('injected GraphQL resolvers:')
|
console.group('injected GraphQL resolvers:')
|
||||||
@ -63,9 +65,15 @@ function injectResolvers (resolvers) {
|
|||||||
|
|
||||||
return await upsertWallet({
|
return await upsertWallet({
|
||||||
wallet,
|
wallet,
|
||||||
|
walletDef,
|
||||||
testCreateInvoice:
|
testCreateInvoice:
|
||||||
walletDef.testCreateInvoice && validateLightning && canReceive({ def: walletDef, config: data })
|
walletDef.testCreateInvoice && validateLightning && canReceive({ def: walletDef, config: data })
|
||||||
? (data) => walletDef.testCreateInvoice(data, { logger, me, models })
|
? (data) => withTimeout(
|
||||||
|
walletDef.testCreateInvoice(data, {
|
||||||
|
logger,
|
||||||
|
signal: timeoutSignal(WALLET_CREATE_INVOICE_TIMEOUT_MS)
|
||||||
|
}),
|
||||||
|
WALLET_CREATE_INVOICE_TIMEOUT_MS)
|
||||||
: null
|
: null
|
||||||
}, {
|
}, {
|
||||||
settings,
|
settings,
|
||||||
@ -551,7 +559,10 @@ const resolvers = {
|
|||||||
|
|
||||||
const logger = walletLogger({ wallet, models })
|
const logger = walletLogger({ wallet, models })
|
||||||
await models.wallet.delete({ where: { userId: me.id, id: Number(id) } })
|
await models.wallet.delete({ where: { userId: me.id, id: Number(id) } })
|
||||||
logger.info('wallet detached')
|
|
||||||
|
if (canReceive({ def: getWalletByType(wallet.type), config: wallet.wallet })) {
|
||||||
|
logger.info('details for receiving deleted')
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
@ -606,6 +617,15 @@ const resolvers = {
|
|||||||
satsReceived: i => msatsToSats(i.msatsReceived),
|
satsReceived: i => msatsToSats(i.msatsReceived),
|
||||||
satsRequested: i => msatsToSats(i.msatsRequested),
|
satsRequested: i => msatsToSats(i.msatsRequested),
|
||||||
// we never want to fetch the sensitive data full monty in nested resolvers
|
// we never want to fetch the sensitive data full monty in nested resolvers
|
||||||
|
forwardStatus: async (invoice, args, { models }) => {
|
||||||
|
const forward = await models.invoiceForward.findUnique({
|
||||||
|
where: { invoiceId: Number(invoice.id) },
|
||||||
|
include: {
|
||||||
|
withdrawl: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return forward?.withdrawl?.status
|
||||||
|
},
|
||||||
forwardedSats: async (invoice, args, { models }) => {
|
forwardedSats: async (invoice, args, { models }) => {
|
||||||
const msats = (await models.invoiceForward.findUnique({
|
const msats = (await models.invoiceForward.findUnique({
|
||||||
where: { invoiceId: Number(invoice.id) },
|
where: { invoiceId: Number(invoice.id) },
|
||||||
@ -750,7 +770,7 @@ export const walletLogger = ({ wallet, models }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function upsertWallet (
|
async function upsertWallet (
|
||||||
{ wallet, testCreateInvoice }, { settings, data, vaultEntries }, { logger, me, models }) {
|
{ wallet, walletDef, testCreateInvoice }, { settings, data, vaultEntries }, { logger, me, models }) {
|
||||||
if (!me) {
|
if (!me) {
|
||||||
throw new GqlAuthenticationError()
|
throw new GqlAuthenticationError()
|
||||||
}
|
}
|
||||||
@ -856,24 +876,26 @@ async function upsertWallet (
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
txs.push(
|
if (canReceive({ def: walletDef, config: walletData })) {
|
||||||
models.walletLog.createMany({
|
txs.push(
|
||||||
data: {
|
models.walletLog.createMany({
|
||||||
userId: me.id,
|
data: {
|
||||||
wallet: wallet.type,
|
userId: me.id,
|
||||||
level: 'SUCCESS',
|
wallet: wallet.type,
|
||||||
message: id ? 'wallet details updated' : 'wallet attached'
|
level: 'SUCCESS',
|
||||||
}
|
message: id ? 'details for receiving updated' : 'details for receiving saved'
|
||||||
}),
|
}
|
||||||
models.walletLog.create({
|
}),
|
||||||
data: {
|
models.walletLog.create({
|
||||||
userId: me.id,
|
data: {
|
||||||
wallet: wallet.type,
|
userId: me.id,
|
||||||
level: enabled ? 'SUCCESS' : 'INFO',
|
wallet: wallet.type,
|
||||||
message: enabled ? 'wallet enabled' : 'wallet disabled'
|
level: enabled ? 'SUCCESS' : 'INFO',
|
||||||
}
|
message: enabled ? 'receiving enabled' : 'receiving disabled'
|
||||||
})
|
}
|
||||||
)
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const [upsertedWallet] = await models.$transaction(txs)
|
const [upsertedWallet] = await models.$transaction(txs)
|
||||||
return upsertedWallet
|
return upsertedWallet
|
||||||
|
@ -59,6 +59,11 @@ export default gql`
|
|||||||
photoId: Int
|
photoId: Int
|
||||||
since: Int
|
since: Int
|
||||||
|
|
||||||
|
"""
|
||||||
|
this is only returned when we sort stackers by value
|
||||||
|
"""
|
||||||
|
proportion: Float
|
||||||
|
|
||||||
optional: UserOptional!
|
optional: UserOptional!
|
||||||
privates: UserPrivates
|
privates: UserPrivates
|
||||||
|
|
||||||
|
@ -129,6 +129,7 @@ const typeDefs = `
|
|||||||
item: Item
|
item: Item
|
||||||
itemAct: ItemAct
|
itemAct: ItemAct
|
||||||
forwardedSats: Int
|
forwardedSats: Int
|
||||||
|
forwardStatus: String
|
||||||
}
|
}
|
||||||
|
|
||||||
type Withdrawl {
|
type Withdrawl {
|
||||||
|
@ -152,10 +152,11 @@ Gudnessuche,issue,#1662,#1661,good-first-issue,,,,2k,everythingsatoshi@getalby.c
|
|||||||
aegroto,pr,#1589,#1586,easy,,,,100k,aegroto@blink.sv,2024-12-07
|
aegroto,pr,#1589,#1586,easy,,,,100k,aegroto@blink.sv,2024-12-07
|
||||||
aegroto,issue,#1589,#1586,easy,,,,10k,aegroto@blink.sv,2024-12-07
|
aegroto,issue,#1589,#1586,easy,,,,10k,aegroto@blink.sv,2024-12-07
|
||||||
aegroto,pr,#1619,#914,easy,,,,100k,aegroto@blink.sv,2024-12-07
|
aegroto,pr,#1619,#914,easy,,,,100k,aegroto@blink.sv,2024-12-07
|
||||||
felipebueno,pr,#1620,,medium,,,1,225k,felipebueno@getalby.com,???
|
felipebueno,pr,#1620,,medium,,,1,225k,felipebueno@getalby.com,2024-12-09
|
||||||
Soxasora,pr,#1647,#1645,easy,,,,100k,soxasora@blink.sv,2024-12-07
|
Soxasora,pr,#1647,#1645,easy,,,,100k,soxasora@blink.sv,2024-12-07
|
||||||
Soxasora,pr,#1667,#1568,easy,,,,100k,soxasora@blink.sv,2024-12-07
|
Soxasora,pr,#1667,#1568,easy,,,,100k,soxasora@blink.sv,2024-12-07
|
||||||
aegroto,pr,#1633,#1471,easy,,,1,90k,aegroto@blink.sv,2024-12-07
|
aegroto,pr,#1633,#1471,easy,,,1,90k,aegroto@blink.sv,2024-12-07
|
||||||
Darth-Coin,issue,#1649,#1421,medium,,,,25k,darthcoin@stacker.news,2024-12-07
|
Darth-Coin,issue,#1649,#1421,medium,,,,25k,darthcoin@stacker.news,2024-12-07
|
||||||
Soxasora,pr,#1685,,medium,,,,250k,soxasora@blink.sv,2024-12-07
|
Soxasora,pr,#1685,,medium,,,,250k,soxasora@blink.sv,2024-12-07
|
||||||
aegroto,pr,#1606,#1242,medium,,,,250k,aegroto@blink.sv,2024-12-07
|
aegroto,pr,#1606,#1242,medium,,,,250k,aegroto@blink.sv,2024-12-07
|
||||||
|
sfr0xyz,issue,#1696,#1196,good-first-issue,,,,2k,sefiro@getalby.com,2024-12-10
|
||||||
|
|
@ -11,7 +11,7 @@ RUN npm ci
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
ADD http://ftp.de.debian.org/debian/pool/main/f/fonts-noto-color-emoji/fonts-noto-color-emoji_0~20200916-1_all.deb fonts-noto-color-emoji.deb
|
ADD https://deb.debian.org/debian/pool/main/f/fonts-noto-color-emoji/fonts-noto-color-emoji_0~20200916-1_all.deb fonts-noto-color-emoji.deb
|
||||||
RUN dpkg -i fonts-noto-color-emoji.deb
|
RUN dpkg -i fonts-noto-color-emoji.deb
|
||||||
CMD [ "node", "index.js" ]
|
CMD [ "node", "index.js" ]
|
||||||
USER pptruser
|
USER pptruser
|
@ -232,9 +232,15 @@ export function useAct ({ query = ACT_MUTATION, ...options } = {}) {
|
|||||||
// because the mutation name we use varies,
|
// because the mutation name we use varies,
|
||||||
// we need to extract the result/invoice from the response
|
// we need to extract the result/invoice from the response
|
||||||
const getPaidActionResult = data => Object.values(data)[0]
|
const getPaidActionResult = data => Object.values(data)[0]
|
||||||
|
const wallets = useSendWallets()
|
||||||
|
|
||||||
const [act] = usePaidMutation(query, {
|
const [act] = usePaidMutation(query, {
|
||||||
waitFor: inv => inv?.satsReceived > 0,
|
waitFor: inv =>
|
||||||
|
// if we have attached wallets, we might be paying a wrapped invoice in which case we need to make sure
|
||||||
|
// we don't prematurely consider the payment as successful (important for receiver fallbacks)
|
||||||
|
wallets.length > 0
|
||||||
|
? inv?.actionState === 'PAID'
|
||||||
|
: inv?.satsReceived > 0,
|
||||||
...options,
|
...options,
|
||||||
update: (cache, { data }) => {
|
update: (cache, { data }) => {
|
||||||
const response = getPaidActionResult(data)
|
const response = getPaidActionResult(data)
|
||||||
|
@ -36,7 +36,7 @@ function QrAuth ({ k1, encodedUrl, callbackUrl, multiAuth }) {
|
|||||||
await window.webln.enable()
|
await window.webln.enable()
|
||||||
await window.webln.lnurl(encodedUrl)
|
await window.webln.lnurl(encodedUrl)
|
||||||
}
|
}
|
||||||
effect()
|
effect().catch(console.error)
|
||||||
}, [encodedUrl])
|
}, [encodedUrl])
|
||||||
|
|
||||||
// output pubkey and k1
|
// output pubkey and k1
|
||||||
|
@ -2,6 +2,7 @@ import { useRouter } from 'next/router'
|
|||||||
import DesktopHeader from './desktop/header'
|
import DesktopHeader from './desktop/header'
|
||||||
import MobileHeader from './mobile/header'
|
import MobileHeader from './mobile/header'
|
||||||
import StickyBar from './sticky-bar'
|
import StickyBar from './sticky-bar'
|
||||||
|
import { PriceCarouselProvider } from './price-carousel'
|
||||||
|
|
||||||
export default function Navigation ({ sub }) {
|
export default function Navigation ({ sub }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@ -16,10 +17,10 @@ export default function Navigation ({ sub }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<PriceCarouselProvider>
|
||||||
<DesktopHeader {...props} />
|
<DesktopHeader {...props} />
|
||||||
<MobileHeader {...props} />
|
<MobileHeader {...props} />
|
||||||
<StickyBar {...props} />
|
<StickyBar {...props} />
|
||||||
</>
|
</PriceCarouselProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
46
components/nav/price-carousel.js
Normal file
46
components/nav/price-carousel.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { createContext, useCallback, useContext, useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'asSats'
|
||||||
|
const DEFAULT_SELECTION = 'fiat'
|
||||||
|
|
||||||
|
const carousel = [
|
||||||
|
'fiat',
|
||||||
|
'yep',
|
||||||
|
'1btc',
|
||||||
|
'blockHeight',
|
||||||
|
'chainFee',
|
||||||
|
'halving'
|
||||||
|
]
|
||||||
|
|
||||||
|
export const PriceCarouselContext = createContext({
|
||||||
|
selection: undefined,
|
||||||
|
handleClick: () => {}
|
||||||
|
})
|
||||||
|
|
||||||
|
export function PriceCarouselProvider ({ children }) {
|
||||||
|
const [selection, setSelection] = useState(undefined)
|
||||||
|
const [pos, setPos] = useState(0)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const selection = window.localStorage.getItem(STORAGE_KEY) ?? DEFAULT_SELECTION
|
||||||
|
setSelection(selection)
|
||||||
|
setPos(carousel.findIndex((item) => item === selection))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
const nextPos = (pos + 1) % carousel.length
|
||||||
|
window.localStorage.setItem(STORAGE_KEY, carousel[nextPos])
|
||||||
|
setSelection(carousel[nextPos])
|
||||||
|
setPos(nextPos)
|
||||||
|
}, [pos])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PriceCarouselContext.Provider value={[selection, handleClick]}>
|
||||||
|
{children}
|
||||||
|
</PriceCarouselContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePriceCarousel () {
|
||||||
|
return useContext(PriceCarouselContext)
|
||||||
|
}
|
@ -1,19 +1,22 @@
|
|||||||
import { Container, Nav, Navbar } from 'react-bootstrap'
|
import { Container, Nav, Navbar } from 'react-bootstrap'
|
||||||
import styles from '../header.module.css'
|
import styles from '../header.module.css'
|
||||||
import { BackOrBrand, NavPrice, SearchItem } from './common'
|
import { BackOrBrand, NavPrice, SearchItem } from './common'
|
||||||
|
import { PriceCarouselProvider } from './price-carousel'
|
||||||
|
|
||||||
export default function StaticHeader () {
|
export default function StaticHeader () {
|
||||||
return (
|
return (
|
||||||
<Container as='header' className='px-sm-0'>
|
<PriceCarouselProvider>
|
||||||
<Navbar>
|
<Container as='header' className='px-sm-0'>
|
||||||
<Nav
|
<Navbar>
|
||||||
className={styles.navbarNav}
|
<Nav
|
||||||
>
|
className={styles.navbarNav}
|
||||||
<BackOrBrand />
|
>
|
||||||
<SearchItem />
|
<BackOrBrand />
|
||||||
<NavPrice className='justify-content-end' />
|
<SearchItem />
|
||||||
</Nav>
|
<NavPrice className='justify-content-end' />
|
||||||
</Navbar>
|
</Nav>
|
||||||
</Container>
|
</Navbar>
|
||||||
|
</Container>
|
||||||
|
</PriceCarouselProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,71 +1,78 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useState, useCallback, useEffect, useRef } from 'react'
|
||||||
import { gql, useMutation } from '@apollo/client'
|
import { gql, useMutation } from '@apollo/client'
|
||||||
import { signIn } from 'next-auth/react'
|
import { signIn } from 'next-auth/react'
|
||||||
import Container from 'react-bootstrap/Container'
|
|
||||||
import Col from 'react-bootstrap/Col'
|
import Col from 'react-bootstrap/Col'
|
||||||
import Row from 'react-bootstrap/Row'
|
import Row from 'react-bootstrap/Row'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import AccordianItem from './accordian-item'
|
import AccordianItem from './accordian-item'
|
||||||
import BackIcon from '@/svgs/arrow-left-line.svg'
|
import BackIcon from '@/svgs/arrow-left-line.svg'
|
||||||
|
import Nostr from '@/lib/nostr'
|
||||||
|
import { NDKNip46Signer } from '@nostr-dev-kit/ndk'
|
||||||
|
import { useToast } from '@/components/toast'
|
||||||
|
import { Button, Container } from 'react-bootstrap'
|
||||||
|
import { Form, Input, SubmitButton } from '@/components/form'
|
||||||
|
import Moon from '@/svgs/moon-fill.svg'
|
||||||
import styles from './lightning-auth.module.css'
|
import styles from './lightning-auth.module.css'
|
||||||
import { callWithTimeout } from '@/lib/time'
|
|
||||||
|
|
||||||
function ExtensionError ({ message, details }) {
|
const sanitizeURL = (s) => {
|
||||||
return (
|
try {
|
||||||
<>
|
const url = new URL(s)
|
||||||
<h4 className='fw-bold text-danger pb-1'>error: {message}</h4>
|
if (url.protocol !== 'https:' && url.protocol !== 'http:') throw new Error('invalid protocol')
|
||||||
<div className='text-muted pb-4'>{details}</div>
|
return url.href
|
||||||
</>
|
} catch (e) {
|
||||||
)
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function NostrExplainer ({ text }) {
|
function NostrError ({ message }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ExtensionError message='nostr extension not found' details='Nostr extensions are the safest way to use your nostr identity on Stacker News.' />
|
<h4 className='fw-bold text-danger pb-1'>error</h4>
|
||||||
<Row className='w-100 text-muted'>
|
<div className='text-muted pb-4'>{message}</div>
|
||||||
<AccordianItem
|
|
||||||
header={`Which extensions can I use to ${(text || 'Login').toLowerCase()} with Nostr?`}
|
|
||||||
show
|
|
||||||
body={
|
|
||||||
<>
|
|
||||||
<Row>
|
|
||||||
<Col>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a href='https://getalby.com'>Alby</a><br />
|
|
||||||
available for: chrome, firefox, and safari
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href='https://www.getflamingo.org/'>Flamingo</a><br />
|
|
||||||
available for: chrome
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href='https://github.com/fiatjaf/nos2x'>nos2x</a><br />
|
|
||||||
available for: chrome
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href='https://diegogurpegui.com/nos2x-fox/'>nos2x-fox</a><br />
|
|
||||||
available for: firefox
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href='https://github.com/fiatjaf/horse'>horse</a><br />
|
|
||||||
available for: chrome<br />
|
|
||||||
supports hardware signing
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Row>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NostrAuth ({ text, callbackUrl, multiAuth }) {
|
export function NostrAuth ({ text, callbackUrl, multiAuth }) {
|
||||||
const [createAuth, { data, error }] = useMutation(gql`
|
const [status, setStatus] = useState({
|
||||||
|
msg: '',
|
||||||
|
error: false,
|
||||||
|
loading: false,
|
||||||
|
title: undefined,
|
||||||
|
button: undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
const [suggestion, setSuggestion] = useState(null)
|
||||||
|
const suggestionTimeout = useRef(null)
|
||||||
|
const toaster = useToast()
|
||||||
|
|
||||||
|
const challengeResolver = useCallback(async (challenge) => {
|
||||||
|
const challengeUrl = sanitizeURL(challenge)
|
||||||
|
if (challengeUrl) {
|
||||||
|
setStatus({
|
||||||
|
title: 'Waiting for confirmation',
|
||||||
|
msg: 'Please confirm this action on your remote signer',
|
||||||
|
error: false,
|
||||||
|
loading: true,
|
||||||
|
button: {
|
||||||
|
label: 'open signer',
|
||||||
|
action: () => {
|
||||||
|
window.open(challengeUrl, '_blank')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setStatus({
|
||||||
|
title: 'Waiting for confirmation',
|
||||||
|
msg: challenge,
|
||||||
|
error: false,
|
||||||
|
loading: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// create auth challenge
|
||||||
|
const [createAuth] = useMutation(gql`
|
||||||
mutation createAuth {
|
mutation createAuth {
|
||||||
createAuth {
|
createAuth {
|
||||||
k1
|
k1
|
||||||
@ -74,83 +81,253 @@ export function NostrAuth ({ text, callbackUrl, multiAuth }) {
|
|||||||
// don't cache this mutation
|
// don't cache this mutation
|
||||||
fetchPolicy: 'no-cache'
|
fetchPolicy: 'no-cache'
|
||||||
})
|
})
|
||||||
const [hasExtension, setHasExtension] = useState(undefined)
|
|
||||||
const [extensionError, setExtensionError] = useState(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
// print an error message
|
||||||
createAuth()
|
const setError = useCallback((e) => {
|
||||||
setHasExtension(!!window.nostr)
|
console.error(e)
|
||||||
|
toaster.danger(e.message || e.toString())
|
||||||
|
setStatus({
|
||||||
|
msg: e.message || e.toString(),
|
||||||
|
error: true,
|
||||||
|
loading: false
|
||||||
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const k1 = data?.createAuth.k1
|
const clearSuggestionTimer = () => {
|
||||||
|
if (suggestionTimeout.current) clearTimeout(suggestionTimeout.current)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setSuggestionWithTimer = (msg) => {
|
||||||
|
clearSuggestionTimer()
|
||||||
|
suggestionTimeout.current = setTimeout(() => {
|
||||||
|
setSuggestion(msg)
|
||||||
|
}, 10_000)
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!k1 || !hasExtension) return
|
return () => {
|
||||||
|
clearSuggestionTimer()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
console.info('nostr extension detected')
|
// authorize user
|
||||||
|
const auth = useCallback(async (nip46token) => {
|
||||||
|
setStatus({
|
||||||
|
msg: 'Waiting for authorization',
|
||||||
|
error: false,
|
||||||
|
loading: true
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
const { data, error } = await createAuth()
|
||||||
|
if (error) throw error
|
||||||
|
|
||||||
let mounted = true;
|
const k1 = data?.createAuth.k1
|
||||||
(async function () {
|
if (!k1) throw new Error('Error generating challenge') // should never happen
|
||||||
try {
|
|
||||||
// have them sign a message with the challenge
|
|
||||||
let event
|
|
||||||
try {
|
|
||||||
event = await callWithTimeout(() => window.nostr.signEvent({
|
|
||||||
kind: 22242,
|
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
|
||||||
tags: [['challenge', k1]],
|
|
||||||
content: 'Stacker News Authentication'
|
|
||||||
}), 5000)
|
|
||||||
if (!event) throw new Error('extension returned empty event')
|
|
||||||
} catch (e) {
|
|
||||||
if (e.message === 'window.nostr call already executing' || !mounted) return
|
|
||||||
setExtensionError({ message: 'nostr extension failed to sign event', details: e.message })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// sign them in
|
const useExtension = !nip46token
|
||||||
try {
|
const signer = Nostr.getSigner({ nip46token, supportNip07: useExtension })
|
||||||
await signIn('nostr', {
|
if (!signer && useExtension) throw new Error('No extension found')
|
||||||
event: JSON.stringify(event),
|
|
||||||
callbackUrl,
|
if (signer instanceof NDKNip46Signer) {
|
||||||
multiAuth
|
signer.once('authUrl', challengeResolver)
|
||||||
})
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error('authorization failed', e)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
if (!mounted) return
|
|
||||||
console.log('nostr auth error', e)
|
|
||||||
setExtensionError({ message: `${text} failed`, details: e.message })
|
|
||||||
}
|
}
|
||||||
})()
|
|
||||||
return () => { mounted = false }
|
|
||||||
}, [k1, hasExtension])
|
|
||||||
|
|
||||||
if (error) return <div>error</div>
|
setSuggestionWithTimer('Having trouble? Make sure you used a fresh token or valid NIP-05 address')
|
||||||
|
await signer.blockUntilReady()
|
||||||
|
clearSuggestionTimer()
|
||||||
|
|
||||||
|
setStatus({
|
||||||
|
msg: 'Signing in',
|
||||||
|
error: false,
|
||||||
|
loading: true
|
||||||
|
})
|
||||||
|
|
||||||
|
const signedEvent = await Nostr.sign({
|
||||||
|
kind: 27235,
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
tags: [
|
||||||
|
['challenge', k1],
|
||||||
|
['u', process.env.NEXT_PUBLIC_URL],
|
||||||
|
['method', 'GET']
|
||||||
|
],
|
||||||
|
content: 'Stacker News Authentication'
|
||||||
|
}, { signer })
|
||||||
|
|
||||||
|
await signIn('nostr', {
|
||||||
|
event: JSON.stringify(signedEvent),
|
||||||
|
callbackUrl,
|
||||||
|
multiAuth
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
setError(e)
|
||||||
|
} finally {
|
||||||
|
clearSuggestionTimer()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{hasExtension === false && <NostrExplainer text={text} />}
|
{status.error && <NostrError message={status.msg} />}
|
||||||
{extensionError && <ExtensionError {...extensionError} />}
|
{status.loading
|
||||||
{hasExtension && !extensionError &&
|
? (
|
||||||
<>
|
<>
|
||||||
<h4 className='fw-bold text-success pb-1'>nostr extension found</h4>
|
<div className='text-muted py-4 w-100 line-height-1 d-flex align-items-center gap-2'>
|
||||||
<h6 className='text-muted pb-4'>authorize event signature in extension</h6>
|
<Moon className='spin fill-grey flex-shrink-0' width='30' height='30' />
|
||||||
</>}
|
{status.msg}
|
||||||
|
</div>
|
||||||
|
{status.button && (
|
||||||
|
<Button
|
||||||
|
className='w-100' variant='primary'
|
||||||
|
onClick={() => status.button.action()}
|
||||||
|
>
|
||||||
|
{status.button.label}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{suggestion && (
|
||||||
|
<div className='text-muted text-center small pt-2'>{suggestion}</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<>
|
||||||
|
<Form
|
||||||
|
initial={{ token: '' }}
|
||||||
|
onSubmit={values => {
|
||||||
|
if (!values.token) {
|
||||||
|
setError(new Error('Token or NIP-05 address is required'))
|
||||||
|
} else {
|
||||||
|
auth(values.token)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
label='Connect with token or NIP-05 address'
|
||||||
|
name='token'
|
||||||
|
placeholder='bunker://... or NIP-05 address'
|
||||||
|
required
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<div className='mt-2'>
|
||||||
|
<SubmitButton className='w-100' variant='primary'>
|
||||||
|
{text || 'Login'} with token or NIP-05
|
||||||
|
</SubmitButton>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
<div className='text-center text-muted fw-bold my-2'>or</div>
|
||||||
|
<Button
|
||||||
|
variant='nostr'
|
||||||
|
className='w-100'
|
||||||
|
type='submit'
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
await auth()
|
||||||
|
} catch (e) {
|
||||||
|
setError(e)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{text || 'Login'} with extension
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NostrAuthWithExplainer ({ text, callbackUrl, multiAuth }) {
|
function NostrExplainer ({ text, children }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<div className={styles.login}>
|
<div className={styles.login}>
|
||||||
<div className='w-100 mb-3 text-muted pointer' onClick={() => router.back()}><BackIcon /></div>
|
<div className='w-100 mb-3 text-muted pointer' onClick={() => router.back()}><BackIcon /></div>
|
||||||
<h3 className='w-100 pb-2'>{text || 'Login'} with Nostr</h3>
|
<h3 className='w-100 pb-2'>
|
||||||
<NostrAuth text={text} callbackUrl={callbackUrl} multiAuth={multiAuth} />
|
{text || 'Login'} with Nostr
|
||||||
|
</h3>
|
||||||
|
<Row className='w-100 text-muted'>
|
||||||
|
<Col className='ps-0 mb-4' md>
|
||||||
|
<AccordianItem
|
||||||
|
header='Which NIP-46 signers can I use?'
|
||||||
|
body={
|
||||||
|
<>
|
||||||
|
<Row>
|
||||||
|
<Col xs>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a href='https://nsec.app/'>Nsec.app</a>
|
||||||
|
<ul>
|
||||||
|
<li>available for: chrome, firefox, and safari</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href='https://app.nsecbunker.com/'>nsecBunker</a>
|
||||||
|
<ul>
|
||||||
|
<li>available as: SaaS or self-hosted</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<AccordianItem
|
||||||
|
header='Which extensions can I use?'
|
||||||
|
body={
|
||||||
|
<>
|
||||||
|
<Row>
|
||||||
|
<Col>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a href='https://getalby.com'>Alby</a>
|
||||||
|
<ul>
|
||||||
|
<li>available for: chrome, firefox, and safari</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href='https://www.getflamingo.org/'>Flamingo</a>
|
||||||
|
<ul>
|
||||||
|
<li>available for: chrome</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href='https://github.com/fiatjaf/nos2x'>nos2x</a>
|
||||||
|
<ul>
|
||||||
|
<li>available for: chrome</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href='https://diegogurpegui.com/nos2x-fox/'>nos2x-fox</a>
|
||||||
|
<ul>
|
||||||
|
<li>available for: firefox</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href='https://github.com/fiatjaf/horse'>horse</a>
|
||||||
|
<ul>
|
||||||
|
<li>available for: chrome</li>
|
||||||
|
<li>supports hardware signing</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col md className='mx-auto' style={{ maxWidth: '300px' }}>
|
||||||
|
{children}
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function NostrAuthWithExplainer ({ text, callbackUrl, multiAuth }) {
|
||||||
|
return (
|
||||||
|
<NostrExplainer text={text}>
|
||||||
|
<NostrAuth text={text} callbackUrl={callbackUrl} multiAuth={multiAuth} />
|
||||||
|
</NostrExplainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -501,13 +501,23 @@ function Invoicification ({ n: { invoice, sortTime } }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function WithdrawlPaid ({ n }) {
|
function WithdrawlPaid ({ n }) {
|
||||||
let actionString = n.withdrawl.autoWithdraw ? 'sent to your attached wallet' : 'withdrawn from your account'
|
let amount = n.earnedSats + n.withdrawl.satsFeePaid
|
||||||
|
let actionString = 'withdrawn from your account'
|
||||||
|
|
||||||
|
if (n.withdrawl.autoWithdraw) {
|
||||||
|
actionString = 'sent to your attached wallet'
|
||||||
|
}
|
||||||
|
|
||||||
if (n.withdrawl.forwardedActionType === 'ZAP') {
|
if (n.withdrawl.forwardedActionType === 'ZAP') {
|
||||||
|
// don't expose receivers to routing fees they aren't paying
|
||||||
|
amount = n.earnedSats
|
||||||
actionString = 'zapped directly to your attached wallet'
|
actionString = 'zapped directly to your attached wallet'
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='fw-bold text-info'>
|
<div className='fw-bold text-info'>
|
||||||
<Check className='fill-info me-1' />{numWithUnits(n.earnedSats + n.withdrawl.satsFeePaid, { abbreviate: false, unitSingular: 'sat was ', unitPlural: 'sats were ' })}
|
<Check className='fill-info me-1' />
|
||||||
|
{numWithUnits(amount, { abbreviate: false, unitSingular: 'sat was ', unitPlural: 'sats were ' })}
|
||||||
{actionString}
|
{actionString}
|
||||||
<small className='text-muted ms-1 fw-normal' suppressHydrationWarning>{timeSince(new Date(n.sortTime))}</small>
|
<small className='text-muted ms-1 fw-normal' suppressHydrationWarning>{timeSince(new Date(n.sortTime))}</small>
|
||||||
{(n.withdrawl.forwardedActionType === 'ZAP' && <Badge className={styles.badge} bg={null}>p2p</Badge>) ||
|
{(n.withdrawl.forwardedActionType === 'ZAP' && <Badge className={styles.badge} bg={null}>p2p</Badge>) ||
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useContext, useEffect, useMemo, useState } from 'react'
|
import React, { useContext, useMemo } from 'react'
|
||||||
import { useQuery } from '@apollo/client'
|
import { useQuery } from '@apollo/client'
|
||||||
import { fixedDecimal } from '@/lib/format'
|
import { fixedDecimal } from '@/lib/format'
|
||||||
import { useMe } from './me'
|
import { useMe } from './me'
|
||||||
@ -8,6 +8,7 @@ import { NORMAL_POLL_INTERVAL, SSR } from '@/lib/constants'
|
|||||||
import { useBlockHeight } from './block-height'
|
import { useBlockHeight } from './block-height'
|
||||||
import { useChainFee } from './chain-fee'
|
import { useChainFee } from './chain-fee'
|
||||||
import { CompactLongCountdown } from './countdown'
|
import { CompactLongCountdown } from './countdown'
|
||||||
|
import { usePriceCarousel } from './nav/price-carousel'
|
||||||
|
|
||||||
export const PriceContext = React.createContext({
|
export const PriceContext = React.createContext({
|
||||||
price: null,
|
price: null,
|
||||||
@ -43,43 +44,16 @@ export function PriceProvider ({ price, children }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const STORAGE_KEY = 'asSats'
|
|
||||||
const DEFAULT_SELECTION = 'fiat'
|
|
||||||
|
|
||||||
const carousel = [
|
|
||||||
'fiat',
|
|
||||||
'yep',
|
|
||||||
'1btc',
|
|
||||||
'blockHeight',
|
|
||||||
'chainFee',
|
|
||||||
'halving'
|
|
||||||
]
|
|
||||||
|
|
||||||
export default function Price ({ className }) {
|
export default function Price ({ className }) {
|
||||||
const [asSats, setAsSats] = useState(undefined)
|
const [selection, handleClick] = usePriceCarousel()
|
||||||
const [pos, setPos] = useState(0)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const selection = window.localStorage.getItem(STORAGE_KEY) ?? DEFAULT_SELECTION
|
|
||||||
setAsSats(selection)
|
|
||||||
setPos(carousel.findIndex((item) => item === selection))
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const { price, fiatSymbol } = usePrice()
|
const { price, fiatSymbol } = usePrice()
|
||||||
const { height: blockHeight, halving } = useBlockHeight()
|
const { height: blockHeight, halving } = useBlockHeight()
|
||||||
const { fee: chainFee } = useChainFee()
|
const { fee: chainFee } = useChainFee()
|
||||||
|
|
||||||
const handleClick = () => {
|
|
||||||
const nextPos = (pos + 1) % carousel.length
|
|
||||||
|
|
||||||
window.localStorage.setItem(STORAGE_KEY, carousel[nextPos])
|
|
||||||
setAsSats(carousel[nextPos])
|
|
||||||
setPos(nextPos)
|
|
||||||
}
|
|
||||||
|
|
||||||
const compClassName = (className || '') + ' text-reset pointer'
|
const compClassName = (className || '') + ' text-reset pointer'
|
||||||
|
|
||||||
if (asSats === 'yep') {
|
if (selection === 'yep') {
|
||||||
if (!price || price < 0) return null
|
if (!price || price < 0) return null
|
||||||
return (
|
return (
|
||||||
<div className={compClassName} onClick={handleClick} variant='link'>
|
<div className={compClassName} onClick={handleClick} variant='link'>
|
||||||
@ -88,7 +62,7 @@ export default function Price ({ className }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (asSats === '1btc') {
|
if (selection === '1btc') {
|
||||||
return (
|
return (
|
||||||
<div className={compClassName} onClick={handleClick} variant='link'>
|
<div className={compClassName} onClick={handleClick} variant='link'>
|
||||||
1sat=1sat
|
1sat=1sat
|
||||||
@ -96,7 +70,7 @@ export default function Price ({ className }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (asSats === 'blockHeight') {
|
if (selection === 'blockHeight') {
|
||||||
if (blockHeight <= 0) return null
|
if (blockHeight <= 0) return null
|
||||||
return (
|
return (
|
||||||
<div className={compClassName} onClick={handleClick} variant='link'>
|
<div className={compClassName} onClick={handleClick} variant='link'>
|
||||||
@ -105,7 +79,7 @@ export default function Price ({ className }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (asSats === 'halving') {
|
if (selection === 'halving') {
|
||||||
if (!halving) return null
|
if (!halving) return null
|
||||||
return (
|
return (
|
||||||
<div className={compClassName} onClick={handleClick} variant='link'>
|
<div className={compClassName} onClick={handleClick} variant='link'>
|
||||||
@ -114,7 +88,7 @@ export default function Price ({ className }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (asSats === 'chainFee') {
|
if (selection === 'chainFee') {
|
||||||
if (chainFee <= 0) return null
|
if (chainFee <= 0) return null
|
||||||
return (
|
return (
|
||||||
<div className={compClassName} onClick={handleClick} variant='link'>
|
<div className={compClassName} onClick={handleClick} variant='link'>
|
||||||
@ -123,7 +97,7 @@ export default function Price ({ className }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (asSats === 'fiat') {
|
if (selection === 'fiat') {
|
||||||
if (!price || price < 0) return null
|
if (!price || price < 0) return null
|
||||||
return (
|
return (
|
||||||
<div className={compClassName} onClick={handleClick} variant='link'>
|
<div className={compClassName} onClick={handleClick} variant='link'>
|
||||||
|
@ -118,7 +118,7 @@ export const ToastProvider = ({ children }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ToastContext.Provider value={toaster}>
|
<ToastContext.Provider value={toaster}>
|
||||||
<ToastContainer className={`pb-3 pe-3 ${styles.toastContainer}`} position='bottom-end' containerPosition='fixed'>
|
<ToastContainer className={`pb-3 px-3 ${styles.toastContainer}`} position='bottom-end' containerPosition='fixed'>
|
||||||
{visibleToasts.map(toast => {
|
{visibleToasts.map(toast => {
|
||||||
const textStyle = toast.variant === 'warning' ? 'text-dark' : ''
|
const textStyle = toast.variant === 'warning' ? 'text-dark' : ''
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { datePivot } from '@/lib/time'
|
import { datePivot } from '@/lib/time'
|
||||||
import { useMe } from '@/components/me'
|
import { useMe } from '@/components/me'
|
||||||
import { USER_ID } from '@/lib/constants'
|
import { ITEM_EDIT_SECONDS, USER_ID } from '@/lib/constants'
|
||||||
|
|
||||||
export default function useCanEdit (item) {
|
export default function useCanEdit (item) {
|
||||||
const editThreshold = datePivot(new Date(item.invoice?.confirmedAt ?? item.createdAt), { minutes: 10 })
|
const editThreshold = datePivot(new Date(item.invoice?.confirmedAt ?? item.createdAt), { seconds: ITEM_EDIT_SECONDS })
|
||||||
const { me } = useMe()
|
const { me } = useMe()
|
||||||
|
|
||||||
// deleted items can never be edited and every item has a 10 minute edit window
|
// deleted items can never be edited and every item has a 10 minute edit window
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { useToast } from './toast'
|
import { useToast } from './toast'
|
||||||
import { Button } from 'react-bootstrap'
|
import { Button } from 'react-bootstrap'
|
||||||
import { DEFAULT_CROSSPOSTING_RELAYS, crosspost } from '@/lib/nostr'
|
import Nostr, { DEFAULT_CROSSPOSTING_RELAYS } from '@/lib/nostr'
|
||||||
import { callWithTimeout } from '@/lib/time'
|
|
||||||
import { gql, useMutation, useQuery, useLazyQuery } from '@apollo/client'
|
import { gql, useMutation, useQuery, useLazyQuery } from '@apollo/client'
|
||||||
import { SETTINGS } from '@/fragments/users'
|
import { SETTINGS } from '@/fragments/users'
|
||||||
import { ITEM_FULL_FIELDS, POLL_FIELDS } from '@/fragments/items'
|
import { ITEM_FULL_FIELDS, POLL_FIELDS } from '@/fragments/items'
|
||||||
@ -204,7 +203,7 @@ export default function useCrossposter () {
|
|||||||
|
|
||||||
do {
|
do {
|
||||||
try {
|
try {
|
||||||
const result = await crosspost(event, failedRelays || relays)
|
const result = await Nostr.crosspost(event, { relays: failedRelays || relays })
|
||||||
|
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
failedRelays = []
|
failedRelays = []
|
||||||
@ -239,13 +238,6 @@ export default function useCrossposter () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleCrosspost = useCallback(async (itemId) => {
|
const handleCrosspost = useCallback(async (itemId) => {
|
||||||
try {
|
|
||||||
const pubkey = await callWithTimeout(() => window.nostr.getPublicKey(), 10000)
|
|
||||||
if (!pubkey) throw new Error('failed to get pubkey')
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(`Nostr extension error: ${e.message}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
let noteId
|
let noteId
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { useApolloClient, useMutation } from '@apollo/client'
|
import { useApolloClient, useMutation } from '@apollo/client'
|
||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
|
import { InvoiceCanceledError, InvoiceExpiredError, WalletReceiverError } from '@/wallets/errors'
|
||||||
import { RETRY_PAID_ACTION } from '@/fragments/paidAction'
|
import { RETRY_PAID_ACTION } from '@/fragments/paidAction'
|
||||||
import { INVOICE, CANCEL_INVOICE } from '@/fragments/wallet'
|
import { INVOICE, CANCEL_INVOICE } from '@/fragments/wallet'
|
||||||
import { InvoiceExpiredError, InvoiceCanceledError } from '@/wallets/errors'
|
|
||||||
|
|
||||||
export default function useInvoice () {
|
export default function useInvoice () {
|
||||||
const client = useApolloClient()
|
const client = useApolloClient()
|
||||||
@ -16,20 +16,21 @@ export default function useInvoice () {
|
|||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
const { cancelled, cancelledAt, actionError, actionState, expiresAt } = data.invoice
|
const { cancelled, cancelledAt, actionError, expiresAt, forwardStatus } = data.invoice
|
||||||
|
|
||||||
const expired = cancelledAt && new Date(expiresAt) < new Date(cancelledAt)
|
const expired = cancelledAt && new Date(expiresAt) < new Date(cancelledAt)
|
||||||
if (expired) {
|
if (expired) {
|
||||||
throw new InvoiceExpiredError(data.invoice)
|
throw new InvoiceExpiredError(data.invoice)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cancelled || actionError) {
|
const failedForward = forwardStatus && forwardStatus !== 'CONFIRMED'
|
||||||
throw new InvoiceCanceledError(data.invoice, actionError)
|
if (failedForward) {
|
||||||
|
throw new WalletReceiverError(data.invoice)
|
||||||
}
|
}
|
||||||
|
|
||||||
// write to cache if paid
|
const failed = cancelled || actionError
|
||||||
if (actionState === 'PAID') {
|
if (failed) {
|
||||||
client.writeQuery({ query: INVOICE, variables: { id }, data: { invoice: data.invoice } })
|
throw new InvoiceCanceledError(data.invoice, actionError)
|
||||||
}
|
}
|
||||||
|
|
||||||
return { invoice: data.invoice, check: that(data.invoice) }
|
return { invoice: data.invoice, check: that(data.invoice) }
|
||||||
|
@ -107,13 +107,13 @@ export function User ({ user, rank, statComps, className = 'mb-2', Embellish, ny
|
|||||||
<div className={styles.other}>
|
<div className={styles.other}>
|
||||||
{statComps.map((Comp, i) => <Comp key={i} user={user} />)}
|
{statComps.map((Comp, i) => <Comp key={i} user={user} />)}
|
||||||
</div>}
|
</div>}
|
||||||
{Embellish && <Embellish rank={rank} />}
|
{Embellish && <Embellish rank={rank} user={user} />}
|
||||||
</UserBase>
|
</UserBase>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function UserHidden ({ rank, Embellish }) {
|
function UserHidden ({ rank, user, Embellish }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{rank
|
{rank
|
||||||
@ -133,7 +133,7 @@ function UserHidden ({ rank, Embellish }) {
|
|||||||
<div className={`${styles.title} text-muted d-inline-flex align-items-center`}>
|
<div className={`${styles.title} text-muted d-inline-flex align-items-center`}>
|
||||||
stacker is in hiding
|
stacker is in hiding
|
||||||
</div>
|
</div>
|
||||||
{Embellish && <Embellish rank={rank} />}
|
{Embellish && <Embellish rank={rank} user={user} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@ -148,7 +148,7 @@ export function ListUsers ({ users, rank, statComps = DEFAULT_STAT_COMPONENTS, E
|
|||||||
{users.map((user, i) => (
|
{users.map((user, i) => (
|
||||||
user
|
user
|
||||||
? <User key={user.id} user={user} rank={rank && i + 1} statComps={statComps} Embellish={Embellish} nymActionDropdown={nymActionDropdown} />
|
? <User key={user.id} user={user} rank={rank && i + 1} statComps={statComps} Embellish={Embellish} nymActionDropdown={nymActionDropdown} />
|
||||||
: <UserHidden key={i} rank={rank && i + 1} Embellish={Embellish} />
|
: <UserHidden key={i} rank={rank && i + 1} user={user} Embellish={Embellish} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -133,7 +133,7 @@ export function useWalletLogManager (setLogs) {
|
|||||||
`,
|
`,
|
||||||
{
|
{
|
||||||
onCompleted: (_, { variables: { wallet: walletType } }) => {
|
onCompleted: (_, { variables: { wallet: walletType } }) => {
|
||||||
setLogs?.(logs => logs.filter(l => walletType ? l.wallet !== getWalletByType(walletType).name : false))
|
setLogs?.(logs => logs.filter(l => walletType ? l.wallet !== walletTag(getWalletByType(walletType)) : false))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -259,12 +259,12 @@ export function useWalletLogs (wallet, initialPage = 1, logsPerPage = 10) {
|
|||||||
if (hasMore) {
|
if (hasMore) {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
const result = await loadLogsPage(page + 1, logsPerPage, wallet?.def)
|
const result = await loadLogsPage(page + 1, logsPerPage, wallet?.def)
|
||||||
_setLogs(prevLogs => uniqueSort([...prevLogs, ...result.data]))
|
setLogs(prevLogs => uniqueSort([...prevLogs, ...result.data]))
|
||||||
setHasMore(result.hasMore)
|
setHasMore(result.hasMore)
|
||||||
setPage(prevPage => prevPage + 1)
|
setPage(prevPage => prevPage + 1)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}, [loadLogsPage, page, logsPerPage, wallet?.def, hasMore])
|
}, [setLogs, loadLogsPage, page, logsPerPage, wallet?.def, hasMore])
|
||||||
|
|
||||||
const loadNew = useCallback(async () => {
|
const loadNew = useCallback(async () => {
|
||||||
const latestTs = latestTimestamp.current
|
const latestTs = latestTimestamp.current
|
||||||
|
@ -241,12 +241,15 @@ services:
|
|||||||
- '-debug=1'
|
- '-debug=1'
|
||||||
- '-zmqpubrawblock=tcp://0.0.0.0:${ZMQ_BLOCK_PORT}'
|
- '-zmqpubrawblock=tcp://0.0.0.0:${ZMQ_BLOCK_PORT}'
|
||||||
- '-zmqpubrawtx=tcp://0.0.0.0:${ZMQ_TX_PORT}'
|
- '-zmqpubrawtx=tcp://0.0.0.0:${ZMQ_TX_PORT}'
|
||||||
|
- '-zmqpubhashblock=tcp://bitcoin:${ZMQ_HASHBLOCK_PORT}'
|
||||||
- '-txindex=1'
|
- '-txindex=1'
|
||||||
- '-dnsseed=0'
|
- '-dnsseed=0'
|
||||||
- '-upnp=0'
|
- '-upnp=0'
|
||||||
- '-rpcbind=0.0.0.0'
|
- '-rpcbind=0.0.0.0'
|
||||||
- '-rpcallowip=0.0.0.0/0'
|
- '-rpcallowip=0.0.0.0/0'
|
||||||
|
- '-whitelist=0.0.0.0/0'
|
||||||
- '-rpcport=${RPC_PORT}'
|
- '-rpcport=${RPC_PORT}'
|
||||||
|
- '-deprecatedrpc=signrawtransaction'
|
||||||
- '-rest'
|
- '-rest'
|
||||||
- '-listen=1'
|
- '-listen=1'
|
||||||
- '-listenonion=0'
|
- '-listenonion=0'
|
||||||
@ -262,6 +265,8 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- bitcoin:/home/bitcoin/.bitcoin
|
- bitcoin:/home/bitcoin/.bitcoin
|
||||||
labels:
|
labels:
|
||||||
|
CLI: "bitcoin-cli"
|
||||||
|
CLI_ARGS: "-chain=regtest -rpcport=${RPC_PORT} -rpcuser=${RPC_USER} -rpcpassword=${RPC_PASS}"
|
||||||
ofelia.enabled: "true"
|
ofelia.enabled: "true"
|
||||||
ofelia.job-exec.minecron.schedule: "@every 1m"
|
ofelia.job-exec.minecron.schedule: "@every 1m"
|
||||||
ofelia.job-exec.minecron.command: >
|
ofelia.job-exec.minecron.command: >
|
||||||
@ -270,12 +275,14 @@ services:
|
|||||||
command bitcoin-cli -chain=regtest -rpcport=${RPC_PORT} -rpcuser=${RPC_USER} -rpcpassword=${RPC_PASS} "$$@"
|
command bitcoin-cli -chain=regtest -rpcport=${RPC_PORT} -rpcuser=${RPC_USER} -rpcpassword=${RPC_PASS} "$$@"
|
||||||
}
|
}
|
||||||
blockcount=$$(bitcoin-cli getblockcount 2>/dev/null)
|
blockcount=$$(bitcoin-cli getblockcount 2>/dev/null)
|
||||||
nodes=(${LND_ADDR} ${STACKER_LND_ADDR} ${STACKER_CLN_ADDR})
|
|
||||||
|
nodes=(${SN_LND_ADDR} ${LND_ADDR} ${CLN_ADDR} ${ROUTER_LND_ADDR} ${ECLAIR_ADDR})
|
||||||
|
|
||||||
if (( blockcount <= 0 )); then
|
if (( blockcount <= 0 )); then
|
||||||
echo "Creating wallet and address..."
|
echo "Creating wallet and address..."
|
||||||
bitcoin-cli createwallet ""
|
bitcoin-cli createwallet ""
|
||||||
nodes+=($$(bitcoin-cli getnewaddress))
|
nodes+=($$(bitcoin-cli getnewaddress))
|
||||||
echo "Mining 100 blocks to sn_lnd, lnd, cln..."
|
echo "Mining 100 blocks to sn_lnd, lnd, cln, eclair..."
|
||||||
for addr in "$${nodes[@]}"; do
|
for addr in "$${nodes[@]}"; do
|
||||||
bitcoin-cli generatetoaddress 100 $$addr
|
bitcoin-cli generatetoaddress 100 $$addr
|
||||||
echo "Mining 100 blocks to a random address..."
|
echo "Mining 100 blocks to a random address..."
|
||||||
@ -341,11 +348,15 @@ services:
|
|||||||
- '--allow-circular-route'
|
- '--allow-circular-route'
|
||||||
- '--bitcoin.defaultchanconfs=1'
|
- '--bitcoin.defaultchanconfs=1'
|
||||||
- '--maxpendingchannels=10'
|
- '--maxpendingchannels=10'
|
||||||
|
- '--gossip.sub-batch-delay=1s'
|
||||||
|
- '--protocol.custom-message=513'
|
||||||
|
- '--protocol.custom-nodeann=39'
|
||||||
|
- '--protocol.custom-init=39'
|
||||||
expose:
|
expose:
|
||||||
- "9735"
|
- "9735"
|
||||||
ports:
|
ports:
|
||||||
- "${LND_REST_PORT}:8080"
|
- "${SN_LND_REST_PORT}:8080"
|
||||||
- "${LND_GRPC_PORT}:10009"
|
- "${SN_LND_GRPC_PORT}:10009"
|
||||||
volumes:
|
volumes:
|
||||||
- sn_lnd:/home/lnd/.lnd
|
- sn_lnd:/home/lnd/.lnd
|
||||||
labels:
|
labels:
|
||||||
@ -358,11 +369,39 @@ services:
|
|||||||
if [ $$(lncli getinfo | jq '.num_active_channels + .num_pending_channels') -ge 3 ]; then
|
if [ $$(lncli getinfo | jq '.num_active_channels + .num_pending_channels') -ge 3 ]; then
|
||||||
exit 0
|
exit 0
|
||||||
else
|
else
|
||||||
lncli openchannel --node_key=$STACKER_LND_PUBKEY --connect lnd:9735 --sat_per_vbyte 1 \\
|
lncli openchannel --node_key=$ROUTER_LND_PUBKEY --connect router_lnd:9735 --sat_per_vbyte 1 \\
|
||||||
--min_confs 0 --local_amt=1000000000 --push_amt=500000000
|
--min_confs 0 --local_amt=1000000000 --push_amt=500000000
|
||||||
fi
|
fi
|
||||||
"
|
"
|
||||||
cpu_shares: "${CPU_SHARES_MODERATE}"
|
cpu_shares: "${CPU_SHARES_MODERATE}"
|
||||||
|
sn_lndk:
|
||||||
|
platform: linux/x86_64
|
||||||
|
build:
|
||||||
|
context: ./docker/lndk
|
||||||
|
container_name: sn_lndk
|
||||||
|
restart: unless-stopped
|
||||||
|
profiles:
|
||||||
|
- wallets
|
||||||
|
depends_on:
|
||||||
|
sn_lnd:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: true
|
||||||
|
env_file: *env_file
|
||||||
|
command:
|
||||||
|
- 'lndk'
|
||||||
|
- '--grpc-host=0.0.0.0'
|
||||||
|
- '--address=https://sn_lnd:10009'
|
||||||
|
- '--cert-path=/home/lnd/.lnd/tls.cert'
|
||||||
|
- '--tls-ip=sn_lndk'
|
||||||
|
- '--macaroon-path=/home/lnd/.lnd/data/chain/bitcoin/regtest/admin.macaroon'
|
||||||
|
ports:
|
||||||
|
- "${SN_LNDK_GRPC_PORT}:7000"
|
||||||
|
volumes:
|
||||||
|
- sn_lnd:/home/lnd/.lnd
|
||||||
|
labels:
|
||||||
|
CLI: "lndk-cli --macaroon-path=/home/lnd/.lnd/data/chain/bitcoin/regtest/admin.macaroon"
|
||||||
|
CLI_USER: "lndk"
|
||||||
|
cpu_shares: "${CPU_SHARES_MODERATE}"
|
||||||
lnd:
|
lnd:
|
||||||
build:
|
build:
|
||||||
context: ./docker/lnd
|
context: ./docker/lnd
|
||||||
@ -412,8 +451,8 @@ services:
|
|||||||
- "9735"
|
- "9735"
|
||||||
- "10009"
|
- "10009"
|
||||||
ports:
|
ports:
|
||||||
- "${STACKER_LND_REST_PORT}:8080"
|
- "${LND_REST_PORT}:8080"
|
||||||
- "${STACKER_LND_GRPC_PORT}:10009"
|
- "${LND_GRPC_PORT}:10009"
|
||||||
volumes:
|
volumes:
|
||||||
- lnd:/home/lnd/.lnd
|
- lnd:/home/lnd/.lnd
|
||||||
- tordata:/home/lnd/.tor
|
- tordata:/home/lnd/.tor
|
||||||
@ -429,7 +468,7 @@ services:
|
|||||||
if [ $$(lncli getinfo | jq '.num_active_channels + .num_pending_channels') -ge 3 ]; then
|
if [ $$(lncli getinfo | jq '.num_active_channels + .num_pending_channels') -ge 3 ]; then
|
||||||
exit 0
|
exit 0
|
||||||
else
|
else
|
||||||
lncli openchannel --node_key=$LND_PUBKEY --connect sn_lnd:9735 --sat_per_vbyte 1 \\
|
lncli openchannel --node_key=$ROUTER_LND_PUBKEY --connect router_lnd:9735 --sat_per_vbyte 1 \\
|
||||||
--min_confs 0 --local_amt=1000000000 --push_amt=500000000
|
--min_confs 0 --local_amt=1000000000 --push_amt=500000000
|
||||||
fi
|
fi
|
||||||
"
|
"
|
||||||
@ -477,7 +516,7 @@ services:
|
|||||||
container_name: cln
|
container_name: cln
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
profiles:
|
profiles:
|
||||||
- payments
|
- wallets
|
||||||
healthcheck:
|
healthcheck:
|
||||||
<<: *healthcheck
|
<<: *healthcheck
|
||||||
test: ["CMD-SHELL", "su clightning -c 'lightning-cli --network=regtest getinfo'"]
|
test: ["CMD-SHELL", "su clightning -c 'lightning-cli --network=regtest getinfo'"]
|
||||||
@ -489,6 +528,8 @@ services:
|
|||||||
env_file: *env_file
|
env_file: *env_file
|
||||||
command:
|
command:
|
||||||
- 'lightningd'
|
- 'lightningd'
|
||||||
|
- '--addr=0.0.0.0:9735'
|
||||||
|
- '--announce-addr=cln:9735'
|
||||||
- '--network=regtest'
|
- '--network=regtest'
|
||||||
- '--alias=cln'
|
- '--alias=cln'
|
||||||
- '--bitcoin-rpcconnect=bitcoin'
|
- '--bitcoin-rpcconnect=bitcoin'
|
||||||
@ -497,11 +538,10 @@ services:
|
|||||||
- '--large-channels'
|
- '--large-channels'
|
||||||
- '--rest-port=3010'
|
- '--rest-port=3010'
|
||||||
- '--rest-host=0.0.0.0'
|
- '--rest-host=0.0.0.0'
|
||||||
- '--log-file=/home/clightning/.lightning/debug.log'
|
|
||||||
expose:
|
expose:
|
||||||
- "9735"
|
- "9735"
|
||||||
ports:
|
ports:
|
||||||
- "${STACKER_CLN_REST_PORT}:3010"
|
- "${CLN_REST_PORT}:3010"
|
||||||
volumes:
|
volumes:
|
||||||
- cln:/home/clightning/.lightning
|
- cln:/home/clightning/.lightning
|
||||||
- tordata:/home/clightning/.tor
|
- tordata:/home/clightning/.tor
|
||||||
@ -517,12 +557,120 @@ services:
|
|||||||
if [ $$(lightning-cli --regtest getinfo | jq '.num_active_channels + .num_pending_channels') -ge 3 ]; then
|
if [ $$(lightning-cli --regtest getinfo | jq '.num_active_channels + .num_pending_channels') -ge 3 ]; then
|
||||||
exit 0
|
exit 0
|
||||||
else
|
else
|
||||||
lightning-cli --regtest connect $LND_PUBKEY@sn_lnd:9735
|
lightning-cli --regtest connect $ROUTER_LND_PUBKEY@router_lnd:9735
|
||||||
lightning-cli --regtest fundchannel id=$LND_PUBKEY feerate=1000perkb \\
|
lightning-cli --regtest fundchannel id=$ROUTER_LND_PUBKEY feerate=1000perkb \\
|
||||||
amount=1000000000 push_msat=500000000000 minconf=0
|
amount=1000000000 push_msat=500000000000 minconf=0
|
||||||
fi
|
fi
|
||||||
"
|
"
|
||||||
cpu_shares: "${CPU_SHARES_MODERATE}"
|
cpu_shares: "${CPU_SHARES_MODERATE}"
|
||||||
|
eclair:
|
||||||
|
build:
|
||||||
|
context: ./docker/eclair
|
||||||
|
args:
|
||||||
|
- LN_NODE_FOR=stacker
|
||||||
|
container_name: eclair
|
||||||
|
profiles:
|
||||||
|
- wallets
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
<<: *depends_on_bitcoin
|
||||||
|
environment:
|
||||||
|
<<: *env_file
|
||||||
|
JAVA_OPTS:
|
||||||
|
-Declair.printToConsole
|
||||||
|
-Dakka.loglevel=DEBUG
|
||||||
|
-Declair.server.port=9735
|
||||||
|
-Declair.server.public-ips.0=eclair
|
||||||
|
-Declair.api.binding-ip=0.0.0.0
|
||||||
|
-Declair.api.enabled=true
|
||||||
|
-Declair.api.port=8080
|
||||||
|
-Declair.api.password=pass
|
||||||
|
-Declair.node-alias=eclair
|
||||||
|
-Declair.chain=regtest
|
||||||
|
-Declair.bitcoind.host=bitcoin
|
||||||
|
-Declair.bitcoind.rpcport=${RPC_PORT}
|
||||||
|
-Declair.bitcoind.rpcuser=${RPC_USER}
|
||||||
|
-Declair.bitcoind.rpcpassword=${RPC_PASS}
|
||||||
|
-Declair.bitcoind.zmqblock=tcp://bitcoin:${ZMQ_HASHBLOCK_PORT}
|
||||||
|
-Declair.bitcoind.zmqtx=tcp://bitcoin:${ZMQ_TX_PORT}
|
||||||
|
-Declair.bitcoind.batch-watcher-requests=false
|
||||||
|
-Declair.features.option_onion_messages=optional
|
||||||
|
-Declair.features.option_route_blinding=optional
|
||||||
|
-Declair.features.keysend=optional
|
||||||
|
-Declair.channel.accept-incoming-static-remote-key-channels=true
|
||||||
|
-Declair.tip-jar.description=bolt12
|
||||||
|
-Declair.tip-jar.default-amount-msat=100000000
|
||||||
|
-Declair.tip-jar.max-final-expiry-delta=1000
|
||||||
|
volumes:
|
||||||
|
- eclair:/data
|
||||||
|
expose:
|
||||||
|
- "9735"
|
||||||
|
labels:
|
||||||
|
CLI: "eclair-cli"
|
||||||
|
CLI_USER: "root"
|
||||||
|
CLI_ARGS: "-p pass"
|
||||||
|
ofelia.enabled: "true"
|
||||||
|
ofelia.job-exec.eclair_channel_cron.schedule: "@every 1m"
|
||||||
|
ofelia.job-exec.eclair_channel_cron.command: >
|
||||||
|
bash -c "
|
||||||
|
if [ $$(eclair-cli -p pass channels | jq 'length') -ge 3 ]; then
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
eclair-cli -p pass connect --uri=$SN_LND_PUBKEY@sn_lnd:9735
|
||||||
|
eclair-cli -p pass open --nodeId=$SN_LND_PUBKEY --fundingFeerateSatByte=1 --fundingSatoshis=1000000 --pushMsat=500000000 --announceChannel=true
|
||||||
|
fi
|
||||||
|
"
|
||||||
|
router_lnd:
|
||||||
|
build:
|
||||||
|
context: ./docker/lnd
|
||||||
|
args:
|
||||||
|
- LN_NODE_FOR=router
|
||||||
|
container_name: router_lnd
|
||||||
|
restart: unless-stopped
|
||||||
|
profiles:
|
||||||
|
- payments
|
||||||
|
healthcheck:
|
||||||
|
<<: *healthcheck
|
||||||
|
test: ["CMD-SHELL", "lncli", "getinfo"]
|
||||||
|
depends_on: *depends_on_bitcoin
|
||||||
|
env_file: *env_file
|
||||||
|
command:
|
||||||
|
- 'lnd'
|
||||||
|
- '--noseedbackup'
|
||||||
|
- '--trickledelay=5000'
|
||||||
|
- '--alias=router_lnd'
|
||||||
|
- '--externalip=router_lnd'
|
||||||
|
- '--tlsextradomain=router_lnd'
|
||||||
|
- '--tlsextradomain=host.docker.internal'
|
||||||
|
- '--listen=0.0.0.0:9735'
|
||||||
|
- '--rpclisten=0.0.0.0:10009'
|
||||||
|
- '--restlisten=0.0.0.0:8080'
|
||||||
|
- '--bitcoin.active'
|
||||||
|
- '--bitcoin.regtest'
|
||||||
|
- '--bitcoin.node=bitcoind'
|
||||||
|
- '--bitcoind.rpchost=bitcoin'
|
||||||
|
- '--bitcoind.rpcuser=${RPC_USER}'
|
||||||
|
- '--bitcoind.rpcpass=${RPC_PASS}'
|
||||||
|
- '--bitcoind.zmqpubrawblock=tcp://bitcoin:${ZMQ_BLOCK_PORT}'
|
||||||
|
- '--bitcoind.zmqpubrawtx=tcp://bitcoin:${ZMQ_TX_PORT}'
|
||||||
|
- '--protocol.wumbo-channels'
|
||||||
|
- '--bitcoin.basefee=1000'
|
||||||
|
- '--bitcoin.feerate=0'
|
||||||
|
- '--maxchansize=1000000000'
|
||||||
|
- '--allow-circular-route'
|
||||||
|
- '--bitcoin.defaultchanconfs=1'
|
||||||
|
- '--maxpendingchannels=10'
|
||||||
|
expose:
|
||||||
|
- "9735"
|
||||||
|
ports:
|
||||||
|
- "${ROUTER_LND_REST_PORT}:8080"
|
||||||
|
- "${ROUTER_LND_GRPC_PORT}:10009"
|
||||||
|
volumes:
|
||||||
|
- router_lnd:/home/lnd/.lnd
|
||||||
|
labels:
|
||||||
|
CLI: "lncli"
|
||||||
|
CLI_USER: "lnd"
|
||||||
|
cpu_shares: "${CPU_SHARES_MODERATE}"
|
||||||
channdler:
|
channdler:
|
||||||
image: mcuadros/ofelia:latest
|
image: mcuadros/ofelia:latest
|
||||||
container_name: channdler
|
container_name: channdler
|
||||||
@ -532,7 +680,6 @@ services:
|
|||||||
- bitcoin
|
- bitcoin
|
||||||
- sn_lnd
|
- sn_lnd
|
||||||
- lnd
|
- lnd
|
||||||
- cln
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: daemon --docker -f label=com.docker.compose.project=${COMPOSE_PROJECT_NAME}
|
command: daemon --docker -f label=com.docker.compose.project=${COMPOSE_PROJECT_NAME}
|
||||||
volumes:
|
volumes:
|
||||||
@ -660,7 +807,9 @@ volumes:
|
|||||||
sn_lnd:
|
sn_lnd:
|
||||||
lnd:
|
lnd:
|
||||||
cln:
|
cln:
|
||||||
|
router_lnd:
|
||||||
s3:
|
s3:
|
||||||
nwc_send:
|
nwc_send:
|
||||||
nwc_recv:
|
nwc_recv:
|
||||||
tordata:
|
tordata:
|
||||||
|
eclair:
|
68
docker/eclair/Dockerfile
Normal file
68
docker/eclair/Dockerfile
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
# based on https://github.com/LN-Zap/bolt12-playground
|
||||||
|
FROM acinq/eclair:0.11.0
|
||||||
|
|
||||||
|
|
||||||
|
ENTRYPOINT JAVA_OPTS="${JAVA_OPTS}" eclair-node/bin/eclair-node.sh "-Declair.datadir=${ECLAIR_DATADIR}"
|
||||||
|
|
||||||
|
#################
|
||||||
|
# Builder image #
|
||||||
|
#################
|
||||||
|
FROM maven:3.8.6-openjdk-11-slim AS builder
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
git \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# References for eclair
|
||||||
|
ARG ECLAIR_REF=b73a009a1d7d7ea3a158776cd233512b9a538550
|
||||||
|
ARG ECLAIR_PLUGINS_REF=cdc26dda96774fdc3b54075df078587574891fb7
|
||||||
|
|
||||||
|
WORKDIR /usr/src/eclair
|
||||||
|
RUN git clone https://github.com/ACINQ/eclair.git . \
|
||||||
|
&& git reset --hard ${ECLAIR_REF}
|
||||||
|
RUN mvn install -pl eclair-node -am -DskipTests -Dgit.commit.id=notag -Dgit.commit.id.abbrev=notag
|
||||||
|
|
||||||
|
WORKDIR /usr/src/eclair-plugins
|
||||||
|
RUN git clone https://github.com/ACINQ/eclair-plugins.git . \
|
||||||
|
&& git reset --hard ${ECLAIR_PLUGINS_REF}
|
||||||
|
WORKDIR /usr/src/eclair-plugins/bolt12-tip-jar
|
||||||
|
RUN mvn package -DskipTests
|
||||||
|
|
||||||
|
# ###############
|
||||||
|
# # final image #
|
||||||
|
# ###############
|
||||||
|
FROM openjdk:11.0.16-jre-slim-bullseye
|
||||||
|
WORKDIR /opt
|
||||||
|
|
||||||
|
# Add utils
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
bash jq curl unzip \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# copy and install eclair-cli executable
|
||||||
|
COPY --from=builder /usr/src/eclair/eclair-core/eclair-cli .
|
||||||
|
RUN chmod +x eclair-cli && mv eclair-cli /sbin/eclair-cli
|
||||||
|
|
||||||
|
# we only need the eclair-node.zip to run
|
||||||
|
COPY --from=builder /usr/src/eclair/eclair-node/target/eclair-node-*.zip ./eclair-node.zip
|
||||||
|
RUN unzip eclair-node.zip && mv eclair-node-* eclair-node && chmod +x eclair-node/bin/eclair-node.sh
|
||||||
|
|
||||||
|
# copy and install bolt12-tip-jar plugin
|
||||||
|
COPY --from=builder /usr/src/eclair-plugins/bolt12-tip-jar/target/bolt12-tip-jar-0.10.1-SNAPSHOT.jar .
|
||||||
|
|
||||||
|
ENV ECLAIR_DATADIR=/data
|
||||||
|
ENV JAVA_OPTS=
|
||||||
|
|
||||||
|
RUN mkdir -p "$ECLAIR_DATADIR"
|
||||||
|
VOLUME [ "/data" ]
|
||||||
|
|
||||||
|
ARG LN_NODE_FOR
|
||||||
|
ENV LN_NODE_FOR=$LN_NODE_FOR
|
||||||
|
COPY ["./$LN_NODE_FOR/*", "/data"]
|
||||||
|
|
||||||
|
# ENTRYPOINT JAVA_OPTS="${JAVA_OPTS}" eclair-node/bin/eclair-node.sh "-Declair.datadir=${ECLAIR_DATADIR}"
|
||||||
|
ENTRYPOINT JAVA_OPTS="${JAVA_OPTS}" eclair-node/bin/eclair-node.sh bolt12-tip-jar-0.10.1-SNAPSHOT.jar "-Declair.datadir=${ECLAIR_DATADIR}"
|
1
docker/eclair/stacker/channel_seed.dat
Normal file
1
docker/eclair/stacker/channel_seed.dat
Normal file
@ -0,0 +1 @@
|
|||||||
|
フマthC(涬トロレBワモqFノ<46>`iBチL)L<><4C><EFBFBD>
|
1
docker/eclair/stacker/node_seed.dat
Normal file
1
docker/eclair/stacker/node_seed.dat
Normal file
@ -0,0 +1 @@
|
|||||||
|
6═У1>Т▀ВbgOоЕ╣©░ь}Нk ┴!sb÷²У÷
|
BIN
docker/eclair/stacker/regtest/audit.sqlite
Normal file
BIN
docker/eclair/stacker/regtest/audit.sqlite
Normal file
Binary file not shown.
BIN
docker/eclair/stacker/regtest/eclair.sqlite
Normal file
BIN
docker/eclair/stacker/regtest/eclair.sqlite
Normal file
Binary file not shown.
1
docker/eclair/stacker/regtest/last_jdbcurl
Normal file
1
docker/eclair/stacker/regtest/last_jdbcurl
Normal file
@ -0,0 +1 @@
|
|||||||
|
sqlite
|
BIN
docker/eclair/stacker/regtest/network.sqlite
Normal file
BIN
docker/eclair/stacker/regtest/network.sqlite
Normal file
Binary file not shown.
BIN
docker/lnd/router/regtest/admin.macaroon
Normal file
BIN
docker/lnd/router/regtest/admin.macaroon
Normal file
Binary file not shown.
BIN
docker/lnd/router/regtest/macaroons.db
Normal file
BIN
docker/lnd/router/regtest/macaroons.db
Normal file
Binary file not shown.
BIN
docker/lnd/router/regtest/wallet.db
Normal file
BIN
docker/lnd/router/regtest/wallet.db
Normal file
Binary file not shown.
15
docker/lnd/router/tls.cert
Normal file
15
docker/lnd/router/tls.cert
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIICRzCCAe2gAwIBAgIRALrTKBEy2NhGUue4RgGKhpgwCgYIKoZIzj0EAwIwODEf
|
||||||
|
MB0GA1UEChMWbG5kIGF1dG9nZW5lcmF0ZWQgY2VydDEVMBMGA1UEAxMMNWRhMzQx
|
||||||
|
OTEwZDAyMB4XDTI0MTIwOTA4MzcxOVoXDTI2MDIwMzA4MzcxOVowODEfMB0GA1UE
|
||||||
|
ChMWbG5kIGF1dG9nZW5lcmF0ZWQgY2VydDEVMBMGA1UEAxMMNWRhMzQxOTEwZDAy
|
||||||
|
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFPJk3jfBfWyHM7TB2pCJ45J5VqVI
|
||||||
|
r9x4nvBIZPQdvizgV4qqiNnnKTohZtH7eJ/T/epN3V9UNH3jW5MTcnIv+qOB1zCB
|
||||||
|
1DAOBgNVHQ8BAf8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH/
|
||||||
|
BAUwAwEB/zAdBgNVHQ4EFgQUN7Er1+iR3NeiwJXqLMD6CXb86qIwfQYDVR0RBHYw
|
||||||
|
dIIMNWRhMzQxOTEwZDAygglsb2NhbGhvc3SCCnJvdXRlcl9sbmSCFGhvc3QuZG9j
|
||||||
|
a2VyLmludGVybmFsggR1bml4ggp1bml4cGFja2V0ggdidWZjb25uhwR/AAABhxAA
|
||||||
|
AAAAAAAAAAAAAAAAAAABhwSsEgAJMAoGCCqGSM49BAMCA0gAMEUCIAucaM+ZivUy
|
||||||
|
G2PDcCfQZGDa0O8XVGQwofI2ZpMQwVe6AiEA9vYnOSZG1ozi0IKNgqbEs3ObByjE
|
||||||
|
dM+krTDuPzk8Kd4=
|
||||||
|
-----END CERTIFICATE-----
|
5
docker/lnd/router/tls.key
Normal file
5
docker/lnd/router/tls.key
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
-----BEGIN EC PRIVATE KEY-----
|
||||||
|
MHcCAQEEIBThCj41Abt/iEDYYMXb+mfHJmXN211JGYDjekJOmCbUoAoGCCqGSM49
|
||||||
|
AwEHoUQDQgAEFPJk3jfBfWyHM7TB2pCJ45J5VqVIr9x4nvBIZPQdvizgV4qqiNnn
|
||||||
|
KTohZtH7eJ/T/epN3V9UNH3jW5MTcnIv+g==
|
||||||
|
-----END EC PRIVATE KEY-----
|
17
docker/lndk/Dockerfile
Normal file
17
docker/lndk/Dockerfile
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# This image uses fedora 40 because the official pre-built lndk binaries require
|
||||||
|
# glibc 2.39 which is not available on debian or ubuntu images.
|
||||||
|
FROM fedora:40
|
||||||
|
RUN useradd -u 1000 -m lndk
|
||||||
|
|
||||||
|
RUN mkdir -p /home/lndk/.lndk
|
||||||
|
COPY ["./tls-*", "/home/lndk/.lndk"]
|
||||||
|
RUN chown 1000:1000 -Rvf /home/lndk/.lndk && \
|
||||||
|
chmod 644 /home/lndk/.lndk/tls-cert.pem && \
|
||||||
|
chmod 600 /home/lndk/.lndk/tls-key.pem
|
||||||
|
|
||||||
|
USER lndk
|
||||||
|
RUN curl --proto '=https' --tlsv1.2 -LsSf https://github.com/lndk-org/lndk/releases/download/v0.2.0/lndk-installer.sh | sh
|
||||||
|
RUN echo 'source /home/lndk/.cargo/env' >> $HOME/.bashrc
|
||||||
|
WORKDIR /home/lndk
|
||||||
|
EXPOSE 7000
|
||||||
|
ENV PATH="/home/lndk/.cargo/bin:${PATH}"
|
10
docker/lndk/tls-cert.pem
Normal file
10
docker/lndk/tls-cert.pem
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIBaDCCAQ2gAwIBAgIUOms3xZ+pBVUntnFD7J0m7Ll1MZYwCgYIKoZIzj0EAwIw
|
||||||
|
ITEfMB0GA1UEAwwWcmNnZW4gc2VsZiBzaWduZWQgY2VydDAgFw03NTAxMDEwMDAw
|
||||||
|
MDBaGA80MDk2MDEwMTAwMDAwMFowITEfMB0GA1UEAwwWcmNnZW4gc2VsZiBzaWdu
|
||||||
|
ZWQgY2VydDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABGdu9cXUGSPIycSCbmGb
|
||||||
|
6/4U+txvE0aSvzsMc+pKFiXlB+P/3x/WxYMxlHB0lh9fTQU8tdViJ2AY/QnHVwUk
|
||||||
|
O4CjITAfMB0GA1UdEQQWMBSCCWxvY2FsaG9zdIIHc25fbG5kazAKBggqhkjOPQQD
|
||||||
|
AgNJADBGAiEA78UdPHgdaXVyttqt21+uWTlFn4B6queGL/cmYpQbiIsCIQCwxY0n
|
||||||
|
x2v5zXEwPU/bOnaQNeq9F8AT+/4lKelHfON/Gw==
|
||||||
|
-----END CERTIFICATE-----
|
5
docker/lndk/tls-key.pem
Normal file
5
docker/lndk/tls-key.pem
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgTa/r2pnmB05EwKk6
|
||||||
|
a4FbigSagGBok+i/ASxkG9iGedWhRANCAARnbvXF1BkjyMnEgm5hm+v+FPrcbxNG
|
||||||
|
kr87DHPqShYl5Qfj/98f1sWDMZRwdJYfX00FPLXVYidgGP0Jx1cFJDuA
|
||||||
|
-----END PRIVATE KEY-----
|
@ -254,7 +254,7 @@ export const TOP_USERS = gql`
|
|||||||
photoId
|
photoId
|
||||||
ncomments(when: $when, from: $from, to: $to)
|
ncomments(when: $when, from: $from, to: $to)
|
||||||
nposts(when: $when, from: $from, to: $to)
|
nposts(when: $when, from: $from, to: $to)
|
||||||
|
proportion
|
||||||
optional {
|
optional {
|
||||||
stacked(when: $when, from: $from, to: $to)
|
stacked(when: $when, from: $from, to: $to)
|
||||||
spent(when: $when, from: $from, to: $to)
|
spent(when: $when, from: $from, to: $to)
|
||||||
|
@ -23,6 +23,7 @@ export const INVOICE_FIELDS = gql`
|
|||||||
actionError
|
actionError
|
||||||
confirmedPreimage
|
confirmedPreimage
|
||||||
forwardedSats
|
forwardedSats
|
||||||
|
forwardStatus
|
||||||
}`
|
}`
|
||||||
|
|
||||||
export const INVOICE_FULL = gql`
|
export const INVOICE_FULL = gql`
|
||||||
|
52
lib/cln.js
52
lib/cln.js
@ -2,30 +2,44 @@ import fetch from 'cross-fetch'
|
|||||||
import crypto from 'crypto'
|
import crypto from 'crypto'
|
||||||
import { getAgent } from '@/lib/proxy'
|
import { getAgent } from '@/lib/proxy'
|
||||||
import { assertContentTypeJson, assertResponseOk } from './url'
|
import { assertContentTypeJson, assertResponseOk } from './url'
|
||||||
|
import { FetchTimeoutError } from './fetch'
|
||||||
|
import { WALLET_CREATE_INVOICE_TIMEOUT_MS } from './constants'
|
||||||
|
|
||||||
export const createInvoice = async ({ socket, rune, cert, label, description, msats, expiry }) => {
|
export const createInvoice = async ({ msats, description, expiry }, { socket, rune, cert }, { signal }) => {
|
||||||
const agent = getAgent({ hostname: socket, cert })
|
const agent = getAgent({ hostname: socket, cert })
|
||||||
|
|
||||||
const url = `${agent.protocol}//${socket}/v1/invoice`
|
const url = `${agent.protocol}//${socket}/v1/invoice`
|
||||||
const res = await fetch(url, {
|
|
||||||
method: 'POST',
|
let res
|
||||||
headers: {
|
try {
|
||||||
'Content-Type': 'application/json',
|
res = await fetch(url, {
|
||||||
Rune: rune,
|
method: 'POST',
|
||||||
// can be any node id, only required for CLN v23.08 and below
|
headers: {
|
||||||
// see https://docs.corelightning.org/docs/rest#server
|
'Content-Type': 'application/json',
|
||||||
nodeId: '02cb2e2d5a6c5b17fa67b1a883e2973c82e328fb9bd08b2b156a9e23820c87a490'
|
Rune: rune,
|
||||||
},
|
// can be any node id, only required for CLN v23.08 and below
|
||||||
agent,
|
// see https://docs.corelightning.org/docs/rest#server
|
||||||
body: JSON.stringify({
|
nodeId: '02cb2e2d5a6c5b17fa67b1a883e2973c82e328fb9bd08b2b156a9e23820c87a490'
|
||||||
// CLN requires a unique label for every invoice
|
},
|
||||||
// see https://docs.corelightning.org/reference/lightning-invoice
|
agent,
|
||||||
label: crypto.randomBytes(16).toString('hex'),
|
body: JSON.stringify({
|
||||||
description,
|
// CLN requires a unique label for every invoice
|
||||||
amount_msat: msats,
|
// see https://docs.corelightning.org/reference/lightning-invoice
|
||||||
expiry
|
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)
|
assertResponseOk(res)
|
||||||
assertContentTypeJson(res)
|
assertContentTypeJson(res)
|
||||||
|
@ -46,6 +46,7 @@ export const MAX_POST_TEXT_LENGTH = 100000 // 100k
|
|||||||
export const MAX_COMMENT_TEXT_LENGTH = 10000 // 10k
|
export const MAX_COMMENT_TEXT_LENGTH = 10000 // 10k
|
||||||
export const MAX_TERRITORY_DESC_LENGTH = 1000 // 1k
|
export const MAX_TERRITORY_DESC_LENGTH = 1000 // 1k
|
||||||
export const MAX_POLL_CHOICE_LENGTH = 40
|
export const MAX_POLL_CHOICE_LENGTH = 40
|
||||||
|
export const ITEM_EDIT_SECONDS = 600
|
||||||
export const ITEM_SPAM_INTERVAL = '10m'
|
export const ITEM_SPAM_INTERVAL = '10m'
|
||||||
export const ANON_ITEM_SPAM_INTERVAL = '0'
|
export const ANON_ITEM_SPAM_INTERVAL = '0'
|
||||||
export const INV_PENDING_LIMIT = 100
|
export const INV_PENDING_LIMIT = 100
|
||||||
@ -79,6 +80,7 @@ export const ANON_FEE_MULTIPLIER = 100
|
|||||||
export const SSR = typeof window === 'undefined'
|
export const SSR = typeof window === 'undefined'
|
||||||
export const MAX_FORWARDS = 5
|
export const MAX_FORWARDS = 5
|
||||||
export const LND_PATHFINDING_TIMEOUT_MS = 30000
|
export const LND_PATHFINDING_TIMEOUT_MS = 30000
|
||||||
|
export const LND_PATHFINDING_TIME_PREF_PPM = 1e6 // optimize for reliability over fees
|
||||||
export const LNURLP_COMMENT_MAX_LENGTH = 1000
|
export const LNURLP_COMMENT_MAX_LENGTH = 1000
|
||||||
// https://github.com/lightning/bolts/issues/236
|
// https://github.com/lightning/bolts/issues/236
|
||||||
export const MAX_INVOICE_DESCRIPTION_LENGTH = 640
|
export const MAX_INVOICE_DESCRIPTION_LENGTH = 640
|
||||||
@ -189,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 EXTRA_LONG_POLL_INTERVAL = Number(process.env.NEXT_PUBLIC_EXTRA_LONG_POLL_INTERVAL)
|
||||||
|
|
||||||
export const ZAP_UNDO_DELAY_MS = 5_000
|
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
|
||||||
|
32
lib/fetch.js
32
lib/fetch.js
@ -1,14 +1,28 @@
|
|||||||
export async function fetchWithTimeout (resource, { timeout = 1000, ...options } = {}) {
|
import { TimeoutError, timeoutSignal } from '@/lib/time'
|
||||||
const controller = new AbortController()
|
|
||||||
const id = setTimeout(() => controller.abort(), timeout)
|
|
||||||
|
|
||||||
const response = await fetch(resource, {
|
export class FetchTimeoutError extends TimeoutError {
|
||||||
...options,
|
constructor (method, url, timeout) {
|
||||||
signal: controller.signal
|
super(timeout)
|
||||||
})
|
this.name = 'FetchTimeoutError'
|
||||||
clearTimeout(id)
|
this.message = timeout
|
||||||
|
? `${method} ${url}: timeout after ${timeout / 1000}s`
|
||||||
|
: `${method} ${url}: timeout`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return response
|
export async function fetchWithTimeout (resource, { signal, timeout = 1000, ...options } = {}) {
|
||||||
|
try {
|
||||||
|
return await fetch(resource, {
|
||||||
|
...options,
|
||||||
|
signal: signal ?? timeoutSignal(timeout)
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
if (err.name === 'TimeoutError') {
|
||||||
|
// use custom error message
|
||||||
|
throw new FetchTimeoutError(options.method ?? 'GET', resource, err.timeout)
|
||||||
|
}
|
||||||
|
throw err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class LRUCache {
|
class LRUCache {
|
||||||
|
@ -10,7 +10,7 @@ export const defaultCommentSort = (pinned, bio, createdAt) => {
|
|||||||
return 'hot'
|
return 'hot'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isJob = item => item.subName !== 'jobs'
|
export const isJob = item => item.subName === 'jobs'
|
||||||
|
|
||||||
// a delete directive preceded by a non word character that isn't a backtick
|
// a delete directive preceded by a non word character that isn't a backtick
|
||||||
const deletePattern = /\B@delete\s+in\s+(\d+)\s+(second|minute|hour|day|week|month|year)s?/gi
|
const deletePattern = /\B@delete\s+in\s+(\d+)\s+(second|minute|hour|day|week|month|year)s?/gi
|
||||||
|
12
lib/lnurl.js
12
lib/lnurl.js
@ -1,6 +1,8 @@
|
|||||||
import { createHash } from 'crypto'
|
import { createHash } from 'crypto'
|
||||||
import { bech32 } from 'bech32'
|
import { bech32 } from 'bech32'
|
||||||
import { lnAddrSchema } from './validate'
|
import { lnAddrSchema } from './validate'
|
||||||
|
import { FetchTimeoutError } from '@/lib/fetch'
|
||||||
|
import { WALLET_CREATE_INVOICE_TIMEOUT_MS } from './constants'
|
||||||
|
|
||||||
export function encodeLNUrl (url) {
|
export function encodeLNUrl (url) {
|
||||||
const words = bech32.toWords(Buffer.from(url.toString(), 'utf8'))
|
const words = bech32.toWords(Buffer.from(url.toString(), 'utf8'))
|
||||||
@ -25,7 +27,7 @@ export function lnurlPayDescriptionHash (data) {
|
|||||||
return createHash('sha256').update(data).digest('hex')
|
return createHash('sha256').update(data).digest('hex')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function lnAddrOptions (addr) {
|
export async function lnAddrOptions (addr, { signal } = {}) {
|
||||||
await lnAddrSchema().fields.addr.validate(addr)
|
await lnAddrSchema().fields.addr.validate(addr)
|
||||||
const [name, domain] = addr.split('@')
|
const [name, domain] = addr.split('@')
|
||||||
let protocol = 'https'
|
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.`
|
const unexpectedErrorMessage = `An unexpected error occurred fetching the Lightning Address metadata for ${addr}. Check the address and try again.`
|
||||||
let res
|
let res
|
||||||
|
const url = `${protocol}://${domain}/.well-known/lnurlp/${name}`
|
||||||
try {
|
try {
|
||||||
const req = await fetch(`${protocol}://${domain}/.well-known/lnurlp/${name}`)
|
const req = await fetch(url, { signal })
|
||||||
res = await req.json()
|
res = await req.json()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// If `fetch` fails, or if `req.json` fails, catch it here and surface a reasonable error
|
|
||||||
console.log('Error fetching lnurlp', err)
|
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)
|
throw new Error(unexpectedErrorMessage)
|
||||||
}
|
}
|
||||||
if (res.status === 'ERROR') {
|
if (res.status === 'ERROR') {
|
||||||
|
300
lib/nostr.js
300
lib/nostr.js
@ -1,8 +1,6 @@
|
|||||||
import { bech32 } from 'bech32'
|
import { bech32 } from 'bech32'
|
||||||
import { nip19 } from 'nostr-tools'
|
import { nip19 } from 'nostr-tools'
|
||||||
import WebSocket from 'isomorphic-ws'
|
import NDK, { NDKEvent, NDKNip46Signer, NDKRelaySet, NDKPrivateKeySigner, NDKNip07Signer } from '@nostr-dev-kit/ndk'
|
||||||
import { callWithTimeout, withTimeout } from '@/lib/time'
|
|
||||||
import crypto from 'crypto'
|
|
||||||
|
|
||||||
export const NOSTR_PUBKEY_HEX = /^[0-9a-fA-F]{64}$/
|
export const NOSTR_PUBKEY_HEX = /^[0-9a-fA-F]{64}$/
|
||||||
export const NOSTR_PUBKEY_BECH32 = /^npub1[02-9ac-hj-np-z]+$/
|
export const NOSTR_PUBKEY_BECH32 = /^npub1[02-9ac-hj-np-z]+$/
|
||||||
@ -17,155 +15,149 @@ export const DEFAULT_CROSSPOSTING_RELAYS = [
|
|||||||
'wss://nostr.mutinywallet.com/',
|
'wss://nostr.mutinywallet.com/',
|
||||||
'wss://relay.mutinywallet.com/'
|
'wss://relay.mutinywallet.com/'
|
||||||
]
|
]
|
||||||
|
export const RELAYS_BLACKLIST = []
|
||||||
|
|
||||||
export class Relay {
|
/* eslint-disable camelcase */
|
||||||
constructor (relayUrl) {
|
|
||||||
const ws = new WebSocket(relayUrl)
|
|
||||||
|
|
||||||
ws.onmessage = (msg) => {
|
/**
|
||||||
const [type, notice] = JSON.parse(msg.data)
|
* @import {NDKSigner} from '@nostr-dev-kit/ndk'
|
||||||
if (type === 'NOTICE') {
|
* @import { NDK } from '@nostr-dev-kit/ndk'
|
||||||
console.log('relay notice:', notice)
|
* @import {NDKNwc} from '@nostr-dev-kit/ndk'
|
||||||
}
|
* @typedef {Object} Nostr
|
||||||
}
|
* @property {NDK} ndk
|
||||||
|
* @property {function(string, {logger: Object}): Promise<NDKNwc>} nwc
|
||||||
|
* @property {function(Object, {privKey: string, signer: NDKSigner}): Promise<NDKEvent>} sign
|
||||||
|
* @property {function(Object, {relays: Array<string>, privKey: string, signer: NDKSigner}): Promise<NDKEvent>} publish
|
||||||
|
*/
|
||||||
|
export class Nostr {
|
||||||
|
/**
|
||||||
|
* @type {NDK}
|
||||||
|
*/
|
||||||
|
_ndk = null
|
||||||
|
|
||||||
ws.onerror = (err) => {
|
constructor ({ privKey, defaultSigner, relays, nip46token, supportNip07 = false, ...ndkOptions } = {}) {
|
||||||
console.error('websocket error:', err.message)
|
this._ndk = new NDK({
|
||||||
this.error = err.message
|
explicitRelayUrls: relays,
|
||||||
}
|
blacklistRelayUrls: RELAYS_BLACKLIST,
|
||||||
|
autoConnectUserRelays: false,
|
||||||
this.ws = ws
|
autoFetchUserMutelist: false,
|
||||||
this.url = relayUrl
|
clientName: 'stacker.news',
|
||||||
this.error = null
|
signer: defaultSigner ?? this.getSigner({ privKey, supportNip07, nip46token }),
|
||||||
|
...ndkOptions
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
static async connect (url, { timeout } = {}) {
|
/**
|
||||||
const relay = new Relay(url)
|
* @type {NDK}
|
||||||
await relay.waitUntilConnected({ timeout })
|
*/
|
||||||
return relay
|
get ndk () {
|
||||||
|
return this._ndk
|
||||||
}
|
}
|
||||||
|
|
||||||
get connected () {
|
/**
|
||||||
return this.ws.readyState === WebSocket.OPEN
|
*
|
||||||
|
* @param {Object} param0
|
||||||
|
* @param {string} [args.privKey] - private key to use for signing
|
||||||
|
* @param {string} [args.nip46token] - NIP-46 token to use for signing
|
||||||
|
* @param {boolean} [args.supportNip07] - whether to use NIP-07 signer if available
|
||||||
|
* @returns {NDKPrivateKeySigner | NDKNip46Signer | NDKNip07Signer | null} - a signer instance
|
||||||
|
*/
|
||||||
|
getSigner ({ privKey, nip46token, supportNip07 = true } = {}) {
|
||||||
|
if (privKey) return new NDKPrivateKeySigner(privKey)
|
||||||
|
if (nip46token) return new NDKNip46SignerURLPatch(this.ndk, nip46token)
|
||||||
|
if (supportNip07 && typeof window !== 'undefined' && window?.nostr) return new NDKNip07Signer()
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
get closed () {
|
/**
|
||||||
return this.ws.readyState === WebSocket.CLOSING || this.ws.readyState === WebSocket.CLOSED
|
* @param {Object} rawEvent
|
||||||
|
* @param {number} rawEvent.kind
|
||||||
|
* @param {number} rawEvent.created_at
|
||||||
|
* @param {string} rawEvent.content
|
||||||
|
* @param {Array<Array<string>>} rawEvent.tags
|
||||||
|
* @param {Object} context
|
||||||
|
* @param {string} context.privKey
|
||||||
|
* @param {NDKSigner} context.signer
|
||||||
|
* @returns {Promise<NDKEvent>}
|
||||||
|
*/
|
||||||
|
async sign ({ kind, created_at, content, tags }, { signer } = {}) {
|
||||||
|
const event = new NDKEvent(this.ndk, {
|
||||||
|
kind,
|
||||||
|
created_at,
|
||||||
|
content,
|
||||||
|
tags
|
||||||
|
})
|
||||||
|
signer ??= this.ndk.signer
|
||||||
|
if (!signer) throw new Error('no way to sign this event, please provide a signer or private key')
|
||||||
|
await event.sign(signer)
|
||||||
|
return event
|
||||||
}
|
}
|
||||||
|
|
||||||
async waitUntilConnected ({ timeout } = {}) {
|
/**
|
||||||
let interval
|
* @param {Object} rawEvent
|
||||||
|
* @param {number} rawEvent.kind
|
||||||
|
* @param {number} rawEvent.created_at
|
||||||
|
* @param {string} rawEvent.content
|
||||||
|
* @param {Array<Array<string>>} rawEvent.tags
|
||||||
|
* @param {Object} context
|
||||||
|
* @param {Array<string>} context.relays
|
||||||
|
* @param {string} context.privKey
|
||||||
|
* @param {NDKSigner} context.signer
|
||||||
|
* @param {number} context.timeout
|
||||||
|
* @returns {Promise<NDKEvent>}
|
||||||
|
*/
|
||||||
|
async publish ({ created_at, content, tags = [], kind }, { relays, signer, timeout } = {}) {
|
||||||
|
const event = await this.sign({ kind, created_at, content, tags }, { signer })
|
||||||
|
|
||||||
const checkPromise = new Promise((resolve, reject) => {
|
const successfulRelays = []
|
||||||
interval = setInterval(() => {
|
const failedRelays = []
|
||||||
if (this.connected) {
|
|
||||||
resolve()
|
const relaySet = NDKRelaySet.fromRelayUrls(relays, this.ndk, true)
|
||||||
}
|
|
||||||
if (this.closed) {
|
event.on('relay:publish:failed', (relay, error) => {
|
||||||
reject(new Error(`failed to connect to ${this.url}: ` + this.error))
|
failedRelays.push({ relay: relay.url, error })
|
||||||
}
|
|
||||||
}, 100)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
for (const relay of (await relaySet.publish(event, timeout))) {
|
||||||
|
successfulRelays.push(relay.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
event,
|
||||||
|
successfulRelays,
|
||||||
|
failedRelays
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async crosspost ({ created_at, content, tags = [], kind }, { relays = DEFAULT_CROSSPOSTING_RELAYS, signer, timeout } = {}) {
|
||||||
try {
|
try {
|
||||||
return await withTimeout(checkPromise, timeout)
|
signer ??= this.getSigner({ supportNip07: true })
|
||||||
} catch (err) {
|
const { event: signedEvent, successfulRelays, failedRelays } = await this.publish({ created_at, content, tags, kind }, { relays, signer, timeout })
|
||||||
this.close()
|
|
||||||
throw err
|
|
||||||
} finally {
|
|
||||||
clearInterval(interval)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
close () {
|
let noteId = null
|
||||||
const state = this.ws.readyState
|
if (signedEvent.kind !== 1) {
|
||||||
if (state !== WebSocket.CLOSING && state !== WebSocket.CLOSED) {
|
noteId = await nip19.naddrEncode({
|
||||||
this.ws.close()
|
kind: signedEvent.kind,
|
||||||
}
|
pubkey: signedEvent.pubkey,
|
||||||
}
|
identifier: signedEvent.tags[0][1]
|
||||||
|
})
|
||||||
async publish (event, { timeout } = {}) {
|
} else {
|
||||||
const ws = this.ws
|
noteId = hexToBech32(signedEvent.id, 'note')
|
||||||
|
|
||||||
let listener
|
|
||||||
const ackPromise = new Promise((resolve, reject) => {
|
|
||||||
listener = function onmessage (msg) {
|
|
||||||
const [type, eventId, accepted, reason] = JSON.parse(msg.data)
|
|
||||||
|
|
||||||
if (type !== 'OK' || eventId !== event.id) return
|
|
||||||
|
|
||||||
if (accepted) {
|
|
||||||
resolve(eventId)
|
|
||||||
} else {
|
|
||||||
reject(new Error(reason || `event rejected: ${eventId}`))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.addEventListener('message', listener)
|
return { successfulRelays, failedRelays, noteId }
|
||||||
|
} catch (error) {
|
||||||
ws.send(JSON.stringify(['EVENT', event]))
|
console.error('Crosspost error:', error)
|
||||||
})
|
return { error }
|
||||||
|
|
||||||
try {
|
|
||||||
return await withTimeout(ackPromise, timeout)
|
|
||||||
} finally {
|
|
||||||
ws.removeEventListener('message', listener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fetch (filter, { timeout } = {}) {
|
|
||||||
const ws = this.ws
|
|
||||||
|
|
||||||
let listener
|
|
||||||
const ackPromise = new Promise((resolve, reject) => {
|
|
||||||
const id = crypto.randomBytes(16).toString('hex')
|
|
||||||
|
|
||||||
const events = []
|
|
||||||
let eose = false
|
|
||||||
|
|
||||||
listener = function onmessage (msg) {
|
|
||||||
const [type, subId, event] = JSON.parse(msg.data)
|
|
||||||
|
|
||||||
if (subId !== id) return
|
|
||||||
|
|
||||||
if (type === 'EVENT') {
|
|
||||||
events.push(event)
|
|
||||||
if (eose) {
|
|
||||||
// EOSE was already received:
|
|
||||||
// return first event after EOSE
|
|
||||||
resolve(events)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === 'CLOSED') {
|
|
||||||
return resolve(events)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === 'EOSE') {
|
|
||||||
eose = true
|
|
||||||
if (events.length > 0) {
|
|
||||||
// we already received events before EOSE:
|
|
||||||
// return all events before EOSE
|
|
||||||
ws.send(JSON.stringify(['CLOSE', id]))
|
|
||||||
return resolve(events)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ws.addEventListener('message', listener)
|
|
||||||
|
|
||||||
ws.send(JSON.stringify(['REQ', id, ...filter]))
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await withTimeout(ackPromise, timeout)
|
|
||||||
} finally {
|
|
||||||
ws.removeEventListener('message', listener)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Nostr}
|
||||||
|
*/
|
||||||
|
export default new Nostr()
|
||||||
|
|
||||||
export function hexToBech32 (hex, prefix = 'npub') {
|
export function hexToBech32 (hex, prefix = 'npub') {
|
||||||
return bech32.encode(prefix, bech32.toWords(Buffer.from(hex, 'hex')))
|
return bech32.encode(prefix, bech32.toWords(Buffer.from(hex, 'hex')))
|
||||||
}
|
}
|
||||||
@ -187,48 +179,10 @@ export function nostrZapDetails (zap) {
|
|||||||
return { npub, content, note }
|
return { npub, content, note }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function publishNostrEvent (signedEvent, relayUrl) {
|
// workaround NDK url parsing issue (see https://github.com/stackernews/stacker.news/pull/1636)
|
||||||
const timeout = 3000
|
class NDKNip46SignerURLPatch extends NDKNip46Signer {
|
||||||
const relay = await Relay.connect(relayUrl, { timeout })
|
connectionTokenInit (connectionToken) {
|
||||||
try {
|
connectionToken = connectionToken.replace('bunker://', 'http://')
|
||||||
await relay.publish(signedEvent, { timeout })
|
return super.connectionTokenInit(connectionToken)
|
||||||
} finally {
|
|
||||||
relay.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function crosspost (event, relays = DEFAULT_CROSSPOSTING_RELAYS) {
|
|
||||||
try {
|
|
||||||
const signedEvent = await callWithTimeout(() => window.nostr.signEvent(event), 10000)
|
|
||||||
if (!signedEvent) throw new Error('failed to sign event')
|
|
||||||
|
|
||||||
const promises = relays.map(r => publishNostrEvent(signedEvent, r))
|
|
||||||
const results = await Promise.allSettled(promises)
|
|
||||||
const successfulRelays = []
|
|
||||||
const failedRelays = []
|
|
||||||
|
|
||||||
results.forEach((result, index) => {
|
|
||||||
if (result.status === 'fulfilled') {
|
|
||||||
successfulRelays.push(relays[index])
|
|
||||||
} else {
|
|
||||||
failedRelays.push({ relay: relays[index], error: result.reason })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
let noteId = null
|
|
||||||
if (signedEvent.kind !== 1) {
|
|
||||||
noteId = await nip19.naddrEncode({
|
|
||||||
kind: signedEvent.kind,
|
|
||||||
pubkey: signedEvent.pubkey,
|
|
||||||
identifier: signedEvent.tags[0][1]
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
noteId = hexToBech32(signedEvent.id, 'note')
|
|
||||||
}
|
|
||||||
|
|
||||||
return { successfulRelays, failedRelays, noteId }
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Crosspost error:', error)
|
|
||||||
return { error }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
25
lib/time.js
25
lib/time.js
@ -128,12 +128,22 @@ function tzOffset (tz) {
|
|||||||
return targetOffsetHours
|
return targetOffsetHours
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class TimeoutError extends Error {
|
||||||
|
constructor (timeout) {
|
||||||
|
super(`timeout after ${timeout / 1000}s`)
|
||||||
|
this.name = 'TimeoutError'
|
||||||
|
this.timeout = timeout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function timeoutPromise (timeout) {
|
function timeoutPromise (timeout) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// if no timeout is specified, never settle
|
// if no timeout is specified, never settle
|
||||||
if (!timeout) return
|
if (!timeout) return
|
||||||
|
|
||||||
setTimeout(() => reject(new Error(`timeout after ${timeout / 1000}s`)), 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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,3 +154,16 @@ export async function withTimeout (promise, timeout) {
|
|||||||
export async function callWithTimeout (fn, timeout) {
|
export async function callWithTimeout (fn, timeout) {
|
||||||
return await Promise.race([fn(), timeoutPromise(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
|
||||||
|
}
|
||||||
|
@ -203,12 +203,12 @@ export function parseNwcUrl (walletConnectUrl) {
|
|||||||
const params = {}
|
const params = {}
|
||||||
params.walletPubkey = url.host
|
params.walletPubkey = url.host
|
||||||
const secret = url.searchParams.get('secret')
|
const secret = url.searchParams.get('secret')
|
||||||
const relayUrl = url.searchParams.get('relay')
|
const relayUrls = url.searchParams.getAll('relay')
|
||||||
if (secret) {
|
if (secret) {
|
||||||
params.secret = secret
|
params.secret = secret
|
||||||
}
|
}
|
||||||
if (relayUrl) {
|
if (relayUrls) {
|
||||||
params.relayUrl = relayUrl
|
params.relayUrls = relayUrls
|
||||||
}
|
}
|
||||||
return params
|
return params
|
||||||
}
|
}
|
||||||
|
@ -147,15 +147,15 @@ addMethod(string, 'nwcUrl', function () {
|
|||||||
// inspired by https://github.com/jquense/yup/issues/851#issuecomment-1049705180
|
// inspired by https://github.com/jquense/yup/issues/851#issuecomment-1049705180
|
||||||
try {
|
try {
|
||||||
string().matches(/^nostr\+?walletconnect:\/\//, { message: 'must start with nostr+walletconnect://' }).validateSync(nwcUrl)
|
string().matches(/^nostr\+?walletconnect:\/\//, { message: 'must start with nostr+walletconnect://' }).validateSync(nwcUrl)
|
||||||
let relayUrl, walletPubkey, secret
|
let relayUrls, walletPubkey, secret
|
||||||
try {
|
try {
|
||||||
({ relayUrl, walletPubkey, secret } = parseNwcUrl(nwcUrl))
|
({ relayUrls, walletPubkey, secret } = parseNwcUrl(nwcUrl))
|
||||||
} catch {
|
} catch {
|
||||||
// invalid URL error. handle as if pubkey validation failed to not confuse user.
|
// invalid URL error. handle as if pubkey validation failed to not confuse user.
|
||||||
throw new Error('pubkey must be 64 hex chars')
|
throw new Error('pubkey must be 64 hex chars')
|
||||||
}
|
}
|
||||||
string().required('pubkey required').trim().matches(NOSTR_PUBKEY_HEX, 'pubkey must be 64 hex chars').validateSync(walletPubkey)
|
string().required('pubkey required').trim().matches(NOSTR_PUBKEY_HEX, 'pubkey must be 64 hex chars').validateSync(walletPubkey)
|
||||||
string().required('relay url required').trim().wss('relay must use wss://').validateSync(relayUrl)
|
array().of(string().required('relay url required').trim().wss('relay must use wss://')).min(1, 'at least one relay required').validateSync(relayUrls)
|
||||||
string().required('secret required').trim().matches(/^[0-9a-fA-F]{64}$/, 'secret must be 64 hex chars').validateSync(secret)
|
string().required('secret required').trim().matches(/^[0-9a-fA-F]{64}$/, 'secret must be 64 hex chars').validateSync(secret)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return context.createError({ message: err.message })
|
return context.createError({ message: err.message })
|
||||||
|
273
package-lock.json
generated
273
package-lock.json
generated
@ -15,6 +15,7 @@
|
|||||||
"@graphql-tools/schema": "^10.0.6",
|
"@graphql-tools/schema": "^10.0.6",
|
||||||
"@lightninglabs/lnc-web": "^0.3.2-alpha",
|
"@lightninglabs/lnc-web": "^0.3.2-alpha",
|
||||||
"@noble/curves": "^1.6.0",
|
"@noble/curves": "^1.6.0",
|
||||||
|
"@nostr-dev-kit/ndk": "^2.10.5",
|
||||||
"@opensearch-project/opensearch": "^2.12.0",
|
"@opensearch-project/opensearch": "^2.12.0",
|
||||||
"@prisma/client": "^5.20.0",
|
"@prisma/client": "^5.20.0",
|
||||||
"@slack/web-api": "^7.6.0",
|
"@slack/web-api": "^7.6.0",
|
||||||
@ -4371,6 +4372,15 @@
|
|||||||
"url": "https://paulmillr.com/funding/"
|
"url": "https://paulmillr.com/funding/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@noble/secp256k1": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-XLEQQNdablO0XZOIniFQimiXsZDNwaYgL96dZwC54Q30imSbAOFf3NKtepc+cXyuZf5Q1HCgbqgZ2UFFuHVcEw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@nodelib/fs.scandir": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
@ -4406,6 +4416,49 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@nostr-dev-kit/ndk": {
|
||||||
|
"version": "2.10.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-2.10.5.tgz",
|
||||||
|
"integrity": "sha512-QEnarJL9BGCxeenSIE9jxNSDyYQYjzD30YL3sVJ9cNybNZX8tl/I1/vhEUeRRMBz/qjROLtt0M2RV68rZ205tg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/curves": "^1.6.0",
|
||||||
|
"@noble/hashes": "^1.5.0",
|
||||||
|
"@noble/secp256k1": "^2.1.0",
|
||||||
|
"@scure/base": "^1.1.9",
|
||||||
|
"debug": "^4.3.6",
|
||||||
|
"light-bolt11-decoder": "^3.2.0",
|
||||||
|
"nostr-tools": "^2.7.1",
|
||||||
|
"tseep": "^1.2.2",
|
||||||
|
"typescript-lru-cache": "^2.0.0",
|
||||||
|
"utf8-buffer": "^1.0.0",
|
||||||
|
"websocket-polyfill": "^0.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@nostr-dev-kit/ndk/node_modules/@noble/hashes": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.21.3 || >=16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@nostr-dev-kit/ndk/node_modules/@scure/base": {
|
||||||
|
"version": "1.1.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz",
|
||||||
|
"integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@opensearch-project/opensearch": {
|
"node_modules/@opensearch-project/opensearch": {
|
||||||
"version": "2.12.0",
|
"version": "2.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/@opensearch-project/opensearch/-/opensearch-2.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/@opensearch-project/opensearch/-/opensearch-2.12.0.tgz",
|
||||||
@ -7310,6 +7363,19 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bufferutil": {
|
||||||
|
"version": "4.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz",
|
||||||
|
"integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"node-gyp-build": "^4.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.14.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/builtins": {
|
"node_modules/builtins": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz",
|
||||||
@ -8089,6 +8155,19 @@
|
|||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/d": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"es5-ext": "^0.10.64",
|
||||||
|
"type": "^2.7.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/d3-array": {
|
"node_modules/d3-array": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||||
@ -8968,6 +9047,46 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/es5-ext": {
|
||||||
|
"version": "0.10.64",
|
||||||
|
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz",
|
||||||
|
"integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"es6-iterator": "^2.0.3",
|
||||||
|
"es6-symbol": "^3.1.3",
|
||||||
|
"esniff": "^2.0.1",
|
||||||
|
"next-tick": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es6-iterator": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"d": "1",
|
||||||
|
"es5-ext": "^0.10.35",
|
||||||
|
"es6-symbol": "^3.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es6-symbol": {
|
||||||
|
"version": "3.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz",
|
||||||
|
"integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d": "^1.0.2",
|
||||||
|
"ext": "^1.7.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.23.1",
|
"version": "0.23.1",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz",
|
||||||
@ -9581,6 +9700,21 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/esniff": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d": "^1.0.1",
|
||||||
|
"es5-ext": "^0.10.62",
|
||||||
|
"event-emitter": "^0.3.5",
|
||||||
|
"type": "^2.7.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/espree": {
|
"node_modules/espree": {
|
||||||
"version": "10.2.0",
|
"version": "10.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/espree/-/espree-10.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/espree/-/espree-10.2.0.tgz",
|
||||||
@ -9675,6 +9809,16 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/event-emitter": {
|
||||||
|
"version": "0.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
|
||||||
|
"integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"d": "1",
|
||||||
|
"es5-ext": "~0.10.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/eventemitter3": {
|
"node_modules/eventemitter3": {
|
||||||
"version": "4.0.7",
|
"version": "4.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
||||||
@ -9829,6 +9973,15 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/ext": {
|
||||||
|
"version": "1.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz",
|
||||||
|
"integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"type": "^2.7.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/extend": {
|
"node_modules/extend": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||||
@ -14154,6 +14307,15 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/light-bolt11-decoder": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/light-bolt11-decoder/-/light-bolt11-decoder-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-3QEofgiBOP4Ehs9BI+RkZdXZNtSys0nsJ6fyGeSiAGCBsMwHGUDS/JQlY/sTnWs91A2Nh0S9XXfA8Sy9g6QpuQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@scure/base": "1.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lightning": {
|
"node_modules/lightning": {
|
||||||
"version": "10.22.0",
|
"version": "10.22.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightning/-/lightning-10.22.0.tgz",
|
"resolved": "https://registry.npmjs.org/lightning/-/lightning-10.22.0.tgz",
|
||||||
@ -15606,6 +15768,12 @@
|
|||||||
"react-dom": ">=16.0.0"
|
"react-dom": ">=16.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/next-tick": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/no-case": {
|
"node_modules/no-case": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
|
||||||
@ -19408,11 +19576,23 @@
|
|||||||
"resolved": "https://registry.npmjs.org/tsdef/-/tsdef-0.0.14.tgz",
|
"resolved": "https://registry.npmjs.org/tsdef/-/tsdef-0.0.14.tgz",
|
||||||
"integrity": "sha512-UjMD4XKRWWFlFBfwKVQmGFT5YzW/ZaF8x6KpCDf92u9wgKeha/go3FU0e5WqDjXsCOdfiavCkfwfVHNDxRDGMA=="
|
"integrity": "sha512-UjMD4XKRWWFlFBfwKVQmGFT5YzW/ZaF8x6KpCDf92u9wgKeha/go3FU0e5WqDjXsCOdfiavCkfwfVHNDxRDGMA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/tseep": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tseep/-/tseep-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-ZPtfk1tQnZVyr7BPtbJ93qaAh2lZuIOpTMjhrYa4XctT8xe7t4SAW9LIxrySDuYMsfNNayE51E/WNGrNVgVicQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tslib": {
|
"node_modules/tslib": {
|
||||||
"version": "2.6.0",
|
"version": "2.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz",
|
||||||
"integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA=="
|
"integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/tstl": {
|
||||||
|
"version": "2.5.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/tstl/-/tstl-2.5.16.tgz",
|
||||||
|
"integrity": "sha512-+O2ybLVLKcBwKm4HymCEwZIT0PpwS3gCYnxfSDEjJEKADvIFruaQjd3m7CAKNU1c7N3X3WjVz87re7TA2A5FUw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tsx": {
|
"node_modules/tsx": {
|
||||||
"version": "4.19.1",
|
"version": "4.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.1.tgz",
|
||||||
@ -19452,6 +19632,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz",
|
"resolved": "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz",
|
||||||
"integrity": "sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw=="
|
"integrity": "sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/type": {
|
||||||
|
"version": "2.7.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz",
|
||||||
|
"integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/type-check": {
|
"node_modules/type-check": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||||
@ -19574,11 +19760,26 @@
|
|||||||
"node": ">= 18"
|
"node": ">= 18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/typedarray-to-buffer": {
|
||||||
|
"version": "3.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
|
||||||
|
"integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-typedarray": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/typeforce": {
|
"node_modules/typeforce": {
|
||||||
"version": "1.18.0",
|
"version": "1.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz",
|
||||||
"integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g=="
|
"integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g=="
|
||||||
},
|
},
|
||||||
|
"node_modules/typescript-lru-cache": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript-lru-cache/-/typescript-lru-cache-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Jp57Qyy8wXeMkdNuZiglE6v2Cypg13eDA1chHwDG6kq51X7gk4K7P7HaDdzZKCxkegXkVHNcPD0n5aW6OZH3aA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/uint8array-tools": {
|
"node_modules/uint8array-tools": {
|
||||||
"version": "0.0.7",
|
"version": "0.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.7.tgz",
|
||||||
@ -19997,6 +20198,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/utf-8-validate": {
|
||||||
|
"version": "5.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz",
|
||||||
|
"integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"node-gyp-build": "^4.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.14.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/utf8-buffer": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/utf8-buffer/-/utf8-buffer-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ueuhzvWnp5JU5CiGSY4WdKbiN/PO2AZ/lpeLiz2l38qwdLy/cW40XobgyuIWucNyum0B33bVB0owjFCeGBSLqg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/util": {
|
"node_modules/util": {
|
||||||
"version": "0.12.4",
|
"version": "0.12.4",
|
||||||
"resolved": "https://registry.npmjs.org/util/-/util-0.12.4.tgz",
|
"resolved": "https://registry.npmjs.org/util/-/util-0.12.4.tgz",
|
||||||
@ -20320,6 +20543,47 @@
|
|||||||
"npm": ">=3.10.0"
|
"npm": ">=3.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/websocket": {
|
||||||
|
"version": "1.0.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.35.tgz",
|
||||||
|
"integrity": "sha512-/REy6amwPZl44DDzvRCkaI1q1bIiQB0mEFQLUrhz3z2EK91cp3n72rAjUlrTP0zV22HJIUOVHQGPxhFRjxjt+Q==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"debug": "^2.2.0",
|
||||||
|
"es5-ext": "^0.10.63",
|
||||||
|
"typedarray-to-buffer": "^3.1.5",
|
||||||
|
"utf-8-validate": "^5.0.2",
|
||||||
|
"yaeti": "^0.0.6"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/websocket-polyfill": {
|
||||||
|
"version": "0.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/websocket-polyfill/-/websocket-polyfill-0.0.3.tgz",
|
||||||
|
"integrity": "sha512-pF3kR8Uaoau78MpUmFfzbIRxXj9PeQrCuPepGE6JIsfsJ/o/iXr07Q2iQNzKSSblQJ0FiGWlS64N4pVSm+O3Dg==",
|
||||||
|
"dependencies": {
|
||||||
|
"tstl": "^2.0.7",
|
||||||
|
"websocket": "^1.0.28"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/websocket/node_modules/debug": {
|
||||||
|
"version": "2.6.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||||
|
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/websocket/node_modules/ms": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/whatwg-encoding": {
|
"node_modules/whatwg-encoding": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
|
||||||
@ -20896,6 +21160,15 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/yaeti": {
|
||||||
|
"version": "0.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz",
|
||||||
|
"integrity": "sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.32"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yallist": {
|
"node_modules/yallist": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
"@graphql-tools/schema": "^10.0.6",
|
"@graphql-tools/schema": "^10.0.6",
|
||||||
"@lightninglabs/lnc-web": "^0.3.2-alpha",
|
"@lightninglabs/lnc-web": "^0.3.2-alpha",
|
||||||
"@noble/curves": "^1.6.0",
|
"@noble/curves": "^1.6.0",
|
||||||
|
"@nostr-dev-kit/ndk": "^2.10.5",
|
||||||
"@opensearch-project/opensearch": "^2.12.0",
|
"@opensearch-project/opensearch": "^2.12.0",
|
||||||
"@prisma/client": "^5.20.0",
|
"@prisma/client": "^5.20.0",
|
||||||
"@slack/web-api": "^7.6.0",
|
"@slack/web-api": "^7.6.0",
|
||||||
|
@ -16,7 +16,6 @@ import { useToast } from '@/components/toast'
|
|||||||
import { useLightning } from '@/components/lightning'
|
import { useLightning } from '@/components/lightning'
|
||||||
import { ListUsers } from '@/components/user-list'
|
import { ListUsers } from '@/components/user-list'
|
||||||
import { Col, Row } from 'react-bootstrap'
|
import { Col, Row } from 'react-bootstrap'
|
||||||
import { proportions } from '@/lib/madness'
|
|
||||||
import { useData } from '@/components/use-data'
|
import { useData } from '@/components/use-data'
|
||||||
import { GrowthPieChartSkeleton } from '@/components/charts-skeletons'
|
import { GrowthPieChartSkeleton } from '@/components/charts-skeletons'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
@ -50,6 +49,7 @@ ${ITEM_FULL_FIELDS}
|
|||||||
photoId
|
photoId
|
||||||
ncomments
|
ncomments
|
||||||
nposts
|
nposts
|
||||||
|
proportion
|
||||||
|
|
||||||
optional {
|
optional {
|
||||||
streak
|
streak
|
||||||
@ -117,9 +117,10 @@ export default function Rewards ({ ssrData }) {
|
|||||||
|
|
||||||
if (!dat) return <PageLoading />
|
if (!dat) return <PageLoading />
|
||||||
|
|
||||||
function EstimatedReward ({ rank }) {
|
function EstimatedReward ({ rank, user }) {
|
||||||
const referrerReward = Math.floor(total * proportions[rank - 1] * 0.2)
|
if (!user) return null
|
||||||
const reward = Math.floor(total * proportions[rank - 1]) - referrerReward
|
const referrerReward = Math.max(Math.floor(total * user.proportion * 0.2), 0)
|
||||||
|
const reward = Math.max(Math.floor(total * user.proportion) - referrerReward, 0)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='text-muted fst-italic'>
|
<div className='text-muted fst-italic'>
|
||||||
|
@ -0,0 +1,14 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- A unique constraint covering the columns `[predecessorId]` on the table `Invoice` will be added. If there are existing duplicate values, this will fail.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Invoice" ADD COLUMN "predecessorId" INTEGER;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Invoice.predecessorId_unique" ON "Invoice"("predecessorId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Invoice" ADD CONSTRAINT "Invoice_predecessorId_fkey" FOREIGN KEY ("predecessorId") REFERENCES "Invoice"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
@ -0,0 +1,93 @@
|
|||||||
|
CREATE OR REPLACE FUNCTION user_values(
|
||||||
|
min TIMESTAMP(3), max TIMESTAMP(3), ival INTERVAL, date_part TEXT,
|
||||||
|
percentile_cutoff INTEGER DEFAULT 50,
|
||||||
|
each_upvote_portion FLOAT DEFAULT 4.0,
|
||||||
|
each_item_portion FLOAT DEFAULT 4.0,
|
||||||
|
handicap_ids INTEGER[] DEFAULT '{616, 6030, 4502, 27}',
|
||||||
|
handicap_zap_mult FLOAT DEFAULT 0.3)
|
||||||
|
RETURNS TABLE (
|
||||||
|
t TIMESTAMP(3), id INTEGER, proportion FLOAT
|
||||||
|
)
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
min_utc TIMESTAMP(3) := timezone('utc', min AT TIME ZONE 'America/Chicago');
|
||||||
|
BEGIN
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT period.t, u."userId", u.total_proportion
|
||||||
|
FROM generate_series(min, max, ival) period(t),
|
||||||
|
LATERAL
|
||||||
|
(WITH item_ratios AS (
|
||||||
|
SELECT *,
|
||||||
|
CASE WHEN "parentId" IS NULL THEN 'POST' ELSE 'COMMENT' END as type,
|
||||||
|
CASE WHEN "weightedVotes" > 0 THEN "weightedVotes"/(sum("weightedVotes") OVER (PARTITION BY "parentId" IS NULL)) ELSE 0 END AS ratio
|
||||||
|
FROM (
|
||||||
|
SELECT *,
|
||||||
|
NTILE(100) OVER (PARTITION BY "parentId" IS NULL ORDER BY ("weightedVotes"-"weightedDownVotes") desc) AS percentile,
|
||||||
|
ROW_NUMBER() OVER (PARTITION BY "parentId" IS NULL ORDER BY ("weightedVotes"-"weightedDownVotes") desc) AS rank
|
||||||
|
FROM
|
||||||
|
"Item"
|
||||||
|
WHERE date_trunc(date_part, created_at AT TIME ZONE 'UTC' AT TIME ZONE 'America/Chicago') = period.t
|
||||||
|
AND "weightedVotes" > 0
|
||||||
|
AND "deletedAt" IS NULL
|
||||||
|
AND NOT bio
|
||||||
|
AND ("invoiceActionState" IS NULL OR "invoiceActionState" = 'PAID')
|
||||||
|
) x
|
||||||
|
WHERE x.percentile <= percentile_cutoff
|
||||||
|
),
|
||||||
|
-- get top upvoters of top posts and comments
|
||||||
|
upvoter_islands AS (
|
||||||
|
SELECT "ItemAct"."userId", item_ratios.id, item_ratios.ratio, item_ratios."parentId",
|
||||||
|
"ItemAct".msats as tipped, "ItemAct".created_at as acted_at,
|
||||||
|
ROW_NUMBER() OVER (partition by item_ratios.id order by "ItemAct".created_at asc)
|
||||||
|
- ROW_NUMBER() OVER (partition by item_ratios.id, "ItemAct"."userId" order by "ItemAct".created_at asc) AS island
|
||||||
|
FROM item_ratios
|
||||||
|
JOIN "ItemAct" on "ItemAct"."itemId" = item_ratios.id
|
||||||
|
WHERE act = 'TIP'
|
||||||
|
AND date_trunc(date_part, "ItemAct".created_at AT TIME ZONE 'UTC' AT TIME ZONE 'America/Chicago') = period.t
|
||||||
|
AND ("ItemAct"."invoiceActionState" IS NULL OR "ItemAct"."invoiceActionState" = 'PAID')
|
||||||
|
),
|
||||||
|
-- isolate contiguous upzaps from the same user on the same item so that when we take the log
|
||||||
|
-- of the upzaps it accounts for successive zaps and does not disproportionately reward them
|
||||||
|
-- quad root of the total tipped
|
||||||
|
upvoters AS (
|
||||||
|
SELECT "userId", upvoter_islands.id, ratio, "parentId", GREATEST(power(sum(tipped) / 1000, 0.25), 0) as tipped, min(acted_at) as acted_at
|
||||||
|
FROM upvoter_islands
|
||||||
|
GROUP BY "userId", upvoter_islands.id, ratio, "parentId", island
|
||||||
|
),
|
||||||
|
-- the relative contribution of each upvoter to the post/comment
|
||||||
|
-- early component: 1/ln(early_rank + e - 1)
|
||||||
|
-- tipped component: how much they tipped relative to the total tipped for the item
|
||||||
|
-- multiplied by the relative rank of the item to the total items
|
||||||
|
-- multiplied by the trust of the user
|
||||||
|
upvoter_ratios AS (
|
||||||
|
SELECT "userId", sum((early_multiplier+tipped_ratio)*ratio*CASE WHEN users.id = ANY (handicap_ids) THEN handicap_zap_mult ELSE users.trust+0.1 END) as upvoter_ratio,
|
||||||
|
"parentId" IS NULL as "isPost", CASE WHEN "parentId" IS NULL THEN 'TIP_POST' ELSE 'TIP_COMMENT' END as type
|
||||||
|
FROM (
|
||||||
|
SELECT *,
|
||||||
|
1.0/LN(ROW_NUMBER() OVER (partition by upvoters.id order by acted_at asc) + EXP(1.0) - 1) AS early_multiplier,
|
||||||
|
tipped::float/(sum(tipped) OVER (partition by upvoters.id)) tipped_ratio
|
||||||
|
FROM upvoters
|
||||||
|
WHERE tipped > 2.1
|
||||||
|
) u
|
||||||
|
JOIN users on "userId" = users.id
|
||||||
|
GROUP BY "userId", "parentId" IS NULL
|
||||||
|
),
|
||||||
|
proportions AS (
|
||||||
|
SELECT "userId", NULL as id, type, ROW_NUMBER() OVER (PARTITION BY "isPost" ORDER BY upvoter_ratio DESC) as rank,
|
||||||
|
upvoter_ratio/(sum(upvoter_ratio) OVER (PARTITION BY "isPost"))/each_upvote_portion as proportion
|
||||||
|
FROM upvoter_ratios
|
||||||
|
WHERE upvoter_ratio > 0
|
||||||
|
UNION ALL
|
||||||
|
SELECT "userId", item_ratios.id, type, rank, ratio/each_item_portion as proportion
|
||||||
|
FROM item_ratios
|
||||||
|
)
|
||||||
|
SELECT "userId", sum(proportions.proportion) AS total_proportion
|
||||||
|
FROM proportions
|
||||||
|
GROUP BY "userId"
|
||||||
|
HAVING sum(proportions.proportion) > 0.000001) u;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
REFRESH MATERIALIZED VIEW CONCURRENTLY user_values_today;
|
||||||
|
REFRESH MATERIALIZED VIEW CONCURRENTLY user_values_days;
|
@ -904,39 +904,41 @@ model ItemMention {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Invoice {
|
model Invoice {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||||
userId Int
|
userId Int
|
||||||
hash String @unique(map: "Invoice.hash_unique")
|
hash String @unique(map: "Invoice.hash_unique")
|
||||||
preimage String? @unique(map: "Invoice.preimage_unique")
|
preimage String? @unique(map: "Invoice.preimage_unique")
|
||||||
isHeld Boolean?
|
isHeld Boolean?
|
||||||
bolt11 String
|
bolt11 String
|
||||||
expiresAt DateTime
|
expiresAt DateTime
|
||||||
confirmedAt DateTime?
|
confirmedAt DateTime?
|
||||||
confirmedIndex BigInt?
|
confirmedIndex BigInt?
|
||||||
cancelled Boolean @default(false)
|
cancelled Boolean @default(false)
|
||||||
cancelledAt DateTime?
|
cancelledAt DateTime?
|
||||||
msatsRequested BigInt
|
msatsRequested BigInt
|
||||||
msatsReceived BigInt?
|
msatsReceived BigInt?
|
||||||
desc String?
|
desc String?
|
||||||
comment String?
|
comment String?
|
||||||
lud18Data Json?
|
lud18Data Json?
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
invoiceForward InvoiceForward?
|
invoiceForward InvoiceForward?
|
||||||
|
predecessorId Int? @unique(map: "Invoice.predecessorId_unique")
|
||||||
actionState InvoiceActionState?
|
predecessorInvoice Invoice? @relation("PredecessorInvoice", fields: [predecessorId], references: [id], onDelete: Cascade)
|
||||||
actionType InvoiceActionType?
|
successorInvoice Invoice? @relation("PredecessorInvoice")
|
||||||
actionOptimistic Boolean?
|
actionState InvoiceActionState?
|
||||||
actionId Int?
|
actionType InvoiceActionType?
|
||||||
actionArgs Json? @db.JsonB
|
actionOptimistic Boolean?
|
||||||
actionError String?
|
actionId Int?
|
||||||
actionResult Json? @db.JsonB
|
actionArgs Json? @db.JsonB
|
||||||
ItemAct ItemAct[]
|
actionError String?
|
||||||
Item Item[]
|
actionResult Json? @db.JsonB
|
||||||
Upload Upload[]
|
ItemAct ItemAct[]
|
||||||
PollVote PollVote[]
|
Item Item[]
|
||||||
PollBlindVote PollBlindVote[]
|
Upload Upload[]
|
||||||
|
PollVote PollVote[]
|
||||||
|
PollBlindVote PollBlindVote[]
|
||||||
|
|
||||||
@@index([createdAt], map: "Invoice.created_at_index")
|
@@index([createdAt], map: "Invoice.created_at_index")
|
||||||
@@index([userId], map: "Invoice.userId_index")
|
@@index([userId], map: "Invoice.userId_index")
|
||||||
|
20
scripts/test-routing.sh
Normal file
20
scripts/test-routing.sh
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# test if every node can pay invoices from every other node
|
||||||
|
|
||||||
|
SN_LND_PUBKEY=02cb2e2d5a6c5b17fa67b1a883e2973c82e328fb9bd08b2b156a9e23820c87a490
|
||||||
|
LND_PUBKEY=028093ae52e011d45b3e67f2e0f2cb6c3a1d7f88d2920d408f3ac6db3a56dc4b35
|
||||||
|
CLN_PUBKEY=03ca7acec181dbf5e427c682c4261a46a0dd9ea5f35d97acb094e399f727835b90
|
||||||
|
|
||||||
|
# -e: exit on first failure | -x: print commands
|
||||||
|
set -ex
|
||||||
|
|
||||||
|
sndev cli lnd queryroutes $SN_LND_PUBKEY 1000
|
||||||
|
sndev cli lnd queryroutes $CLN_PUBKEY 1000
|
||||||
|
|
||||||
|
sndev cli sn_lnd queryroutes $LND_PUBKEY 1000
|
||||||
|
sndev cli sn_lnd queryroutes $CLN_PUBKEY 1000
|
||||||
|
|
||||||
|
# https://docs.corelightning.org/reference/lightning-getroute
|
||||||
|
sndev cli cln getroute $LND_PUBKEY 1000 0
|
||||||
|
sndev cli cln getroute $SN_LND_PUBKEY 1000 0
|
@ -1,9 +1,10 @@
|
|||||||
import { getScopes, SCOPE_READ, SCOPE_WRITE, getWallet, request } from '@/wallets/blink/common'
|
import { getScopes, SCOPE_READ, SCOPE_WRITE, getWallet, request } from '@/wallets/blink/common'
|
||||||
export * from '@/wallets/blink'
|
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')
|
logger.info('trying to fetch ' + currency + ' wallet')
|
||||||
const scopes = await getScopes(apiKey)
|
|
||||||
|
const scopes = await getScopes({ apiKey }, { signal })
|
||||||
if (!scopes.includes(SCOPE_READ)) {
|
if (!scopes.includes(SCOPE_READ)) {
|
||||||
throw new Error('missing READ scope')
|
throw new Error('missing READ scope')
|
||||||
}
|
}
|
||||||
@ -12,46 +13,48 @@ export async function testSendPayment ({ apiKey, currency }, { logger }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
currency = currency ? currency.toUpperCase() : 'BTC'
|
currency = currency ? currency.toUpperCase() : 'BTC'
|
||||||
await getWallet(apiKey, currency)
|
await getWallet({ apiKey, currency }, { signal })
|
||||||
|
|
||||||
logger.ok(currency + ' wallet found')
|
logger.ok(currency + ' wallet found')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sendPayment (bolt11, { apiKey, currency }) {
|
export async function sendPayment (bolt11, { apiKey, currency }, { signal }) {
|
||||||
const wallet = await getWallet(apiKey, currency)
|
const wallet = await getWallet({ apiKey, currency }, { signal })
|
||||||
return await payInvoice(apiKey, wallet, bolt11)
|
return await payInvoice(bolt11, { apiKey, wallet }, { signal })
|
||||||
}
|
}
|
||||||
|
|
||||||
async function payInvoice (authToken, wallet, invoice) {
|
async function payInvoice (bolt11, { apiKey, wallet }, { signal }) {
|
||||||
const walletId = wallet.id
|
const out = await request({
|
||||||
const out = await request(authToken, `
|
apiKey,
|
||||||
mutation LnInvoicePaymentSend($input: LnInvoicePaymentInput!) {
|
query: `
|
||||||
|
mutation LnInvoicePaymentSend($input: LnInvoicePaymentInput!) {
|
||||||
lnInvoicePaymentSend(input: $input) {
|
lnInvoicePaymentSend(input: $input) {
|
||||||
status
|
status
|
||||||
errors {
|
errors {
|
||||||
message
|
message
|
||||||
path
|
path
|
||||||
code
|
code
|
||||||
}
|
}
|
||||||
transaction {
|
transaction {
|
||||||
settlementVia {
|
settlementVia {
|
||||||
... on SettlementViaIntraLedger {
|
... on SettlementViaIntraLedger {
|
||||||
preImage
|
preImage
|
||||||
}
|
}
|
||||||
... on SettlementViaLn {
|
... on SettlementViaLn {
|
||||||
preImage
|
preImage
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}`,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
paymentRequest: bolt11,
|
||||||
|
walletId: wallet.id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`,
|
}, { signal })
|
||||||
{
|
|
||||||
input: {
|
|
||||||
paymentRequest: invoice,
|
|
||||||
walletId
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const status = out.data.lnInvoicePaymentSend.status
|
const status = out.data.lnInvoicePaymentSend.status
|
||||||
const errors = out.data.lnInvoicePaymentSend.errors
|
const errors = out.data.lnInvoicePaymentSend.errors
|
||||||
if (errors && errors.length > 0) {
|
if (errors && errors.length > 0) {
|
||||||
@ -76,7 +79,7 @@ async function payInvoice (authToken, wallet, invoice) {
|
|||||||
// at some point it should either be settled or fail on the backend, so the loop will exit
|
// 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))
|
await new Promise(resolve => setTimeout(resolve, 100))
|
||||||
|
|
||||||
const txInfo = await getTxInfo(authToken, wallet, invoice)
|
const txInfo = await getTxInfo(bolt11, { apiKey, wallet }, { signal })
|
||||||
// settled
|
// settled
|
||||||
if (txInfo.status === 'SUCCESS') {
|
if (txInfo.status === 'SUCCESS') {
|
||||||
if (!txInfo.preImage) throw new Error('no preimage')
|
if (!txInfo.preImage) throw new Error('no preimage')
|
||||||
@ -95,36 +98,37 @@ async function payInvoice (authToken, wallet, invoice) {
|
|||||||
throw new Error('unexpected error')
|
throw new Error('unexpected error')
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getTxInfo (authToken, wallet, invoice) {
|
async function getTxInfo (bolt11, { apiKey, wallet }, { signal }) {
|
||||||
const walletId = wallet.id
|
|
||||||
let out
|
let out
|
||||||
try {
|
try {
|
||||||
out = await request(authToken, `
|
out = await request({
|
||||||
query GetTxInfo($walletId: WalletId!, $paymentRequest: LnPaymentRequest!) {
|
apiKey,
|
||||||
me {
|
query: `
|
||||||
defaultAccount {
|
query GetTxInfo($walletId: WalletId!, $paymentRequest: LnPaymentRequest!) {
|
||||||
walletById(walletId: $walletId) {
|
me {
|
||||||
transactionsByPaymentRequest(paymentRequest: $paymentRequest) {
|
defaultAccount {
|
||||||
status
|
walletById(walletId: $walletId) {
|
||||||
direction
|
transactionsByPaymentRequest(paymentRequest: $paymentRequest) {
|
||||||
settlementVia {
|
status
|
||||||
|
direction
|
||||||
|
settlementVia {
|
||||||
... on SettlementViaIntraLedger {
|
... on SettlementViaIntraLedger {
|
||||||
preImage
|
preImage
|
||||||
}
|
}
|
||||||
... on SettlementViaLn {
|
... on SettlementViaLn {
|
||||||
preImage
|
preImage
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}`,
|
||||||
|
variables: {
|
||||||
|
paymentRequest: bolt11,
|
||||||
|
walletId: wallet.Id
|
||||||
}
|
}
|
||||||
`,
|
}, { signal })
|
||||||
{
|
|
||||||
paymentRequest: invoice,
|
|
||||||
walletId
|
|
||||||
})
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// something went wrong during the query,
|
// something went wrong during the query,
|
||||||
// maybe the connection was lost, so we just return
|
// maybe the connection was lost, so we just return
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { fetchWithTimeout } from '@/lib/fetch'
|
||||||
import { assertContentTypeJson, assertResponseOk } from '@/lib/url'
|
import { assertContentTypeJson, assertResponseOk } from '@/lib/url'
|
||||||
|
|
||||||
export const galoyBlinkUrl = 'https://api.blink.sv/graphql'
|
export const galoyBlinkUrl = 'https://api.blink.sv/graphql'
|
||||||
@ -7,38 +8,42 @@ export const SCOPE_READ = 'READ'
|
|||||||
export const SCOPE_WRITE = 'WRITE'
|
export const SCOPE_WRITE = 'WRITE'
|
||||||
export const SCOPE_RECEIVE = 'RECEIVE'
|
export const SCOPE_RECEIVE = 'RECEIVE'
|
||||||
|
|
||||||
export async function getWallet (authToken, currency) {
|
export async function getWallet ({ apiKey, currency }, { signal }) {
|
||||||
const out = await request(authToken, `
|
const out = await request({
|
||||||
|
apiKey,
|
||||||
|
query: `
|
||||||
query me {
|
query me {
|
||||||
me {
|
me {
|
||||||
defaultAccount {
|
defaultAccount {
|
||||||
wallets {
|
wallets {
|
||||||
id
|
id
|
||||||
walletCurrency
|
walletCurrency
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`, {})
|
}`
|
||||||
|
}, { signal })
|
||||||
|
|
||||||
const wallets = out.data.me.defaultAccount.wallets
|
const wallets = out.data.me.defaultAccount.wallets
|
||||||
for (const wallet of wallets) {
|
for (const wallet of wallets) {
|
||||||
if (wallet.walletCurrency === currency) {
|
if (wallet.walletCurrency === currency) {
|
||||||
return wallet
|
return wallet
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`wallet ${currency} not found`)
|
throw new Error(`wallet ${currency} not found`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function request (authToken, query, variables = {}) {
|
export async function request ({ apiKey, query, variables = {} }, { signal }) {
|
||||||
const options = {
|
const res = await fetchWithTimeout(galoyBlinkUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'X-API-KEY': authToken
|
'X-API-KEY': apiKey
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ query, variables })
|
body: JSON.stringify({ query, variables }),
|
||||||
}
|
signal
|
||||||
const res = await fetch(galoyBlinkUrl, options)
|
})
|
||||||
|
|
||||||
assertResponseOk(res)
|
assertResponseOk(res)
|
||||||
assertContentTypeJson(res)
|
assertContentTypeJson(res)
|
||||||
@ -46,14 +51,16 @@ export async function request (authToken, query, variables = {}) {
|
|||||||
return res.json()
|
return res.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getScopes (authToken) {
|
export async function getScopes ({ apiKey }, { signal }) {
|
||||||
const out = await request(authToken, `
|
const out = await request({
|
||||||
query scopes {
|
apiKey,
|
||||||
|
query: `
|
||||||
|
query scopes {
|
||||||
authorization {
|
authorization {
|
||||||
scopes
|
scopes
|
||||||
}
|
}
|
||||||
}
|
}`
|
||||||
`, {})
|
}, { signal })
|
||||||
const scopes = out?.data?.authorization?.scopes
|
const scopes = out?.data?.authorization?.scopes
|
||||||
return 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 { getScopes, SCOPE_READ, SCOPE_RECEIVE, SCOPE_WRITE, getWallet, request } from '@/wallets/blink/common'
|
||||||
import { msatsToSats } from '@/lib/format'
|
import { msatsToSats } from '@/lib/format'
|
||||||
export * from '@/wallets/blink'
|
export * from '@/wallets/blink'
|
||||||
|
|
||||||
export async function testCreateInvoice ({ apiKeyRecv, currencyRecv }) {
|
export async function testCreateInvoice ({ apiKeyRecv, currencyRecv }, { signal }) {
|
||||||
const scopes = await getScopes(apiKeyRecv)
|
const scopes = await getScopes({ apiKey: apiKeyRecv }, { signal })
|
||||||
if (!scopes.includes(SCOPE_READ)) {
|
if (!scopes.includes(SCOPE_READ)) {
|
||||||
throw new Error('missing READ scope')
|
throw new Error('missing READ scope')
|
||||||
}
|
}
|
||||||
@ -15,47 +14,50 @@ export async function testCreateInvoice ({ apiKeyRecv, currencyRecv }) {
|
|||||||
throw new Error('missing RECEIVE scope')
|
throw new Error('missing RECEIVE scope')
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeout = 15_000
|
|
||||||
currencyRecv = currencyRecv ? currencyRecv.toUpperCase() : 'BTC'
|
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 (
|
export async function createInvoice (
|
||||||
{ msats, description, expiry },
|
{ msats, description, expiry },
|
||||||
{ apiKeyRecv, currencyRecv }) {
|
{ apiKeyRecv: apiKey, currencyRecv: currency },
|
||||||
currencyRecv = currencyRecv ? currencyRecv.toUpperCase() : 'BTC'
|
{ signal }) {
|
||||||
|
currency = currency ? currency.toUpperCase() : 'BTC'
|
||||||
|
|
||||||
const wallet = await getWallet(apiKeyRecv, currencyRecv)
|
const wallet = await getWallet({ apiKey, currency }, { signal })
|
||||||
|
|
||||||
if (currencyRecv !== 'BTC') {
|
if (currency !== 'BTC') {
|
||||||
throw new Error('unsupported currency ' + currencyRecv)
|
throw new Error('unsupported currency ' + currency)
|
||||||
}
|
}
|
||||||
const mutation = `
|
|
||||||
mutation LnInvoiceCreate($input: LnInvoiceCreateInput!) {
|
|
||||||
lnInvoiceCreate(input: $input) {
|
|
||||||
invoice {
|
|
||||||
paymentRequest
|
|
||||||
}
|
|
||||||
errors {
|
|
||||||
message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const out = await request(apiKeyRecv, mutation, {
|
const out = await request({
|
||||||
input: {
|
apiKey,
|
||||||
amount: msatsToSats(msats),
|
query: `
|
||||||
expiresIn: Math.floor(expiry / 60) || 1,
|
mutation LnInvoiceCreate($input: LnInvoiceCreateInput!) {
|
||||||
memo: description,
|
lnInvoiceCreate(input: $input) {
|
||||||
walletId: wallet.id
|
invoice {
|
||||||
|
paymentRequest
|
||||||
|
}
|
||||||
|
errors {
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
amount: msatsToSats(msats),
|
||||||
|
expiresIn: Math.floor(expiry / 60) || 1,
|
||||||
|
memo: description,
|
||||||
|
walletId: wallet.id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
}, { signal })
|
||||||
|
|
||||||
const res = out.data.lnInvoiceCreate
|
const res = out.data.lnInvoiceCreate
|
||||||
const errors = res.errors
|
const errors = res.errors
|
||||||
if (errors && errors.length > 0) {
|
if (errors && errors.length > 0) {
|
||||||
throw new Error(errors.map(e => e.code + ' ' + e.message).join(', '))
|
throw new Error(errors.map(e => e.code + ' ' + e.message).join(', '))
|
||||||
}
|
}
|
||||||
const invoice = res.invoice.paymentRequest
|
|
||||||
return invoice
|
return res.invoice.paymentRequest
|
||||||
}
|
}
|
||||||
|
@ -2,23 +2,26 @@ import { createInvoice as clnCreateInvoice } from '@/lib/cln'
|
|||||||
|
|
||||||
export * from '@/wallets/cln'
|
export * from '@/wallets/cln'
|
||||||
|
|
||||||
export const testCreateInvoice = async ({ socket, rune, cert }) => {
|
export const testCreateInvoice = async ({ socket, rune, cert }, { signal }) => {
|
||||||
return await createInvoice({ msats: 1000, expiry: 1, description: '' }, { socket, rune, cert })
|
return await createInvoice({ msats: 1000, expiry: 1, description: '' }, { socket, rune, cert }, { signal })
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createInvoice = async (
|
export const createInvoice = async (
|
||||||
{ msats, description, descriptionHash, expiry },
|
{ msats, description, expiry },
|
||||||
{ socket, rune, cert }
|
{ socket, rune, cert },
|
||||||
) => {
|
{ signal }) => {
|
||||||
const inv = await clnCreateInvoice({
|
const inv = await clnCreateInvoice(
|
||||||
socket,
|
{
|
||||||
rune,
|
msats,
|
||||||
cert,
|
description,
|
||||||
description,
|
expiry
|
||||||
descriptionHash,
|
},
|
||||||
msats,
|
{
|
||||||
expiry
|
socket,
|
||||||
})
|
rune,
|
||||||
|
cert
|
||||||
|
},
|
||||||
|
{ signal })
|
||||||
|
|
||||||
return inv.bolt11
|
return inv.bolt11
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,8 @@ import { REMOVE_WALLET } from '@/fragments/wallet'
|
|||||||
import { useWalletLogger } from '@/wallets/logger'
|
import { useWalletLogger } from '@/wallets/logger'
|
||||||
import { useWallets } from '.'
|
import { useWallets } from '.'
|
||||||
import validateWallet from './validate'
|
import validateWallet from './validate'
|
||||||
|
import { WALLET_SEND_PAYMENT_TIMEOUT_MS } from '@/lib/constants'
|
||||||
|
import { timeoutSignal, withTimeout } from '@/lib/time'
|
||||||
|
|
||||||
export function useWalletConfigurator (wallet) {
|
export function useWalletConfigurator (wallet) {
|
||||||
const { me } = useMe()
|
const { me } = useMe()
|
||||||
@ -37,17 +39,28 @@ export function useWalletConfigurator (wallet) {
|
|||||||
let serverConfig = serverWithShared
|
let serverConfig = serverWithShared
|
||||||
|
|
||||||
if (canSend({ def: wallet.def, config: clientConfig })) {
|
if (canSend({ def: wallet.def, config: clientConfig })) {
|
||||||
let transformedConfig = await validateWallet(wallet.def, clientWithShared, { skipGenerated: true })
|
try {
|
||||||
if (transformedConfig) {
|
let transformedConfig = await validateWallet(wallet.def, clientWithShared, { skipGenerated: true })
|
||||||
clientConfig = Object.assign(clientConfig, transformedConfig)
|
|
||||||
}
|
|
||||||
if (wallet.def.testSendPayment && validateLightning) {
|
|
||||||
transformedConfig = await wallet.def.testSendPayment(clientConfig, { me, logger })
|
|
||||||
if (transformedConfig) {
|
if (transformedConfig) {
|
||||||
clientConfig = Object.assign(clientConfig, transformedConfig)
|
clientConfig = Object.assign(clientConfig, transformedConfig)
|
||||||
}
|
}
|
||||||
// validate again to ensure generated fields are valid
|
if (wallet.def.testSendPayment && validateLightning) {
|
||||||
await validateWallet(wallet.def, clientConfig)
|
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)
|
||||||
|
}
|
||||||
|
// validate again to ensure generated fields are valid
|
||||||
|
await validateWallet(wallet.def, clientConfig)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err.message)
|
||||||
|
throw err
|
||||||
}
|
}
|
||||||
} else if (canReceive({ def: wallet.def, config: serverConfig })) {
|
} else if (canReceive({ def: wallet.def, config: serverConfig })) {
|
||||||
const transformedConfig = await validateWallet(wallet.def, serverConfig)
|
const transformedConfig = await validateWallet(wallet.def, serverConfig)
|
||||||
@ -71,33 +84,52 @@ export function useWalletConfigurator (wallet) {
|
|||||||
}, [me?.id, wallet.def.name, reloadLocalWallets])
|
}, [me?.id, wallet.def.name, reloadLocalWallets])
|
||||||
|
|
||||||
const save = useCallback(async (newConfig, validateLightning = true) => {
|
const save = useCallback(async (newConfig, validateLightning = true) => {
|
||||||
const { clientConfig, serverConfig } = await _validate(newConfig, validateLightning)
|
const { clientWithShared: oldClientConfig } = siftConfig(wallet.def.fields, wallet.config)
|
||||||
|
const { clientConfig: newClientConfig, serverConfig: newServerConfig } = await _validate(newConfig, validateLightning)
|
||||||
|
|
||||||
|
const oldCanSend = canSend({ def: wallet.def, config: oldClientConfig })
|
||||||
|
const newCanSend = canSend({ def: wallet.def, config: newClientConfig })
|
||||||
|
|
||||||
// if vault is active, encrypt and send to server regardless of wallet type
|
// if vault is active, encrypt and send to server regardless of wallet type
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
await _saveToServer(serverConfig, clientConfig, validateLightning)
|
await _saveToServer(newServerConfig, newClientConfig, validateLightning)
|
||||||
await _detachFromLocal()
|
await _detachFromLocal()
|
||||||
} else {
|
} else {
|
||||||
if (canSend({ def: wallet.def, config: clientConfig })) {
|
if (newCanSend) {
|
||||||
await _saveToLocal(clientConfig)
|
await _saveToLocal(newClientConfig)
|
||||||
} else {
|
} else {
|
||||||
// if it previously had a client config, remove it
|
// if it previously had a client config, remove it
|
||||||
await _detachFromLocal()
|
await _detachFromLocal()
|
||||||
}
|
}
|
||||||
if (canReceive({ def: wallet.def, config: serverConfig })) {
|
if (canReceive({ def: wallet.def, config: newServerConfig })) {
|
||||||
await _saveToServer(serverConfig, clientConfig, validateLightning)
|
await _saveToServer(newServerConfig, newClientConfig, validateLightning)
|
||||||
} else if (wallet.config.id) {
|
} else if (wallet.config.id) {
|
||||||
// we previously had a server config
|
// we previously had a server config
|
||||||
if (wallet.vaultEntries.length > 0) {
|
if (wallet.vaultEntries.length > 0) {
|
||||||
// we previously had a server config with vault entries, save it
|
// we previously had a server config with vault entries, save it
|
||||||
await _saveToServer(serverConfig, clientConfig, validateLightning)
|
await _saveToServer(newServerConfig, newClientConfig, validateLightning)
|
||||||
} else {
|
} else {
|
||||||
// we previously had a server config without vault entries, remove it
|
// we previously had a server config without vault entries, remove it
|
||||||
await _detachFromServer()
|
await _detachFromServer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [isActive, wallet.def, _saveToServer, _saveToLocal, _validate,
|
|
||||||
|
if (newCanSend) {
|
||||||
|
if (oldCanSend) {
|
||||||
|
logger.ok('details for sending updated')
|
||||||
|
} else {
|
||||||
|
logger.ok('details for sending saved')
|
||||||
|
}
|
||||||
|
if (newConfig.enabled) {
|
||||||
|
logger.ok('sending enabled')
|
||||||
|
} else {
|
||||||
|
logger.info('sending disabled')
|
||||||
|
}
|
||||||
|
} else if (oldCanSend) {
|
||||||
|
logger.info('details for sending deleted')
|
||||||
|
}
|
||||||
|
}, [isActive, wallet.def, wallet.config, _saveToServer, _saveToLocal, _validate,
|
||||||
_detachFromLocal, _detachFromServer])
|
_detachFromLocal, _detachFromServer])
|
||||||
|
|
||||||
const detach = useCallback(async () => {
|
const detach = useCallback(async () => {
|
||||||
@ -112,7 +144,9 @@ export function useWalletConfigurator (wallet) {
|
|||||||
// if vault is not active and has a client config, delete from local storage
|
// if vault is not active and has a client config, delete from local storage
|
||||||
await _detachFromLocal()
|
await _detachFromLocal()
|
||||||
}
|
}
|
||||||
}, [isActive, _detachFromServer, _detachFromLocal])
|
|
||||||
|
logger.info('details for sending deleted')
|
||||||
|
}, [logger, isActive, _detachFromServer, _detachFromLocal])
|
||||||
|
|
||||||
return { save, detach }
|
return { save, detach }
|
||||||
}
|
}
|
||||||
|
@ -47,6 +47,14 @@ export class WalletSenderError extends WalletPaymentError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class WalletReceiverError extends WalletPaymentError {
|
||||||
|
constructor (invoice) {
|
||||||
|
super(`payment forwarding failed for invoice ${invoice.hash}`)
|
||||||
|
this.name = 'WalletReceiverError'
|
||||||
|
this.invoice = invoice
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class WalletsNotAvailableError extends WalletConfigurationError {
|
export class WalletsNotAvailableError extends WalletConfigurationError {
|
||||||
constructor () {
|
constructor () {
|
||||||
super('no wallet available')
|
super('no wallet available')
|
||||||
|
@ -220,7 +220,7 @@ export function useWallet (name) {
|
|||||||
|
|
||||||
export function useSendWallets () {
|
export function useSendWallets () {
|
||||||
const { wallets } = useWallets()
|
const { wallets } = useWallets()
|
||||||
// return the first enabled wallet that is available and can send
|
// return all enabled wallets that are available and can send
|
||||||
return wallets
|
return wallets
|
||||||
.filter(w => !w.def.isAvailable || w.def.isAvailable())
|
.filter(w => !w.def.isAvailable || w.def.isAvailable())
|
||||||
.filter(w => w.config?.enabled && canSend(w))
|
.filter(w => w.config?.enabled && canSend(w))
|
||||||
|
@ -1,18 +1,20 @@
|
|||||||
|
import { fetchWithTimeout } from '@/lib/fetch'
|
||||||
import { msatsSatsFloor } from '@/lib/format'
|
import { msatsSatsFloor } from '@/lib/format'
|
||||||
import { lnAddrOptions } from '@/lib/lnurl'
|
import { lnAddrOptions } from '@/lib/lnurl'
|
||||||
import { assertContentTypeJson, assertResponseOk } from '@/lib/url'
|
import { assertContentTypeJson, assertResponseOk } from '@/lib/url'
|
||||||
|
|
||||||
export * from '@/wallets/lightning-address'
|
export * from '@/wallets/lightning-address'
|
||||||
|
|
||||||
export const testCreateInvoice = async ({ address }) => {
|
export const testCreateInvoice = async ({ address }, { signal }) => {
|
||||||
return await createInvoice({ msats: 1000 }, { address })
|
return await createInvoice({ msats: 1000 }, { address }, { signal })
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createInvoice = async (
|
export const createInvoice = async (
|
||||||
{ msats, description },
|
{ msats, description },
|
||||||
{ address }
|
{ address },
|
||||||
|
{ signal }
|
||||||
) => {
|
) => {
|
||||||
const { callback, commentAllowed } = await lnAddrOptions(address)
|
const { callback, commentAllowed } = await lnAddrOptions(address, { signal })
|
||||||
const callbackUrl = new URL(callback)
|
const callbackUrl = new URL(callback)
|
||||||
|
|
||||||
// most lnurl providers suck nards so we have to floor to nearest sat
|
// 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
|
// call callback with amount and conditionally comment
|
||||||
const res = await fetch(callbackUrl.toString())
|
const res = await fetchWithTimeout(callbackUrl.toString(), { signal })
|
||||||
|
|
||||||
assertResponseOk(res)
|
assertResponseOk(res)
|
||||||
assertContentTypeJson(res)
|
assertContentTypeJson(res)
|
||||||
|
@ -1,22 +1,23 @@
|
|||||||
|
import { fetchWithTimeout } from '@/lib/fetch'
|
||||||
import { assertContentTypeJson } from '@/lib/url'
|
import { assertContentTypeJson } from '@/lib/url'
|
||||||
|
|
||||||
export * from '@/wallets/lnbits'
|
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')
|
logger.info('trying to fetch wallet')
|
||||||
|
|
||||||
url = url.replace(/\/+$/, '')
|
url = url.replace(/\/+$/, '')
|
||||||
await getWallet({ url, adminKey, invoiceKey })
|
await getWallet({ url, adminKey, invoiceKey }, { signal })
|
||||||
|
|
||||||
logger.ok('wallet found')
|
logger.ok('wallet found')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sendPayment (bolt11, { url, adminKey }) {
|
export async function sendPayment (bolt11, { url, adminKey }, { signal }) {
|
||||||
url = url.replace(/\/+$/, '')
|
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) {
|
if (!checkResponse.preimage) {
|
||||||
throw new Error('No preimage')
|
throw new Error('No preimage')
|
||||||
}
|
}
|
||||||
@ -24,7 +25,7 @@ export async function sendPayment (bolt11, { url, adminKey }) {
|
|||||||
return checkResponse.preimage
|
return checkResponse.preimage
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getWallet ({ url, adminKey, invoiceKey }) {
|
async function getWallet ({ url, adminKey, invoiceKey }, { signal }) {
|
||||||
const path = '/api/v1/wallet'
|
const path = '/api/v1/wallet'
|
||||||
|
|
||||||
const headers = new Headers()
|
const headers = new Headers()
|
||||||
@ -32,7 +33,7 @@ async function getWallet ({ url, adminKey, invoiceKey }) {
|
|||||||
headers.append('Content-Type', 'application/json')
|
headers.append('Content-Type', 'application/json')
|
||||||
headers.append('X-Api-Key', adminKey || invoiceKey)
|
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)
|
assertContentTypeJson(res)
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
@ -44,7 +45,7 @@ async function getWallet ({ url, adminKey, invoiceKey }) {
|
|||||||
return wallet
|
return wallet
|
||||||
}
|
}
|
||||||
|
|
||||||
async function postPayment (bolt11, { url, adminKey }) {
|
async function postPayment (bolt11, { url, adminKey }, { signal }) {
|
||||||
const path = '/api/v1/payments'
|
const path = '/api/v1/payments'
|
||||||
|
|
||||||
const headers = new Headers()
|
const headers = new Headers()
|
||||||
@ -54,7 +55,7 @@ async function postPayment (bolt11, { url, adminKey }) {
|
|||||||
|
|
||||||
const body = JSON.stringify({ bolt11, out: true })
|
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)
|
assertContentTypeJson(res)
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
@ -66,7 +67,7 @@ async function postPayment (bolt11, { url, adminKey }) {
|
|||||||
return payment
|
return payment
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getPayment (paymentHash, { url, adminKey }) {
|
async function getPayment (paymentHash, { url, adminKey }, { signal }) {
|
||||||
const path = `/api/v1/payments/${paymentHash}`
|
const path = `/api/v1/payments/${paymentHash}`
|
||||||
|
|
||||||
const headers = new Headers()
|
const headers = new Headers()
|
||||||
@ -74,7 +75,7 @@ async function getPayment (paymentHash, { url, adminKey }) {
|
|||||||
headers.append('Content-Type', 'application/json')
|
headers.append('Content-Type', 'application/json')
|
||||||
headers.append('X-Api-Key', adminKey)
|
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)
|
assertContentTypeJson(res)
|
||||||
if (!res.ok) {
|
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 { msatsToSats } from '@/lib/format'
|
||||||
import { getAgent } from '@/lib/proxy'
|
import { getAgent } from '@/lib/proxy'
|
||||||
import { assertContentTypeJson } from '@/lib/url'
|
import { assertContentTypeJson } from '@/lib/url'
|
||||||
@ -5,13 +7,14 @@ import fetch from 'cross-fetch'
|
|||||||
|
|
||||||
export * from '@/wallets/lnbits'
|
export * from '@/wallets/lnbits'
|
||||||
|
|
||||||
export async function testCreateInvoice ({ url, invoiceKey }) {
|
export async function testCreateInvoice ({ url, invoiceKey }, { signal }) {
|
||||||
return await createInvoice({ msats: 1000, expiry: 1 }, { url, invoiceKey })
|
return await createInvoice({ msats: 1000, expiry: 1 }, { url, invoiceKey }, { signal })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createInvoice (
|
export async function createInvoice (
|
||||||
{ msats, description, descriptionHash, expiry },
|
{ msats, description, descriptionHash, expiry },
|
||||||
{ url, invoiceKey }) {
|
{ url, invoiceKey },
|
||||||
|
{ signal }) {
|
||||||
const path = '/api/v1/payments'
|
const path = '/api/v1/payments'
|
||||||
|
|
||||||
const headers = new Headers()
|
const headers = new Headers()
|
||||||
@ -38,12 +41,23 @@ export async function createInvoice (
|
|||||||
hostname = 'lnbits:5000'
|
hostname = 'lnbits:5000'
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(`${agent.protocol}//${hostname}${path}`, {
|
let res
|
||||||
method: 'POST',
|
try {
|
||||||
headers,
|
res = await fetch(`${agent.protocol}//${hostname}${path}`, {
|
||||||
agent,
|
method: 'POST',
|
||||||
body
|
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)
|
assertContentTypeJson(res)
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
@ -1,21 +1,16 @@
|
|||||||
import { nwcCall, supportedMethods } from '@/wallets/nwc'
|
import { getNwc, supportedMethods, nwcTryRun } from '@/wallets/nwc'
|
||||||
export * from '@/wallets/nwc'
|
export * from '@/wallets/nwc'
|
||||||
|
|
||||||
export async function testSendPayment ({ nwcUrl }, { logger }) {
|
export async function testSendPayment ({ nwcUrl }, { signal }) {
|
||||||
const timeout = 15_000
|
const supported = await supportedMethods(nwcUrl, { signal })
|
||||||
|
|
||||||
const supported = await supportedMethods(nwcUrl, { logger, timeout })
|
|
||||||
if (!supported.includes('pay_invoice')) {
|
if (!supported.includes('pay_invoice')) {
|
||||||
throw new Error('pay_invoice not supported')
|
throw new Error('pay_invoice not supported')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sendPayment (bolt11, { nwcUrl }, { logger }) {
|
export async function sendPayment (bolt11, { nwcUrl }, { signal }) {
|
||||||
const result = await nwcCall({
|
const nwc = await getNwc(nwcUrl, { signal })
|
||||||
nwcUrl,
|
// TODO: support AbortSignal
|
||||||
method: 'pay_invoice',
|
const result = await nwcTryRun(() => nwc.payInvoice(bolt11))
|
||||||
params: { invoice: bolt11 }
|
|
||||||
},
|
|
||||||
{ logger })
|
|
||||||
return result.preimage
|
return result.preimage
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import { Relay } from '@/lib/nostr'
|
import Nostr from '@/lib/nostr'
|
||||||
import { parseNwcUrl } from '@/lib/url'
|
|
||||||
import { string } from '@/lib/yup'
|
import { string } from '@/lib/yup'
|
||||||
import { finalizeEvent, nip04, verifyEvent } from 'nostr-tools'
|
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 name = 'nwc'
|
||||||
export const walletType = 'NWC'
|
export const walletType = 'NWC'
|
||||||
@ -33,61 +36,49 @@ export const card = {
|
|||||||
subtitle: 'use Nostr Wallet Connect for payments'
|
subtitle: 'use Nostr Wallet Connect for payments'
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function nwcCall ({ nwcUrl, method, params }, { logger, timeout } = {}) {
|
export async function getNwc (nwcUrl, { signal }) {
|
||||||
const { relayUrl, walletPubkey, secret } = parseNwcUrl(nwcUrl)
|
const ndk = Nostr.ndk
|
||||||
|
const { walletPubkey, secret, relayUrls } = parseNwcUrl(nwcUrl)
|
||||||
const relay = await Relay.connect(relayUrl, { timeout })
|
const nwc = new NDKNwc({
|
||||||
logger?.ok(`connected to ${relayUrl}`)
|
ndk,
|
||||||
|
pubkey: walletPubkey,
|
||||||
|
relayUrls,
|
||||||
|
secret
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: support AbortSignal
|
||||||
try {
|
try {
|
||||||
const payload = { method, params }
|
await nwc.blockUntilReady(NWC_CONNECT_TIMEOUT_MS)
|
||||||
const encrypted = await nip04.encrypt(secret, walletPubkey, JSON.stringify(payload))
|
} catch (err) {
|
||||||
|
if (err.message === 'Timeout') {
|
||||||
const request = finalizeEvent({
|
throw new TimeoutError(NWC_CONNECT_TIMEOUT_MS)
|
||||||
kind: 23194,
|
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
|
||||||
tags: [['p', walletPubkey]],
|
|
||||||
content: encrypted
|
|
||||||
}, secret)
|
|
||||||
|
|
||||||
// we need to subscribe to the response before publishing the request
|
|
||||||
// since NWC events are ephemeral (20000 <= kind < 30000)
|
|
||||||
const subscription = relay.fetch([{
|
|
||||||
kinds: [23195],
|
|
||||||
authors: [walletPubkey],
|
|
||||||
'#e': [request.id]
|
|
||||||
}], { timeout })
|
|
||||||
|
|
||||||
await relay.publish(request, { timeout })
|
|
||||||
|
|
||||||
logger?.info(`published ${method} request`)
|
|
||||||
|
|
||||||
logger?.info(`waiting for ${method} response ...`)
|
|
||||||
|
|
||||||
const [response] = await subscription
|
|
||||||
|
|
||||||
if (!response) {
|
|
||||||
throw new Error(`no ${method} response`)
|
|
||||||
}
|
}
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
|
||||||
logger?.ok(`${method} response received`)
|
return nwc
|
||||||
|
}
|
||||||
|
|
||||||
if (!verifyEvent(response)) throw new Error(`invalid ${method} response: failed to verify`)
|
/**
|
||||||
|
* Run a nwc function and throw if it errors
|
||||||
const decrypted = await nip04.decrypt(secret, walletPubkey, response.content)
|
* (workaround to handle ambiguous NDK error handling)
|
||||||
const content = JSON.parse(decrypted)
|
* @param {function} fun - the nwc function to run
|
||||||
|
* @returns - the result of the nwc function
|
||||||
if (content.error) throw new Error(content.error.message)
|
*/
|
||||||
if (content.result) return content.result
|
export async function nwcTryRun (fun) {
|
||||||
|
try {
|
||||||
throw new Error(`invalid ${method} response: missing error or result`)
|
const { error, result } = await fun()
|
||||||
} finally {
|
if (error) throw new Error(error.message || error.code)
|
||||||
relay?.close()
|
return result
|
||||||
logger?.info(`closed connection to ${relayUrl}`)
|
} catch (e) {
|
||||||
|
if (e.error) throw new Error(e.error.message || e.error.code)
|
||||||
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function supportedMethods (nwcUrl, { logger, timeout } = {}) {
|
export async function supportedMethods (nwcUrl, { signal }) {
|
||||||
const result = await nwcCall({ nwcUrl, method: 'get_info' }, { logger, timeout })
|
const nwc = await getNwc(nwcUrl, { signal })
|
||||||
|
// TODO: support AbortSignal
|
||||||
|
const result = await nwcTryRun(() => nwc.getInfo())
|
||||||
return result.methods
|
return result.methods
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,8 @@
|
|||||||
import { withTimeout } from '@/lib/time'
|
import { getNwc, supportedMethods, nwcTryRun } from '@/wallets/nwc'
|
||||||
import { nwcCall, supportedMethods } from '@/wallets/nwc'
|
|
||||||
export * from '@/wallets/nwc'
|
export * from '@/wallets/nwc'
|
||||||
|
|
||||||
export async function testCreateInvoice ({ nwcUrlRecv }, { logger }) {
|
export async function testCreateInvoice ({ nwcUrlRecv }, { signal }) {
|
||||||
const timeout = 15_000
|
const supported = await supportedMethods(nwcUrlRecv, { signal })
|
||||||
|
|
||||||
const supported = await supportedMethods(nwcUrlRecv, { logger, timeout })
|
|
||||||
|
|
||||||
const supports = (method) => supported.includes(method)
|
const supports = (method) => supported.includes(method)
|
||||||
|
|
||||||
@ -20,20 +17,12 @@ export async function testCreateInvoice ({ nwcUrlRecv }, { logger }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return await withTimeout(createInvoice({ msats: 1000, expiry: 1 }, { nwcUrlRecv }, { logger }), timeout)
|
return await createInvoice({ msats: 1000, expiry: 1 }, { nwcUrlRecv }, { signal })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createInvoice (
|
export async function createInvoice ({ msats, description, expiry }, { nwcUrlRecv }, { signal }) {
|
||||||
{ msats, description, expiry },
|
const nwc = await getNwc(nwcUrlRecv, { signal })
|
||||||
{ nwcUrlRecv }, { logger }) {
|
// TODO: support AbortSignal
|
||||||
const result = await nwcCall({
|
const result = await nwcTryRun(() => nwc.sendReq('make_invoice', { amount: msats, description, expiry }))
|
||||||
nwcUrl: nwcUrlRecv,
|
|
||||||
method: 'make_invoice',
|
|
||||||
params: {
|
|
||||||
amount: msats,
|
|
||||||
description,
|
|
||||||
expiry
|
|
||||||
}
|
|
||||||
}, { logger })
|
|
||||||
return result.invoice
|
return result.invoice
|
||||||
}
|
}
|
||||||
|
@ -2,17 +2,19 @@ import { useCallback } from 'react'
|
|||||||
import { useSendWallets } from '@/wallets'
|
import { useSendWallets } from '@/wallets'
|
||||||
import { formatSats } from '@/lib/format'
|
import { formatSats } from '@/lib/format'
|
||||||
import useInvoice from '@/components/use-invoice'
|
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 {
|
import {
|
||||||
WalletsNotAvailableError, WalletSenderError, WalletAggregateError, WalletPaymentAggregateError,
|
WalletsNotAvailableError, WalletSenderError, WalletAggregateError, WalletPaymentAggregateError,
|
||||||
WalletNotEnabledError, WalletSendNotConfiguredError, WalletPaymentError, WalletError
|
WalletNotEnabledError, WalletSendNotConfiguredError, WalletPaymentError, WalletError, WalletReceiverError
|
||||||
} from '@/wallets/errors'
|
} from '@/wallets/errors'
|
||||||
import { canSend } from './common'
|
import { canSend } from './common'
|
||||||
import { useWalletLoggerFactory } from './logger'
|
import { useWalletLoggerFactory } from './logger'
|
||||||
|
import { timeoutSignal, withTimeout } from '@/lib/time'
|
||||||
|
|
||||||
export function useWalletPayment () {
|
export function useWalletPayment () {
|
||||||
const wallets = useSendWallets()
|
const wallets = useSendWallets()
|
||||||
const sendPayment = useSendPayment()
|
const sendPayment = useSendPayment()
|
||||||
|
const loggerFactory = useWalletLoggerFactory()
|
||||||
const invoiceHelper = useInvoice()
|
const invoiceHelper = useInvoice()
|
||||||
|
|
||||||
return useCallback(async (invoice, { waitFor, updateOnFallback }) => {
|
return useCallback(async (invoice, { waitFor, updateOnFallback }) => {
|
||||||
@ -24,44 +26,71 @@ export function useWalletPayment () {
|
|||||||
throw new WalletsNotAvailableError()
|
throw new WalletsNotAvailableError()
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [i, wallet] of wallets.entries()) {
|
for (let i = 0; i < wallets.length; i++) {
|
||||||
|
const wallet = wallets[i]
|
||||||
|
const logger = loggerFactory(wallet)
|
||||||
|
|
||||||
|
const { bolt11 } = latestInvoice
|
||||||
const controller = invoiceController(latestInvoice, invoiceHelper.isInvoice)
|
const controller = invoiceController(latestInvoice, invoiceHelper.isInvoice)
|
||||||
|
|
||||||
|
const walletPromise = sendPayment(wallet, logger, latestInvoice)
|
||||||
|
const pollPromise = controller.wait(waitFor)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await new Promise((resolve, reject) => {
|
return await new Promise((resolve, reject) => {
|
||||||
// can't await wallet payments since we might pay hold invoices and thus payments might not settle immediately.
|
// can't await wallet payments since we might pay hold invoices and thus payments might not settle immediately.
|
||||||
// that's why we separately check if we received the payment with the invoice controller.
|
// that's why we separately check if we received the payment with the invoice controller.
|
||||||
sendPayment(wallet, latestInvoice).catch(reject)
|
walletPromise.catch(reject)
|
||||||
controller.wait(waitFor)
|
pollPromise.then(resolve).catch(reject)
|
||||||
.then(resolve)
|
|
||||||
.catch(reject)
|
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// cancel invoice to make sure it cannot be paid later and create new invoice to retry.
|
let paymentError = err
|
||||||
// we only need to do this if payment was attempted which is not the case if the wallet is not enabled.
|
const message = `payment failed: ${paymentError.reason ?? paymentError.message}`
|
||||||
if (err instanceof WalletPaymentError) {
|
|
||||||
await invoiceHelper.cancel(latestInvoice)
|
|
||||||
|
|
||||||
// is there another wallet to try?
|
if (!(paymentError instanceof WalletError)) {
|
||||||
const lastAttempt = i === wallets.length - 1
|
// payment failed for some reason unrelated to wallets (ie invoice expired or was canceled).
|
||||||
if (!lastAttempt) {
|
// bail out of attempting wallets.
|
||||||
latestInvoice = await invoiceHelper.retry(latestInvoice, { update: updateOnFallback })
|
logger.error(message, { bolt11 })
|
||||||
|
throw paymentError
|
||||||
|
}
|
||||||
|
|
||||||
|
// at this point, paymentError is always a wallet error,
|
||||||
|
// we just need to distinguish between receiver and sender errors
|
||||||
|
|
||||||
|
try {
|
||||||
|
// we need to poll one more time to check for failed forwards since sender wallet errors
|
||||||
|
// can be caused by them which we want to handle as receiver errors, not sender errors.
|
||||||
|
await invoiceHelper.isInvoice(latestInvoice, waitFor)
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof WalletError) {
|
||||||
|
paymentError = err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: receiver fallbacks
|
if (paymentError instanceof WalletReceiverError) {
|
||||||
//
|
// if payment failed because of the receiver, use the same wallet again
|
||||||
// if payment failed because of the receiver, we should use the same wallet again.
|
// and log this as info, not error
|
||||||
// if (err instanceof ReceiverError) { ... }
|
logger.info('failed to forward payment to receiver, retrying with new invoice', { bolt11 })
|
||||||
|
i -= 1
|
||||||
// try next wallet if the payment failed because of the wallet
|
} else if (paymentError instanceof WalletPaymentError) {
|
||||||
// and not because it expired or was canceled
|
// only log payment errors, not configuration errors
|
||||||
if (err instanceof WalletError) {
|
logger.error(message, { bolt11 })
|
||||||
aggregateError = new WalletAggregateError([aggregateError, err], latestInvoice)
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// payment failed not because of the sender or receiver wallet. bail out of attemping wallets.
|
if (paymentError instanceof WalletPaymentError) {
|
||||||
throw err
|
// if a payment was attempted, cancel invoice to make sure it cannot be paid later and create new invoice to retry.
|
||||||
|
await invoiceHelper.cancel(latestInvoice)
|
||||||
|
}
|
||||||
|
|
||||||
|
// only create a new invoice if we will try to pay with a wallet again
|
||||||
|
const retry = paymentError instanceof WalletReceiverError || i < wallets.length - 1
|
||||||
|
if (retry) {
|
||||||
|
latestInvoice = await invoiceHelper.retry(latestInvoice, { update: updateOnFallback })
|
||||||
|
}
|
||||||
|
|
||||||
|
aggregateError = new WalletAggregateError([aggregateError, paymentError], latestInvoice)
|
||||||
|
|
||||||
|
continue
|
||||||
} finally {
|
} finally {
|
||||||
controller.stop()
|
controller.stop()
|
||||||
}
|
}
|
||||||
@ -111,11 +140,7 @@ function invoiceController (inv, isInvoice) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function useSendPayment () {
|
function useSendPayment () {
|
||||||
const factory = useWalletLoggerFactory()
|
return useCallback(async (wallet, logger, invoice) => {
|
||||||
|
|
||||||
return useCallback(async (wallet, invoice) => {
|
|
||||||
const logger = factory(wallet)
|
|
||||||
|
|
||||||
if (!wallet.config.enabled) {
|
if (!wallet.config.enabled) {
|
||||||
throw new WalletNotEnabledError(wallet.def.name)
|
throw new WalletNotEnabledError(wallet.def.name)
|
||||||
}
|
}
|
||||||
@ -128,12 +153,17 @@ function useSendPayment () {
|
|||||||
|
|
||||||
logger.info(`↗ sending payment: ${formatSats(satsRequested)}`, { bolt11 })
|
logger.info(`↗ sending payment: ${formatSats(satsRequested)}`, { bolt11 })
|
||||||
try {
|
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 })
|
logger.ok(`↗ payment sent: ${formatSats(satsRequested)}`, { bolt11, preimage })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
// we don't log the error here since we want to handle receiver errors separately
|
||||||
const message = err.message || err.toString?.()
|
const message = err.message || err.toString?.()
|
||||||
logger.error(`payment failed: ${message}`, { bolt11 })
|
|
||||||
throw new WalletSenderError(wallet.def.name, invoice, message)
|
throw new WalletSenderError(wallet.def.name, invoice, message)
|
||||||
}
|
}
|
||||||
}, [factory])
|
}, [])
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
|
import { fetchWithTimeout } from '@/lib/fetch'
|
||||||
import { assertContentTypeJson, assertResponseOk } from '@/lib/url'
|
import { assertContentTypeJson, assertResponseOk } from '@/lib/url'
|
||||||
|
|
||||||
export * from '@/wallets/phoenixd'
|
export * from '@/wallets/phoenixd'
|
||||||
|
|
||||||
export async function testSendPayment (config, { logger }) {
|
export async function testSendPayment (config, { logger, signal }) {
|
||||||
// TODO:
|
// TODO:
|
||||||
// Not sure which endpoint to call to test primary password
|
// Not sure which endpoint to call to test primary password
|
||||||
// see https://phoenix.acinq.co/server/api
|
// 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
|
// https://phoenix.acinq.co/server/api#pay-bolt11-invoice
|
||||||
const path = '/payinvoice'
|
const path = '/payinvoice'
|
||||||
|
|
||||||
@ -21,10 +22,11 @@ export async function sendPayment (bolt11, { url, primaryPassword }) {
|
|||||||
const body = new URLSearchParams()
|
const body = new URLSearchParams()
|
||||||
body.append('invoice', bolt11)
|
body.append('invoice', bolt11)
|
||||||
|
|
||||||
const res = await fetch(url + path, {
|
const res = await fetchWithTimeout(url + path, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers,
|
headers,
|
||||||
body
|
body,
|
||||||
|
signal
|
||||||
})
|
})
|
||||||
|
|
||||||
assertResponseOk(res)
|
assertResponseOk(res)
|
||||||
|
@ -1,17 +1,20 @@
|
|||||||
|
import { fetchWithTimeout } from '@/lib/fetch'
|
||||||
import { msatsToSats } from '@/lib/format'
|
import { msatsToSats } from '@/lib/format'
|
||||||
import { assertContentTypeJson, assertResponseOk } from '@/lib/url'
|
import { assertContentTypeJson, assertResponseOk } from '@/lib/url'
|
||||||
|
|
||||||
export * from '@/wallets/phoenixd'
|
export * from '@/wallets/phoenixd'
|
||||||
|
|
||||||
export async function testCreateInvoice ({ url, secondaryPassword }) {
|
export async function testCreateInvoice ({ url, secondaryPassword }, { signal }) {
|
||||||
return await createInvoice(
|
return await createInvoice(
|
||||||
{ msats: 1000, description: 'SN test invoice', expiry: 1 },
|
{ msats: 1000, description: 'SN test invoice', expiry: 1 },
|
||||||
{ url, secondaryPassword })
|
{ url, secondaryPassword },
|
||||||
|
{ signal })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createInvoice (
|
export async function createInvoice (
|
||||||
{ msats, description, descriptionHash, expiry },
|
{ msats, description, descriptionHash, expiry },
|
||||||
{ url, secondaryPassword }
|
{ url, secondaryPassword },
|
||||||
|
{ signal }
|
||||||
) {
|
) {
|
||||||
// https://phoenix.acinq.co/server/api#create-bolt11-invoice
|
// https://phoenix.acinq.co/server/api#create-bolt11-invoice
|
||||||
const path = '/createinvoice'
|
const path = '/createinvoice'
|
||||||
@ -24,10 +27,11 @@ export async function createInvoice (
|
|||||||
body.append('description', description)
|
body.append('description', description)
|
||||||
body.append('amountSat', msatsToSats(msats))
|
body.append('amountSat', msatsToSats(msats))
|
||||||
|
|
||||||
const res = await fetch(url + path, {
|
const res = await fetchWithTimeout(url + path, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers,
|
headers,
|
||||||
body
|
body,
|
||||||
|
signal
|
||||||
})
|
})
|
||||||
|
|
||||||
assertResponseOk(res)
|
assertResponseOk(res)
|
||||||
|
@ -15,8 +15,8 @@ import { walletLogger } from '@/api/resolvers/wallet'
|
|||||||
import walletDefs from '@/wallets/server'
|
import walletDefs from '@/wallets/server'
|
||||||
import { parsePaymentRequest } from 'ln-service'
|
import { parsePaymentRequest } from 'ln-service'
|
||||||
import { toPositiveBigInt, toPositiveNumber, formatMsats, formatSats, msatsToSats } from '@/lib/format'
|
import { toPositiveBigInt, toPositiveNumber, formatMsats, formatSats, msatsToSats } from '@/lib/format'
|
||||||
import { PAID_ACTION_TERMINAL_STATES } from '@/lib/constants'
|
import { PAID_ACTION_TERMINAL_STATES, WALLET_CREATE_INVOICE_TIMEOUT_MS } from '@/lib/constants'
|
||||||
import { withTimeout } from '@/lib/time'
|
import { timeoutSignal, withTimeout } from '@/lib/time'
|
||||||
import { canReceive } from './common'
|
import { canReceive } from './common'
|
||||||
import wrapInvoice from './wrap'
|
import wrapInvoice from './wrap'
|
||||||
|
|
||||||
@ -24,9 +24,9 @@ export default [lnd, cln, lnAddr, lnbits, nwc, phoenixd, blink, lnc, webln]
|
|||||||
|
|
||||||
const MAX_PENDING_INVOICES_PER_WALLET = 25
|
const MAX_PENDING_INVOICES_PER_WALLET = 25
|
||||||
|
|
||||||
export async function createInvoice (userId, { msats, description, descriptionHash, expiry = 360 }, { models }) {
|
export async function createInvoice (userId, { msats, description, descriptionHash, expiry = 360 }, { predecessorId, models }) {
|
||||||
// get the wallets in order of priority
|
// get the wallets in order of priority
|
||||||
const wallets = await getInvoiceableWallets(userId, { models })
|
const wallets = await getInvoiceableWallets(userId, { predecessorId, models })
|
||||||
|
|
||||||
msats = toPositiveNumber(msats)
|
msats = toPositiveNumber(msats)
|
||||||
|
|
||||||
@ -81,7 +81,7 @@ export async function createInvoice (userId, { msats, description, descriptionHa
|
|||||||
|
|
||||||
export async function createWrappedInvoice (userId,
|
export async function createWrappedInvoice (userId,
|
||||||
{ msats, feePercent, description, descriptionHash, expiry = 360 },
|
{ msats, feePercent, description, descriptionHash, expiry = 360 },
|
||||||
{ models, me, lnd }) {
|
{ predecessorId, models, me, lnd }) {
|
||||||
let logger, bolt11
|
let logger, bolt11
|
||||||
try {
|
try {
|
||||||
const { invoice, wallet } = await createInvoice(userId, {
|
const { invoice, wallet } = await createInvoice(userId, {
|
||||||
@ -90,7 +90,7 @@ export async function createWrappedInvoice (userId,
|
|||||||
description,
|
description,
|
||||||
descriptionHash,
|
descriptionHash,
|
||||||
expiry
|
expiry
|
||||||
}, { models })
|
}, { predecessorId, models })
|
||||||
|
|
||||||
logger = walletLogger({ wallet, models })
|
logger = walletLogger({ wallet, models })
|
||||||
bolt11 = invoice
|
bolt11 = invoice
|
||||||
@ -110,18 +110,48 @@ export async function createWrappedInvoice (userId,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getInvoiceableWallets (userId, { models }) {
|
export async function getInvoiceableWallets (userId, { predecessorId, models }) {
|
||||||
const wallets = await models.wallet.findMany({
|
// filter out all wallets that have already been tried by recursively following the retry chain of predecessor invoices.
|
||||||
where: { userId, enabled: true },
|
// the current predecessor invoice is in state 'FAILED' and not in state 'RETRYING' because we are currently retrying it
|
||||||
include: {
|
// so it has not been updated yet.
|
||||||
user: true
|
// if predecessorId is not provided, the subquery will be empty and thus no wallets are filtered out.
|
||||||
},
|
const wallets = await models.$queryRaw`
|
||||||
orderBy: [
|
SELECT
|
||||||
{ priority: 'asc' },
|
"Wallet".*,
|
||||||
// use id as tie breaker (older wallet first)
|
jsonb_build_object(
|
||||||
{ id: 'asc' }
|
'id', "users"."id",
|
||||||
]
|
'hideInvoiceDesc', "users"."hideInvoiceDesc"
|
||||||
})
|
) AS "user"
|
||||||
|
FROM "Wallet"
|
||||||
|
JOIN "users" ON "users"."id" = "Wallet"."userId"
|
||||||
|
WHERE
|
||||||
|
"Wallet"."userId" = ${userId}
|
||||||
|
AND "Wallet"."enabled" = true
|
||||||
|
AND "Wallet"."id" NOT IN (
|
||||||
|
WITH RECURSIVE "Retries" AS (
|
||||||
|
-- select the current failed invoice that we are currently retrying
|
||||||
|
-- this failed invoice will be used to start the recursion
|
||||||
|
SELECT "Invoice"."id", "Invoice"."predecessorId"
|
||||||
|
FROM "Invoice"
|
||||||
|
WHERE "Invoice"."id" = ${predecessorId} AND "Invoice"."actionState" = 'FAILED'
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
-- recursive part: use predecessorId to select the previous invoice that failed in the chain
|
||||||
|
-- until there is no more previous invoice
|
||||||
|
SELECT "Invoice"."id", "Invoice"."predecessorId"
|
||||||
|
FROM "Invoice"
|
||||||
|
JOIN "Retries" ON "Invoice"."id" = "Retries"."predecessorId"
|
||||||
|
WHERE "Invoice"."actionState" = 'RETRYING'
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
"InvoiceForward"."walletId"
|
||||||
|
FROM "Retries"
|
||||||
|
JOIN "InvoiceForward" ON "InvoiceForward"."invoiceId" = "Retries"."id"
|
||||||
|
JOIN "Withdrawl" ON "Withdrawl".id = "InvoiceForward"."withdrawlId"
|
||||||
|
WHERE "Withdrawl"."status" IS DISTINCT FROM 'CONFIRMED'
|
||||||
|
)
|
||||||
|
ORDER BY "Wallet"."priority" ASC, "Wallet"."id" ASC`
|
||||||
|
|
||||||
const walletsWithDefs = wallets.map(wallet => {
|
const walletsWithDefs = wallets.map(wallet => {
|
||||||
const w = walletDefs.find(w => w.walletType === wallet.type)
|
const w = walletDefs.find(w => w.walletType === wallet.type)
|
||||||
@ -171,6 +201,9 @@ async function walletCreateInvoice ({ wallet, def }, {
|
|||||||
expiry
|
expiry
|
||||||
},
|
},
|
||||||
wallet.wallet,
|
wallet.wallet,
|
||||||
{ logger }
|
{
|
||||||
), 10_000)
|
logger,
|
||||||
|
signal: timeoutSignal(WALLET_CREATE_INVOICE_TIMEOUT_MS)
|
||||||
|
}
|
||||||
|
), WALLET_CREATE_INVOICE_TIMEOUT_MS)
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { notifyEarner } from '@/lib/webPush'
|
import { notifyEarner } from '@/lib/webPush'
|
||||||
import createPrisma from '@/lib/create-prisma'
|
import createPrisma from '@/lib/create-prisma'
|
||||||
import { proportions } from '@/lib/madness'
|
|
||||||
import { SN_NO_REWARDS_IDS } from '@/lib/constants'
|
import { SN_NO_REWARDS_IDS } from '@/lib/constants'
|
||||||
|
|
||||||
const TOTAL_UPPER_BOUND_MSATS = 1_000_000_000
|
const TOTAL_UPPER_BOUND_MSATS = 1_000_000_000
|
||||||
@ -40,18 +39,19 @@ export async function earn ({ name }) {
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
How earnings (used to) work:
|
How earnings (used to) work:
|
||||||
1/3: top 21% posts over last 36 hours, scored on a relative basis
|
1/3: top 50% posts over last 36 hours, scored on a relative basis
|
||||||
1/3: top 21% comments over last 36 hours, scored on a relative basis
|
1/3: top 50% comments over last 36 hours, scored on a relative basis
|
||||||
1/3: top upvoters of top posts/comments, scored on:
|
1/3: top upvoters of top posts/comments, scored on:
|
||||||
- their trust
|
- their trust
|
||||||
- how much they tipped
|
- how much they tipped
|
||||||
- how early they upvoted it
|
- how early they upvoted it
|
||||||
- how the post/comment scored
|
- how the post/comment scored
|
||||||
|
|
||||||
Now: 80% of earnings go to top 100 stackers by value, and 10% each to their forever and one day referrers
|
Now: 80% of earnings go to top stackers by relative value, and 10% each to their forever and one day referrers
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// get earners { userId, id, type, rank, proportion, foreverReferrerId, oneDayReferrerId }
|
// get earners { userId, id, type, rank, proportion, foreverReferrerId, oneDayReferrerId }
|
||||||
|
// has to earn at least 125000 msats to be eligible (so that they get at least 1 sat after referrals)
|
||||||
const earners = await models.$queryRaw`
|
const earners = await models.$queryRaw`
|
||||||
WITH earners AS (
|
WITH earners AS (
|
||||||
SELECT users.id AS "userId", users."referrerId" AS "foreverReferrerId",
|
SELECT users.id AS "userId", users."referrerId" AS "foreverReferrerId",
|
||||||
@ -63,8 +63,8 @@ export async function earn ({ name }) {
|
|||||||
'day') uv
|
'day') uv
|
||||||
JOIN users ON users.id = uv.id
|
JOIN users ON users.id = uv.id
|
||||||
WHERE NOT (users.id = ANY (${SN_NO_REWARDS_IDS}))
|
WHERE NOT (users.id = ANY (${SN_NO_REWARDS_IDS}))
|
||||||
|
AND uv.proportion >= 0.0000125
|
||||||
ORDER BY proportion DESC
|
ORDER BY proportion DESC
|
||||||
LIMIT 100
|
|
||||||
)
|
)
|
||||||
SELECT earners.*,
|
SELECT earners.*,
|
||||||
COALESCE(
|
COALESCE(
|
||||||
@ -86,10 +86,10 @@ export async function earn ({ name }) {
|
|||||||
let total = 0
|
let total = 0
|
||||||
|
|
||||||
const notifications = {}
|
const notifications = {}
|
||||||
for (const [i, earner] of earners.entries()) {
|
for (const [, earner] of earners.entries()) {
|
||||||
const foreverReferrerEarnings = Math.floor(parseFloat(earner.proportion * sum * 0.1)) // 10% of earnings
|
const foreverReferrerEarnings = Math.floor(parseFloat(earner.proportion * sum * 0.1)) // 10% of earnings
|
||||||
let oneDayReferrerEarnings = Math.floor(parseFloat(earner.proportion * sum * 0.1)) // 10% of earnings
|
let oneDayReferrerEarnings = Math.floor(parseFloat(earner.proportion * sum * 0.1)) // 10% of earnings
|
||||||
const earnerEarnings = Math.floor(parseFloat(proportions[i] * sum)) - foreverReferrerEarnings - oneDayReferrerEarnings
|
const earnerEarnings = Math.floor(parseFloat(earner.proportion * sum)) - foreverReferrerEarnings - oneDayReferrerEarnings
|
||||||
|
|
||||||
total += earnerEarnings + foreverReferrerEarnings + oneDayReferrerEarnings
|
total += earnerEarnings + foreverReferrerEarnings + oneDayReferrerEarnings
|
||||||
if (total > sum) {
|
if (total > sum) {
|
||||||
@ -108,7 +108,7 @@ export async function earn ({ name }) {
|
|||||||
'oneDayReferrer', earner.oneDayReferrerId,
|
'oneDayReferrer', earner.oneDayReferrerId,
|
||||||
'oneDayReferrerEarnings', oneDayReferrerEarnings)
|
'oneDayReferrerEarnings', oneDayReferrerEarnings)
|
||||||
|
|
||||||
if (earnerEarnings > 0) {
|
if (earnerEarnings > 1000) {
|
||||||
stmts.push(...earnStmts({
|
stmts.push(...earnStmts({
|
||||||
msats: earnerEarnings,
|
msats: earnerEarnings,
|
||||||
userId: earner.userId,
|
userId: earner.userId,
|
||||||
@ -140,7 +140,7 @@ export async function earn ({ name }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (earner.foreverReferrerId && foreverReferrerEarnings > 0) {
|
if (earner.foreverReferrerId && foreverReferrerEarnings > 1000) {
|
||||||
stmts.push(...earnStmts({
|
stmts.push(...earnStmts({
|
||||||
msats: foreverReferrerEarnings,
|
msats: foreverReferrerEarnings,
|
||||||
userId: earner.foreverReferrerId,
|
userId: earner.foreverReferrerId,
|
||||||
@ -153,7 +153,7 @@ export async function earn ({ name }) {
|
|||||||
oneDayReferrerEarnings += foreverReferrerEarnings
|
oneDayReferrerEarnings += foreverReferrerEarnings
|
||||||
}
|
}
|
||||||
|
|
||||||
if (earner.oneDayReferrerId && oneDayReferrerEarnings > 0) {
|
if (earner.oneDayReferrerId && oneDayReferrerEarnings > 1000) {
|
||||||
stmts.push(...earnStmts({
|
stmts.push(...earnStmts({
|
||||||
msats: oneDayReferrerEarnings,
|
msats: oneDayReferrerEarnings,
|
||||||
userId: earner.oneDayReferrerId,
|
userId: earner.oneDayReferrerId,
|
||||||
|
@ -38,6 +38,12 @@ import { expireBoost } from './expireBoost'
|
|||||||
import { payingActionConfirmed, payingActionFailed } from './payingAction'
|
import { payingActionConfirmed, payingActionFailed } from './payingAction'
|
||||||
import { autoDropBolt11s } from './autoDropBolt11'
|
import { autoDropBolt11s } from './autoDropBolt11'
|
||||||
|
|
||||||
|
// WebSocket polyfill
|
||||||
|
import ws from 'isomorphic-ws'
|
||||||
|
if (typeof WebSocket === 'undefined') {
|
||||||
|
global.WebSocket = ws
|
||||||
|
}
|
||||||
|
|
||||||
async function work () {
|
async function work () {
|
||||||
const boss = new PgBoss(process.env.DATABASE_URL)
|
const boss = new PgBoss(process.env.DATABASE_URL)
|
||||||
const models = createPrisma({
|
const models = createPrisma({
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { signId, calculateId, getPublicKey } from 'nostr'
|
import Nostr from '@/lib/nostr'
|
||||||
import { Relay } from '@/lib/nostr'
|
|
||||||
|
|
||||||
const nostrOptions = { startAfter: 5, retryLimit: 21, retryBackoff: true }
|
const nostrOptions = { startAfter: 5, retryLimit: 21, retryBackoff: true }
|
||||||
|
|
||||||
@ -40,26 +39,18 @@ export async function nip57 ({ data: { hash }, boss, lnd, models }) {
|
|||||||
|
|
||||||
const e = {
|
const e = {
|
||||||
kind: 9735,
|
kind: 9735,
|
||||||
pubkey: getPublicKey(process.env.NOSTR_PRIVATE_KEY),
|
|
||||||
created_at: Math.floor(new Date(inv.confirmedAt).getTime() / 1000),
|
created_at: Math.floor(new Date(inv.confirmedAt).getTime() / 1000),
|
||||||
content: '',
|
content: '',
|
||||||
tags
|
tags
|
||||||
}
|
}
|
||||||
e.id = await calculateId(e)
|
|
||||||
e.sig = await signId(process.env.NOSTR_PRIVATE_KEY, e.id)
|
|
||||||
|
|
||||||
console.log('zap note', e, relays)
|
console.log('zap note', e, relays)
|
||||||
await Promise.allSettled(
|
const signer = Nostr.getSigner({ privKey: process.env.NOSTR_PRIVATE_KEY })
|
||||||
relays.map(async r => {
|
await Nostr.publish(e, {
|
||||||
const timeout = 1000
|
relays,
|
||||||
const relay = await Relay.connect(r, { timeout })
|
signer,
|
||||||
try {
|
timeout: 1000
|
||||||
await relay.publish(e, { timeout })
|
})
|
||||||
} finally {
|
|
||||||
relay.close()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e)
|
console.log(e)
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { getPaymentFailureStatus, hodlInvoiceCltvDetails, getPaymentOrNotSent } from '@/api/lnd'
|
import { getPaymentFailureStatus, hodlInvoiceCltvDetails, getPaymentOrNotSent } from '@/api/lnd'
|
||||||
import { paidActions } from '@/api/paidAction'
|
import { paidActions } from '@/api/paidAction'
|
||||||
import { walletLogger } from '@/api/resolvers/wallet'
|
import { walletLogger } from '@/api/resolvers/wallet'
|
||||||
import { LND_PATHFINDING_TIMEOUT_MS, PAID_ACTION_TERMINAL_STATES } from '@/lib/constants'
|
import { LND_PATHFINDING_TIME_PREF_PPM, LND_PATHFINDING_TIMEOUT_MS, PAID_ACTION_TERMINAL_STATES } from '@/lib/constants'
|
||||||
import { formatMsats, formatSats, msatsToSats, toPositiveNumber } from '@/lib/format'
|
import { formatMsats, formatSats, msatsToSats, toPositiveNumber } from '@/lib/format'
|
||||||
import { datePivot } from '@/lib/time'
|
import { datePivot } from '@/lib/time'
|
||||||
import { Prisma } from '@prisma/client'
|
import { Prisma } from '@prisma/client'
|
||||||
@ -270,6 +270,7 @@ export async function paidActionForwarding ({ data: { invoiceId, ...args }, mode
|
|||||||
request: bolt11,
|
request: bolt11,
|
||||||
max_fee_mtokens: String(maxFeeMsats),
|
max_fee_mtokens: String(maxFeeMsats),
|
||||||
pathfinding_timeout: LND_PATHFINDING_TIMEOUT_MS,
|
pathfinding_timeout: LND_PATHFINDING_TIMEOUT_MS,
|
||||||
|
confidence: LND_PATHFINDING_TIME_PREF_PPM,
|
||||||
max_timeout_height: maxTimeoutHeight
|
max_timeout_height: maxTimeoutHeight
|
||||||
}).catch(console.error)
|
}).catch(console.error)
|
||||||
}
|
}
|
||||||
@ -316,13 +317,11 @@ export async function paidActionForwarded ({ data: { invoiceId, withdrawal, ...a
|
|||||||
}, { models, lnd, boss })
|
}, { models, lnd, boss })
|
||||||
|
|
||||||
if (transitionedInvoice) {
|
if (transitionedInvoice) {
|
||||||
const { bolt11, msatsPaid, msatsFeePaid } = transitionedInvoice.invoiceForward.withdrawl
|
const { bolt11, msatsPaid } = transitionedInvoice.invoiceForward.withdrawl
|
||||||
// the amount we paid includes the fee so we need to subtract it to get the amount received
|
|
||||||
const received = Number(msatsPaid) - Number(msatsFeePaid)
|
|
||||||
|
|
||||||
const logger = walletLogger({ wallet: transitionedInvoice.invoiceForward.wallet, models })
|
const logger = walletLogger({ wallet: transitionedInvoice.invoiceForward.wallet, models })
|
||||||
logger.ok(
|
logger.ok(
|
||||||
`↙ payment received: ${formatSats(msatsToSats(received))}`,
|
`↙ payment received: ${formatSats(msatsToSats(Number(msatsPaid)))}`,
|
||||||
{
|
{
|
||||||
bolt11,
|
bolt11,
|
||||||
preimage: transitionedInvoice.preimage
|
preimage: transitionedInvoice.preimage
|
||||||
|
Loading…
x
Reference in New Issue
Block a user