stacker.news/api/resolvers/wallet.js

213 lines
6.4 KiB
JavaScript
Raw Normal View History

2021-06-03 22:08:00 +00:00
import { createInvoice, decodePaymentRequest, subscribeToPayViaRequest } 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'
import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
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
},
walletHistory: async (parent, { cursor }, { me, models, lnd }) => {
const decodedCursor = decodeCursor(cursor)
// if (!me) {
// throw new AuthenticationError('you must be logged in')
// }
2021-11-30 15:23:26 +00:00
// TODO
// 1. union invoices and withdrawals
// 2. add to union spending and receiving
const history = await models.$queryRaw(`
(SELECT id, bolt11, created_at as "createdAt",
"msatsReceived" as msats, NULL as "msatsFee",
CASE WHEN "confirmedAt" IS NOT NULL THEN 'CONFIRMED'
WHEN "expiresAt" IS NOT NULL THEN 'EXPIRED'
WHEN cancelled THEN 'CANCELLED'
ELSE 'PENDING' END as status,
'invoice' as type
FROM "Invoice"
WHERE "userId" = $1
AND created_at <= $2
ORDER BY created_at desc
LIMIT ${LIMIT}+$3)
UNION ALL
(SELECT id, 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
ORDER BY created_at desc
LIMIT ${LIMIT}+$3)
ORDER BY "createdAt" DESC
OFFSET $3
LIMIT ${LIMIT}`, 624, decodedCursor.time, decodedCursor.offset)
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
2021-06-09 20:07:32 +00:00
const data = {
hash: invoice.id,
bolt11: invoice.request,
expiresAt: expiresAt,
msatsRequested: amount * 1000,
user: {
connect: {
2021-06-27 03:09:39 +00:00
id: me.id
2021-06-09 20:07:32 +00:00
}
2021-05-11 15:52:50 +00:00
}
}
2021-06-09 20:07:32 +00:00
return await models.invoice.create({ data })
} 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
// TODO: test
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})`)
// create the payment, subscribing to its status
const sub = subscribeToPayViaRequest({
lnd,
request: invoice,
// can't use max_fee_mtokens https://github.com/alexbosworth/ln-service/issues/141
max_fee: Number(maxFee),
pathfinding_timeout: 30000
})
// if it's confirmed, update confirmed returning extra fees to user
2021-08-19 19:53:38 +00:00
sub.once('confirmed', async e => {
2021-05-20 01:09:32 +00:00
console.log(e)
2021-08-19 19:53:38 +00:00
sub.removeAllListeners()
2021-05-20 01:09:32 +00:00
// mtokens also contains the fee
const fee = Number(e.fee_mtokens)
const paid = Number(e.mtokens) - fee
await serialize(models, models.$queryRaw`
SELECT confirm_withdrawl(${withdrawl.id}, ${paid}, ${fee})`)
})
// if the payment fails, we need to
// 1. return the funds to the user
// 2. update the widthdrawl as failed
2021-08-19 19:53:38 +00:00
sub.once('failed', async e => {
2021-05-20 01:09:32 +00:00
console.log(e)
2021-08-19 19:53:38 +00:00
sub.removeAllListeners()
2021-05-20 01:09:32 +00:00
let status = 'UNKNOWN_FAILURE'
if (e.is_insufficient_balance) {
status = 'INSUFFICIENT_BALANCE'
} else if (e.is_invalid_payment) {
status = 'INVALID_PAYMENT'
} else if (e.is_pathfinding_timeout) {
status = 'PATHFINDING_TIMEOUT'
} else if (e.is_route_not_found) {
status = 'ROUTE_NOT_FOUND'
2021-05-13 21:19:51 +00:00
}
2021-05-20 01:09:32 +00:00
await serialize(models, models.$queryRaw`
SELECT reverse_withdrawl(${withdrawl.id}, ${status})`)
})
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-04-30 21:42:51 +00:00
}
}