make withdrawls mostly work

This commit is contained in:
keyan 2021-05-13 16:19:51 -05:00
parent ce55fdfe9c
commit 157488ea5d
12 changed files with 156 additions and 52 deletions

View File

@ -7,7 +7,6 @@ export default {
return await models.invoice.findUnique({ where: { id: Number(id) } }) return await models.invoice.findUnique({ where: { id: Number(id) } })
}, },
withdrawl: async (parent, { id }, { me, models, lnd }) => { withdrawl: async (parent, { id }, { me, models, lnd }) => {
console.log(models)
return await models.withdrawl.findUnique({ where: { id: Number(id) } }) return await models.withdrawl.findUnique({ where: { id: Number(id) } })
} }
}, },
@ -54,30 +53,64 @@ export default {
// decode invoice to get amount // decode invoice to get amount
const decoded = await decodePaymentRequest({ lnd, request: invoice }) const decoded = await decodePaymentRequest({ lnd, request: invoice })
const msatsFee = Number(maxFee) * 1000
// create withdrawl transactionally (id, bolt11, amount, fee) // create withdrawl transactionally (id, bolt11, amount, fee)
const [withdrawl] = try {
const [withdrawl] =
await models.$queryRaw`SELECT * FROM create_withdrawl(${decoded.id}, ${invoice}, await models.$queryRaw`SELECT * FROM create_withdrawl(${decoded.id}, ${invoice},
${Number(decoded.mtokens)}, ${Number(maxFee)}, ${me.name})` ${Number(decoded.mtokens)}, ${msatsFee}, ${me.name})`
// create the payment, subscribing to its status // create the payment, subscribing to its status
const sub = subscribeToPayViaRequest({ const sub = subscribeToPayViaRequest({
lnd, lnd,
request: invoice, request: invoice,
max_fee_mtokens: maxFee, // can't use max_fee_mtokens https://github.com/alexbosworth/ln-service/issues/141
pathfinding_timeout: 30000 max_fee: Number(maxFee),
}) pathfinding_timeout: 30000
})
// if it's confirmed, update confirmed // if it's confirmed, update confirmed returning extra fees to user
sub.on('confirmed', console.log) sub.on('confirmed', async e => {
console.log(e)
// mtokens also contains the fee
const fee = Number(e.fee_mtokens)
const paid = Number(e.mtokens) - fee
await models.$queryRaw`SELECT confirm_withdrawl(${withdrawl.id}, ${paid}, ${fee})`
})
// if the payment fails, we need to // if the payment fails, we need to
// 1. transactionally return the funds to the user // 1. return the funds to the user
// 2. transactionally update the widthdrawl as failed // 2. update the widthdrawl as failed
sub.on('failed', console.log) sub.on('failed', async e => {
console.log(e)
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'
}
await models.$queryRaw`SELECT reverse_withdrawl(${withdrawl.id}, ${status})`
})
// in walletd return withdrawl
// for each payment that hasn't failed or succeede } catch (error) {
return withdrawl const { meta: { message } } = error
if (message.includes('SN_INSUFFICIENT_FUNDS')) {
throw new UserInputError('insufficient funds')
}
throw error
}
} }
},
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)
} }
} }

View File

@ -27,9 +27,13 @@ export default gql`
hash: String! hash: String!
bolt11: String! bolt11: String!
msatsPaying: Int! msatsPaying: Int!
satsPaying: Int!
msatsPaid: Int msatsPaid: Int
satsPaid: Int
msatsFeePaying: Int! msatsFeePaying: Int!
satsFeePaying: Int!
msatsFeePaid: Int msatsFeePaid: Int
satsFeePaid: Int
status: String status: String
} }
` `

View File

@ -83,9 +83,9 @@ export function Input ({ label, prepend, append, hint, ...props }) {
} }
export function Form ({ export function Form ({
initial, schema, onSubmit, children, ...props initial, schema, onSubmit, children, initialError, ...props
}) { }) {
const [error, setError] = useState() const [error, setError] = useState(initialError)
return ( return (
<Formik <Formik

View File

@ -1,6 +1,6 @@
import Moon from '../svgs/moon-fill.svg' import Moon from '../svgs/moon-fill.svg'
import Check from '../svgs/check-double-line.svg' import Check from '../svgs/check-double-line.svg'
import Fail from '../svgs/close-line.svg' import ThumbDown from '../svgs/thumb-down-fill.svg'
function InvoiceDefaultStatus ({ status }) { function InvoiceDefaultStatus ({ status }) {
return ( return (
@ -23,7 +23,7 @@ function InvoiceConfirmedStatus ({ status }) {
function InvoiceFailedStatus ({ status }) { function InvoiceFailedStatus ({ status }) {
return ( return (
<div className='d-flex mt-2'> <div className='d-flex mt-2'>
<Fail className='fill-danger' /> <ThumbDown className='fill-danger' />
<div className='ml-3 text-danger' style={{ fontWeight: '600' }}>{status}</div> <div className='ml-3 text-danger' style={{ fontWeight: '600' }}>{status}</div>
</div> </div>
) )

View File

@ -60,3 +60,8 @@ async function checkPending () {
} }
checkPending() checkPending()
// TODO
// in walletd
// for each payment that hasn't failed or succeeded after 30 seconds after creation
// request status from lnd and record

View File

@ -7,6 +7,7 @@ import { gql, useMutation, useQuery } from '@apollo/client'
import { InvoiceSkeleton } from '../components/invoice' import { InvoiceSkeleton } from '../components/invoice'
import LayoutCenter from '../components/layout-center' import LayoutCenter from '../components/layout-center'
import InputGroup from 'react-bootstrap/InputGroup' import InputGroup from 'react-bootstrap/InputGroup'
import { WithdrawlSkeleton } from './withdrawls/[id]'
export default function Wallet () { export default function Wallet () {
return ( return (
@ -47,14 +48,14 @@ export const FundSchema = Yup.object({
export function FundForm () { export function FundForm () {
const router = useRouter() const router = useRouter()
const [createInvoice, { called }] = useMutation(gql` const [createInvoice, { called, error }] = useMutation(gql`
mutation createInvoice($amount: Int!) { mutation createInvoice($amount: Int!) {
createInvoice(amount: $amount) { createInvoice(amount: $amount) {
id id
} }
}`) }`)
if (called) { if (called && !error) {
return <InvoiceSkeleton status='generating' /> return <InvoiceSkeleton status='generating' />
} }
@ -92,22 +93,26 @@ export function WithdrawlForm () {
const query = gql` const query = gql`
{ {
me { me {
msats sats
} }
}` }`
const { data } = useQuery(query, { pollInterval: 1000 }) const { data } = useQuery(query, { pollInterval: 1000 })
const [createWithdrawl] = useMutation(gql` const [createWithdrawl, { called, error }] = useMutation(gql`
mutation createWithdrawl($invoice: String!, $maxFee: Int!) { mutation createWithdrawl($invoice: String!, $maxFee: Int!) {
createWithdrawl(invoice: $invoice, maxFee: $maxFee) { createWithdrawl(invoice: $invoice, maxFee: $maxFee) {
id id
} }
}`) }`)
if (called && !error) {
return <WithdrawlSkeleton status='sending' />
}
return ( return (
<> <>
<h2 className={`${data ?? 'invisible'} text-success pb-5`}> <h2 className={`${data ? 'visible' : 'invisible'} text-success pb-5`}>
you have <span className='text-monospace'>{data && data.me.msats}</span> millisats you have <span className='text-monospace'>{data && data.me.sats}</span> sats
</h2> </h2>
<Form <Form
className='pt-3' className='pt-3'
@ -115,8 +120,10 @@ export function WithdrawlForm () {
invoice: '', invoice: '',
maxFee: 0 maxFee: 0
}} }}
initialError={error ? error.toString() : undefined}
schema={WithdrawlSchema} schema={WithdrawlSchema}
onSubmit={async ({ invoice, maxFee }) => { onSubmit={async ({ invoice, maxFee }) => {
console.log('calling')
const { data } = await createWithdrawl({ variables: { invoice, maxFee: Number(maxFee) } }) const { data } = await createWithdrawl({ variables: { invoice, maxFee: Number(maxFee) } })
router.push(`/withdrawls/${data.createWithdrawl.id}`) router.push(`/withdrawls/${data.createWithdrawl.id}`)
}} }}
@ -131,7 +138,7 @@ export function WithdrawlForm () {
label='max fee' label='max fee'
name='maxFee' name='maxFee'
required required
append={<InputGroup.Text className='text-monospace'>millisats</InputGroup.Text>} append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
/> />
<SubmitButton variant='success' className='mt-2'>withdrawl</SubmitButton> <SubmitButton variant='success' className='mt-2'>withdrawl</SubmitButton>
</Form> </Form>

View File

@ -18,7 +18,9 @@ export default function Withdrawl ({ id }) {
{ {
withdrawl(id: ${id}) { withdrawl(id: ${id}) {
bolt11 bolt11
msatsFeePaying satsPaid
satsFeePaying
satsFeePaid
status status
} }
}` }`
@ -29,21 +31,52 @@ export default function Withdrawl ({ id }) {
) )
} }
export function WithdrawlSkeleton ({ status }) {
return (
<>
<div className='w-100'>
<InputSkeleton label='invoice' />
</div>
<div className='w-100'>
<InputSkeleton label='max fee' />
</div>
<InvoiceStatus status={status} />
</>
)
}
function LoadWithdrawl ({ query }) { function LoadWithdrawl ({ query }) {
const { loading, error, data } = useQuery(query, { pollInterval: 1000 }) const { loading, error, data } = useQuery(query, { pollInterval: 1000 })
if (error) return <div>error</div> if (error) return <div>error</div>
if (!data || loading) { if (!data || loading) {
return ( return <WithdrawlSkeleton status='loading' />
<> }
<div className='w-100'>
<InputSkeleton label='invoice' /> let status = 'pending'
</div> let variant = 'default'
<div className='w-100'> switch (data.withdrawl.status) {
<InputSkeleton label='max fee' /> case 'CONFIRMED':
</div> status = `sent ${data.withdrawl.satsPaid} sats with ${data.withdrawl.satsFeePaid} sats in routing fees`
<InvoiceStatus status='pending' /> variant = 'confirmed'
</> break
) case 'INSUFFICIENT_BALANCE':
status = <>insufficient balance <small className='ml-3'>contact keyan!</small></>
variant = 'failed'
break
case 'INVALID_PAYMENT':
status = 'invalid invoice'
variant = 'failed'
break
case 'PATHFINDING_TIMEOUT':
status = <>timed out trying to find route <small className='ml-3'>try increasing max fee</small></>
variant = 'failed'
break
case 'ROUTE_NOT_FOUND':
status = <>could not find route <small className='ml-3'>try increasing max fee</small></>
variant = 'failed'
break
default:
break
} }
return ( return (
@ -57,11 +90,11 @@ function LoadWithdrawl ({ query }) {
<div className='w-100'> <div className='w-100'>
<Input <Input
label='max fee' type='text' label='max fee' type='text'
placeholder={data.withdrawl.msatsFeePaying} readOnly placeholder={data.withdrawl.satsFeePaying} readOnly
append={<InputGroup.Text className='text-monospace'>millisats</InputGroup.Text>} append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
/> />
</div> </div>
<InvoiceStatus status='pending' /> <InvoiceStatus variant={variant} status={status} />
</> </>
) )
} }

View File

@ -18,8 +18,11 @@ BEGIN
IF EXISTS (SELECT 1 FROM "Vote" WHERE "itemId" = item_id AND "userId" = user_id) THEN IF EXISTS (SELECT 1 FROM "Vote" WHERE "itemId" = item_id AND "userId" = user_id) THEN
INSERT INTO "Vote" (sats, "itemId", "userId", boost, updated_at) VALUES (vote_sats, item_id, user_id, true, 'now'); INSERT INTO "Vote" (sats, "itemId", "userId", boost, updated_at) VALUES (vote_sats, item_id, user_id, true, 'now');
ELSE ELSE
INSERT INTO "Vote" (sats, "itemId", "userId", updated_at) VALUES (vote_sats, item_id, user_id, 'now'); INSERT INTO "Vote" (sats, "itemId", "userId", updated_at) VALUES (1, item_id, user_id, 'now');
UPDATE users SET msats = msats + (vote_sats * 1000) WHERE id = (SELECT "userId" FROM "Item" WHERE id = item_id); UPDATE users SET msats = msats + 1000 WHERE id = (SELECT "userId" FROM "Item" WHERE id = item_id);
IF vote_sats > 1 THEN
INSERT INTO "Vote" (sats, "itemId", "userId", boost, updated_at) VALUES (vote_sats - 1, item_id, user_id, true, 'now');
END IF;
END IF; END IF;
RETURN vote_sats; RETURN vote_sats;

View File

@ -21,24 +21,38 @@ BEGIN
END; END;
$$; $$;
CREATE OR REPLACE FUNCTION confirm_withdrawl(lnd_id TEXT, msats_paid INTEGER, msats_fee_paid INTEGER) CREATE OR REPLACE FUNCTION confirm_withdrawl(wid INTEGER, msats_paid INTEGER, msats_fee_paid INTEGER)
RETURNS INTEGER RETURNS INTEGER
LANGUAGE plpgsql LANGUAGE plpgsql
AS $$ AS $$
DECLARE DECLARE
msats_fee_paying INTEGER;
user_id INTEGER;
BEGIN BEGIN
IF EXISTS (SELECT 1 FROM "Withdrawl" WHERE id = wid AND status IS NULL) THEN
UPDATE "Withdrawl" SET status = 'CONFIRMED', "msatsPaid" = msats_paid, "msatsFeePaid" = msats_fee_paid WHERE id = wid;
SELECT "msatsFeePaying", "userId" INTO msats_fee_paying, user_id FROM "Withdrawl" WHERE id = wid;
UPDATE users SET msats = msats + (msats_fee_paying - msats_fee_paid) WHERE id = user_id;
END IF;
RETURN 0;
END; END;
$$; $$;
CREATE OR REPLACE FUNCTION reverse_withdrawl(lnd_id TEXT, msats_paid INTEGER, msats_fee_paid INTEGER) CREATE OR REPLACE FUNCTION reverse_withdrawl(wid INTEGER, wstatus "WithdrawlStatus")
RETURNS INTEGER RETURNS INTEGER
LANGUAGE plpgsql LANGUAGE plpgsql
AS $$ AS $$
DECLARE DECLARE
msats_fee_paying INTEGER;
msats_paying INTEGER;
user_id INTEGER;
BEGIN BEGIN
IF EXISTS (SELECT 1 FROM "Withdrawl" WHERE id = wid AND status IS NULL) THEN
UPDATE "Withdrawl" SET status = wstatus WHERE id = wid;
SELECT "msatsPaying", "msatsFeePaying", "userId" INTO msats_paying, msats_fee_paying, user_id FROM "Withdrawl" WHERE id = wid;
UPDATE users SET msats = msats + msats_paying + msats_fee_paying WHERE id = user_id;
END IF;
RETURN 0;
END; END;
$$; $$;

View File

@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "WithdrawlStatus" ADD VALUE 'CONFIRMED';

View File

@ -88,10 +88,12 @@ model Invoice {
} }
enum WithdrawlStatus { enum WithdrawlStatus {
CONFIRMED
INSUFFICIENT_BALANCE INSUFFICIENT_BALANCE
INVALID_PAYMENT INVALID_PAYMENT
PATHFINDING_TIMEOUT PATHFINDING_TIMEOUT
ROUTE_NOT_FOUND ROUTE_NOT_FOUND
UNKNOWN_FAILURE
} }
model Withdrawl { model Withdrawl {

1
svgs/thumb-down-fill.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M22 15h-3V3h3a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1zm-5.293 1.293l-6.4 6.4a.5.5 0 0 1-.654.047L8.8 22.1a1.5 1.5 0 0 1-.553-1.57L9.4 16H3a2 2 0 0 1-2-2v-2.104a2 2 0 0 1 .15-.762L4.246 3.62A1 1 0 0 1 5.17 3H16a1 1 0 0 1 1 1v11.586a1 1 0 0 1-.293.707z"/></svg>

After

Width:  |  Height:  |  Size: 379 B