stacker.news/api/resolvers/wallet.js

318 lines
9.7 KiB
JavaScript
Raw Normal View History

2022-01-05 20:37:34 +00:00
import { createInvoice, decodePaymentRequest, payViaPaymentRequest } from 'ln-service'
2022-08-30 20:33:39 +00:00
import { UserInputError, AuthenticationError } from 'apollo-server-micro'
2021-05-20 01:09:32 +00:00
import serialize from './serial'
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'
import { lnurlPayDescriptionHash } from '../../lib/lnurl'
2022-11-15 20:51:55 +00:00
import { msatsToSats } from '../../lib/format'
2021-05-11 15:52:50 +00:00
2022-03-23 18:54:39 +00:00
export async function getInvoice (parent, { id }, { me, models }) {
if (!me) {
throw new AuthenticationError('you must be logged in')
}
2021-05-20 01:09:32 +00:00
2022-03-23 18:54:39 +00:00
const inv = await models.invoice.findUnique({
where: {
id: Number(id)
},
include: {
user: true
}
})
2021-05-20 01:09:32 +00:00
2022-03-23 18:54:39 +00:00
if (inv.user.id !== me.id) {
throw new AuthenticationError('not ur invoice')
}
2021-05-20 01:09:32 +00:00
2022-03-23 18:54:39 +00:00
return inv
}
export default {
Query: {
invoice: getInvoice,
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,
2022-11-15 20:51:55 +00:00
MAX("ItemAct".created_at) as "createdAt", sum("ItemAct".msats) as msats,
2021-12-16 20:02:17 +00:00
0 as "msatsFee", NULL as status, 'stacked' as type
FROM "ItemAct"
JOIN "Item" on "ItemAct"."itemId" = "Item".id
2022-11-23 18:12:09 +00:00
WHERE act = 'TIP'
2022-04-19 18:32:39 +00:00
AND (("Item"."userId" = $1 AND "Item"."fwdUserId" IS NULL)
OR ("Item"."fwdUserId" = $1 AND "ItemAct"."userId" <> "Item"."userId"))
AND "ItemAct".created_at <= $2
2021-12-16 20:02:17 +00:00
GROUP BY "Item".id)`)
2022-03-17 20:13:19 +00:00
queries.push(
`(SELECT ('earn' || min("Earn".id)) as id, min("Earn".id) as "factId", NULL as bolt11,
created_at as "createdAt", sum(msats),
2022-03-17 20:13:19 +00:00
0 as "msatsFee", NULL as status, 'earn' as type
FROM "Earn"
WHERE "Earn"."userId" = $1 AND "Earn".created_at <= $2
GROUP BY "userId", created_at)`)
2021-12-16 20:02:17 +00:00
}
if (include.has('spent')) {
queries.push(
`(SELECT ('spent' || "Item".id) as id, "Item".id as "factId", NULL as bolt11,
2022-11-15 20:51:55 +00:00
MAX("ItemAct".created_at) as "createdAt", sum("ItemAct".msats) as msats,
2021-12-16 20:02:17 +00:00
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-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 ')}
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
})
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
}
const user = await models.user.findUnique({ where: { id: me.id } })
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 @${user.name} on stacker.news`
2021-06-09 20:07:32 +00:00
try {
const invoice = await createInvoice({
2022-08-30 21:50:47 +00:00
description: user.hideInvoiceDesc ? undefined : description,
2021-06-09 20:07:32 +00:00
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
},
2022-01-23 17:21:55 +00:00
createWithdrawl: createWithdrawal,
sendToLnAddr: async (parent, { addr, amount, maxFee }, { me, models, lnd }) => {
const [name, domain] = addr.split('@')
let req
try {
req = await fetch(`https://${domain}/.well-known/lnurlp/${name}`)
} catch (e) {
throw new Error(`error initiating protocol with https://${domain}`)
}
const res1 = await req.json()
2022-01-23 17:21:55 +00:00
if (res1.status === 'ERROR') {
throw new Error(res1.reason)
2021-05-25 00:08:56 +00:00
}
2021-05-12 23:04:19 +00:00
2022-01-23 17:21:55 +00:00
const milliamount = amount * 1000
// check that amount is within min and max sendable
if (milliamount < res1.minSendable || milliamount > res1.maxSendable) {
throw new UserInputError(
`amount must be >= ${res1.minSendable / 1000} and <= ${res1.maxSendable / 1000}`,
{ argumentName: 'amount' })
2021-08-12 23:25:19 +00:00
}
const callback = new URL(res1.callback)
callback.searchParams.append('amount', milliamount)
2022-01-23 17:21:55 +00:00
// call callback with amount
const res2 = await (await fetch(callback.toString())).json()
2022-01-23 17:21:55 +00:00
if (res2.status === 'ERROR') {
throw new Error(res2.reason)
}
2021-05-20 01:09:32 +00:00
// decode invoice
let decoded
try {
decoded = await decodePaymentRequest({ lnd, request: res2.pr })
} catch (error) {
console.log(error)
throw new Error('could not decode invoice')
}
if (decoded.description_hash !== lnurlPayDescriptionHash(res1.metadata)) {
throw new Error('description hash does not match')
}
2022-01-23 17:21:55 +00:00
// take pr and createWithdrawl
return await createWithdrawal(parent, { invoice: res2.pr, maxFee }, { me, models, lnd })
2021-04-30 21:42:51 +00:00
}
2021-05-13 21:19:51 +00:00
},
Withdrawl: {
2022-11-15 20:51:55 +00:00
satsPaying: w => msatsToSats(w.msatsPaying),
satsPaid: w => msatsToSats(w.msatsPaid),
satsFeePaying: w => msatsToSats(w.msatsFeePaying),
satsFeePaid: w => msatsToSats(w.msatsFeePaid)
},
Invoice: {
satsReceived: i => msatsToSats(i.msatsReceived)
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
2022-11-15 20:51:55 +00:00
},
sats: fact => msatsToSats(fact.msats),
satsFee: fact => msatsToSats(fact.msatsFee)
2021-04-30 21:42:51 +00:00
}
}
2022-01-23 17:21:55 +00:00
async function createWithdrawal (parent, { invoice, maxFee }, { me, models, lnd }) {
// decode invoice to get amount
let decoded
try {
decoded = await decodePaymentRequest({ lnd, request: invoice })
} catch (error) {
console.log(error)
throw new UserInputError('could not decode invoice')
}
2022-11-15 20:51:55 +00:00
if (!decoded.mtokens || BigInt(decoded.mtokens) <= 0) {
2022-05-18 18:13:13 +00:00
throw new UserInputError('your invoice must specify an amount')
2022-01-23 17:21:55 +00:00
}
const msatsFee = Number(maxFee) * 1000
const user = await models.user.findUnique({ where: { id: me.id } })
2022-01-23 17:21:55 +00:00
// create withdrawl transactionally (id, bolt11, amount, fee)
const [withdrawl] = await serialize(models,
models.$queryRaw`SELECT * FROM create_withdrawl(${decoded.id}, ${invoice},
${Number(decoded.mtokens)}, ${msatsFee}, ${user.name})`)
2022-01-23 17:21:55 +00:00
payViaPaymentRequest({
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
}