2022-01-05 20:37:34 +00:00
|
|
|
import { createInvoice, decodePaymentRequest, payViaPaymentRequest } from 'ln-service'
|
2021-05-11 15:52:50 +00:00
|
|
|
import { UserInputError, AuthenticationError } from 'apollo-server-micro'
|
2021-05-20 01:09:32 +00:00
|
|
|
import serialize from './serial'
|
2021-12-14 16:42:54 +00:00
|
|
|
import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
|
2021-12-15 16:50:11 +00:00
|
|
|
import lnpr from 'bolt11'
|
2021-12-16 20:02:17 +00:00
|
|
|
import { SELECT } from './item'
|
2021-05-11 15:52:50 +00:00
|
|
|
|
2021-04-30 21:42:51 +00:00
|
|
|
export default {
|
|
|
|
Query: {
|
2021-05-06 21:15:22 +00:00
|
|
|
invoice: async (parent, { id }, { me, models, lnd }) => {
|
2021-05-20 01:09:32 +00:00
|
|
|
if (!me) {
|
|
|
|
throw new AuthenticationError('you must be logged in')
|
|
|
|
}
|
|
|
|
|
|
|
|
const inv = await models.invoice.findUnique({
|
|
|
|
where: {
|
|
|
|
id: Number(id)
|
|
|
|
},
|
|
|
|
include: {
|
|
|
|
user: true
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2021-06-27 03:09:39 +00:00
|
|
|
if (inv.user.id !== me.id) {
|
2021-05-20 01:09:32 +00:00
|
|
|
throw new AuthenticationError('not ur invoice')
|
|
|
|
}
|
|
|
|
|
|
|
|
return inv
|
2021-05-13 01:51:37 +00:00
|
|
|
},
|
|
|
|
withdrawl: async (parent, { id }, { me, models, lnd }) => {
|
2021-05-20 01:09:32 +00:00
|
|
|
if (!me) {
|
|
|
|
throw new AuthenticationError('you must be logged in')
|
|
|
|
}
|
|
|
|
|
|
|
|
const wdrwl = await models.withdrawl.findUnique({
|
|
|
|
where: {
|
|
|
|
id: Number(id)
|
|
|
|
},
|
|
|
|
include: {
|
|
|
|
user: true
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2021-06-27 03:09:39 +00:00
|
|
|
if (wdrwl.user.id !== me.id) {
|
2021-08-19 21:42:21 +00:00
|
|
|
throw new AuthenticationError('not ur withdrawal')
|
2021-05-20 01:09:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return wdrwl
|
2021-06-02 23:15:28 +00:00
|
|
|
},
|
|
|
|
connectAddress: async (parent, args, { lnd }) => {
|
2021-06-03 22:08:00 +00:00
|
|
|
return process.env.LND_CONNECT_ADDRESS
|
2021-11-30 15:23:26 +00:00
|
|
|
},
|
2021-12-16 17:27:12 +00:00
|
|
|
walletHistory: async (parent, { cursor, inc }, { me, models, lnd }) => {
|
2021-11-30 15:23:26 +00:00
|
|
|
const decodedCursor = decodeCursor(cursor)
|
2021-12-15 16:50:11 +00:00
|
|
|
if (!me) {
|
|
|
|
throw new AuthenticationError('you must be logged in')
|
|
|
|
}
|
2021-11-30 15:23:26 +00:00
|
|
|
|
2021-12-16 20:17:50 +00:00
|
|
|
const include = new Set(inc?.split(','))
|
2021-12-16 17:27:12 +00:00
|
|
|
const queries = []
|
|
|
|
|
|
|
|
if (include.has('invoice')) {
|
|
|
|
queries.push(
|
|
|
|
`(SELECT ('invoice' || id) as id, id as "factId", bolt11, created_at as "createdAt",
|
|
|
|
COALESCE("msatsReceived", "msatsRequested") as msats, NULL as "msatsFee",
|
|
|
|
CASE WHEN "confirmedAt" IS NOT NULL THEN 'CONFIRMED'
|
|
|
|
WHEN "expiresAt" <= $2 THEN 'EXPIRED'
|
|
|
|
WHEN cancelled THEN 'CANCELLED'
|
|
|
|
ELSE 'PENDING' END as status,
|
|
|
|
'invoice' as type
|
|
|
|
FROM "Invoice"
|
|
|
|
WHERE "userId" = $1
|
|
|
|
AND created_at <= $2)`)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (include.has('withdrawal')) {
|
|
|
|
queries.push(
|
|
|
|
`(SELECT ('withdrawal' || id) as id, id as "factId", bolt11, created_at as "createdAt",
|
|
|
|
CASE WHEN status = 'CONFIRMED' THEN "msatsPaid"
|
|
|
|
ELSE "msatsPaying" END as msats,
|
|
|
|
CASE WHEN status = 'CONFIRMED' THEN "msatsFeePaid"
|
|
|
|
ELSE "msatsFeePaying" END as "msatsFee",
|
|
|
|
COALESCE(status::text, 'PENDING') as status,
|
|
|
|
'withdrawal' as type
|
|
|
|
FROM "Withdrawl"
|
|
|
|
WHERE "userId" = $1
|
|
|
|
AND created_at <= $2)`)
|
|
|
|
}
|
|
|
|
|
2021-12-16 20:02:17 +00:00
|
|
|
if (include.has('stacked')) {
|
|
|
|
queries.push(
|
|
|
|
`(SELECT ('stacked' || "Item".id) as id, "Item".id as "factId", NULL as bolt11,
|
|
|
|
MAX("ItemAct".created_at) as "createdAt", sum("ItemAct".sats) * 1000 as msats,
|
|
|
|
0 as "msatsFee", NULL as status, 'stacked' as type
|
|
|
|
FROM "ItemAct"
|
|
|
|
JOIN "Item" on "ItemAct"."itemId" = "Item".id
|
|
|
|
WHERE "ItemAct"."userId" <> $1 AND "ItemAct".act <> 'BOOST'
|
|
|
|
AND "Item"."userId" = $1 AND "ItemAct".created_at <= $2
|
|
|
|
GROUP BY "Item".id)`)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (include.has('spent')) {
|
|
|
|
queries.push(
|
|
|
|
`(SELECT ('spent' || "Item".id) as id, "Item".id as "factId", NULL as bolt11,
|
|
|
|
MAX("ItemAct".created_at) as "createdAt", sum("ItemAct".sats) * 1000 as msats,
|
|
|
|
0 as "msatsFee", NULL as status, 'spent' as type
|
|
|
|
FROM "ItemAct"
|
|
|
|
JOIN "Item" on "ItemAct"."itemId" = "Item".id
|
|
|
|
WHERE "ItemAct"."userId" = $1
|
|
|
|
AND "ItemAct".created_at <= $2
|
|
|
|
GROUP BY "Item".id)`)
|
|
|
|
}
|
2021-12-14 16:42:54 +00:00
|
|
|
|
2021-12-16 17:27:12 +00:00
|
|
|
if (queries.length === 0) {
|
|
|
|
return {
|
|
|
|
cursor: null,
|
|
|
|
facts: []
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-15 16:50:11 +00:00
|
|
|
let history = await models.$queryRaw(`
|
2021-12-16 17:27:12 +00:00
|
|
|
${queries.join(' UNION ALL ')}
|
2021-12-14 16:42:54 +00:00
|
|
|
ORDER BY "createdAt" DESC
|
|
|
|
OFFSET $3
|
2021-12-15 16:50:11 +00:00
|
|
|
LIMIT ${LIMIT}`, me.id, decodedCursor.time, decodedCursor.offset)
|
|
|
|
|
|
|
|
history = history.map(f => {
|
|
|
|
if (f.bolt11) {
|
|
|
|
const inv = lnpr.decode(f.bolt11)
|
|
|
|
if (inv) {
|
|
|
|
const { tags } = inv
|
|
|
|
for (const tag of tags) {
|
|
|
|
if (tag.tagName === 'description') {
|
|
|
|
f.description = tag.data
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
switch (f.type) {
|
|
|
|
case 'withdrawal':
|
2021-12-16 20:02:17 +00:00
|
|
|
f.msats = (-1 * f.msats) - f.msatsFee
|
|
|
|
break
|
|
|
|
case 'spent':
|
2021-12-15 16:50:11 +00:00
|
|
|
f.msats *= -1
|
|
|
|
break
|
|
|
|
default:
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
return f
|
|
|
|
})
|
2021-12-14 16:42:54 +00:00
|
|
|
|
|
|
|
return {
|
|
|
|
cursor: history.length === LIMIT ? nextCursorEncoded(decodedCursor) : null,
|
|
|
|
facts: history
|
|
|
|
}
|
2021-04-30 21:42:51 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
Mutation: {
|
2021-05-06 21:15:22 +00:00
|
|
|
createInvoice: async (parent, { amount }, { me, models, lnd }) => {
|
2021-05-11 15:52:50 +00:00
|
|
|
if (!me) {
|
2021-05-20 01:09:32 +00:00
|
|
|
throw new AuthenticationError('you must be logged in')
|
2021-05-11 15:52:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!amount || amount <= 0) {
|
2021-05-20 01:09:32 +00:00
|
|
|
throw new UserInputError('amount must be positive', { argumentName: 'amount' })
|
2021-05-11 15:52:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// set expires at to 3 hours into future
|
|
|
|
const expiresAt = new Date(new Date().setHours(new Date().getHours() + 3))
|
|
|
|
const description = `${amount} sats for @${me.name} on stacker.news`
|
2021-06-09 20:07:32 +00:00
|
|
|
try {
|
|
|
|
const invoice = await createInvoice({
|
|
|
|
description,
|
|
|
|
lnd,
|
|
|
|
tokens: amount,
|
|
|
|
expires_at: expiresAt
|
|
|
|
})
|
2021-05-11 15:52:50 +00:00
|
|
|
|
2022-01-05 20:37:34 +00:00
|
|
|
const [inv] = await serialize(models,
|
|
|
|
models.$queryRaw`SELECT * FROM create_invoice(${invoice.id}, ${invoice.request},
|
|
|
|
${expiresAt}, ${amount * 1000}, ${me.id})`)
|
2021-05-11 15:52:50 +00:00
|
|
|
|
2022-01-05 20:37:34 +00:00
|
|
|
return inv
|
2021-06-09 20:07:32 +00:00
|
|
|
} catch (error) {
|
2021-06-10 20:54:22 +00:00
|
|
|
console.log(error)
|
2021-06-09 20:07:32 +00:00
|
|
|
throw error
|
|
|
|
}
|
2021-05-12 23:04:19 +00:00
|
|
|
},
|
|
|
|
createWithdrawl: async (parent, { invoice, maxFee }, { me, models, lnd }) => {
|
|
|
|
// decode invoice to get amount
|
2021-05-25 00:08:56 +00:00
|
|
|
let decoded
|
|
|
|
try {
|
|
|
|
decoded = await decodePaymentRequest({ lnd, request: invoice })
|
|
|
|
} catch (error) {
|
|
|
|
throw new UserInputError('could not decode invoice')
|
|
|
|
}
|
2021-05-12 23:04:19 +00:00
|
|
|
|
2021-08-12 23:25:19 +00:00
|
|
|
if (!decoded.mtokens || Number(decoded.mtokens) <= 0) {
|
|
|
|
throw new UserInputError('you must specify amount')
|
|
|
|
}
|
|
|
|
|
2021-05-13 21:19:51 +00:00
|
|
|
const msatsFee = Number(maxFee) * 1000
|
2021-05-20 01:09:32 +00:00
|
|
|
|
2021-05-12 23:04:19 +00:00
|
|
|
// create withdrawl transactionally (id, bolt11, amount, fee)
|
2021-05-20 01:09:32 +00:00
|
|
|
const [withdrawl] = await serialize(models,
|
|
|
|
models.$queryRaw`SELECT * FROM create_withdrawl(${decoded.id}, ${invoice},
|
|
|
|
${Number(decoded.mtokens)}, ${msatsFee}, ${me.name})`)
|
|
|
|
|
2022-01-05 20:37:34 +00:00
|
|
|
payViaPaymentRequest({
|
2021-05-20 01:09:32 +00:00
|
|
|
lnd,
|
|
|
|
request: invoice,
|
|
|
|
// can't use max_fee_mtokens https://github.com/alexbosworth/ln-service/issues/141
|
|
|
|
max_fee: Number(maxFee),
|
|
|
|
pathfinding_timeout: 30000
|
|
|
|
})
|
|
|
|
|
|
|
|
return withdrawl
|
2021-04-30 21:42:51 +00:00
|
|
|
}
|
2021-05-13 21:19:51 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
Withdrawl: {
|
|
|
|
satsPaying: w => Math.floor(w.msatsPaying / 1000),
|
|
|
|
satsPaid: w => Math.floor(w.msatsPaid / 1000),
|
|
|
|
satsFeePaying: w => Math.floor(w.msatsFeePaying / 1000),
|
|
|
|
satsFeePaid: w => Math.floor(w.msatsFeePaid / 1000)
|
2021-12-16 20:02:17 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
Fact: {
|
|
|
|
item: async (fact, args, { models }) => {
|
|
|
|
if (fact.type !== 'spent' && fact.type !== 'stacked') {
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
const [item] = await models.$queryRaw(`
|
|
|
|
${SELECT}
|
|
|
|
FROM "Item"
|
|
|
|
WHERE id = $1`, Number(fact.factId))
|
|
|
|
|
|
|
|
return item
|
|
|
|
}
|
2021-04-30 21:42:51 +00:00
|
|
|
}
|
|
|
|
}
|