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) } })
},
withdrawl: async (parent, { id }, { me, models, lnd }) => {
console.log(models)
return await models.withdrawl.findUnique({ where: { id: Number(id) } })
}
},
@ -54,30 +53,64 @@ export default {
// decode invoice to get amount
const decoded = await decodePaymentRequest({ lnd, request: invoice })
const msatsFee = Number(maxFee) * 1000
// create withdrawl transactionally (id, bolt11, amount, fee)
const [withdrawl] =
try {
const [withdrawl] =
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
const sub = subscribeToPayViaRequest({
lnd,
request: invoice,
max_fee_mtokens: maxFee,
pathfinding_timeout: 30000
})
// 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
sub.on('confirmed', console.log)
// if it's confirmed, update confirmed returning extra fees to user
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
// 1. transactionally return the funds to the user
// 2. transactionally update the widthdrawl as failed
sub.on('failed', console.log)
// if the payment fails, we need to
// 1. return the funds to the user
// 2. update the widthdrawl as failed
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
// for each payment that hasn't failed or succeede
return withdrawl
return withdrawl
} catch (error) {
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!
bolt11: String!
msatsPaying: Int!
satsPaying: Int!
msatsPaid: Int
satsPaid: Int
msatsFeePaying: Int!
satsFeePaying: Int!
msatsFeePaid: Int
satsFeePaid: Int
status: String
}
`

View File

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

View File

@ -1,6 +1,6 @@
import Moon from '../svgs/moon-fill.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 }) {
return (
@ -23,7 +23,7 @@ function InvoiceConfirmedStatus ({ status }) {
function InvoiceFailedStatus ({ status }) {
return (
<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>
)

View File

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

View File

@ -18,7 +18,9 @@ export default function Withdrawl ({ id }) {
{
withdrawl(id: ${id}) {
bolt11
msatsFeePaying
satsPaid
satsFeePaying
satsFeePaid
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 }) {
const { loading, error, data } = useQuery(query, { pollInterval: 1000 })
if (error) return <div>error</div>
if (!data || loading) {
return (
<>
<div className='w-100'>
<InputSkeleton label='invoice' />
</div>
<div className='w-100'>
<InputSkeleton label='max fee' />
</div>
<InvoiceStatus status='pending' />
</>
)
return <WithdrawlSkeleton status='loading' />
}
let status = 'pending'
let variant = 'default'
switch (data.withdrawl.status) {
case 'CONFIRMED':
status = `sent ${data.withdrawl.satsPaid} sats with ${data.withdrawl.satsFeePaid} sats in routing fees`
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 (
@ -57,11 +90,11 @@ function LoadWithdrawl ({ query }) {
<div className='w-100'>
<Input
label='max fee' type='text'
placeholder={data.withdrawl.msatsFeePaying} readOnly
append={<InputGroup.Text className='text-monospace'>millisats</InputGroup.Text>}
placeholder={data.withdrawl.satsFeePaying} readOnly
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
/>
</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
INSERT INTO "Vote" (sats, "itemId", "userId", boost, updated_at) VALUES (vote_sats, item_id, user_id, true, 'now');
ELSE
INSERT INTO "Vote" (sats, "itemId", "userId", updated_at) VALUES (vote_sats, item_id, user_id, 'now');
UPDATE users SET msats = msats + (vote_sats * 1000) WHERE id = (SELECT "userId" FROM "Item" WHERE id = item_id);
INSERT INTO "Vote" (sats, "itemId", "userId", updated_at) VALUES (1, item_id, user_id, 'now');
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;
RETURN vote_sats;

View File

@ -21,24 +21,38 @@ BEGIN
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
LANGUAGE plpgsql
AS $$
DECLARE
msats_fee_paying INTEGER;
user_id INTEGER;
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;
$$;
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
LANGUAGE plpgsql
AS $$
DECLARE
msats_fee_paying INTEGER;
msats_paying INTEGER;
user_id INTEGER;
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;
$$;

View File

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

View File

@ -88,10 +88,12 @@ model Invoice {
}
enum WithdrawlStatus {
CONFIRMED
INSUFFICIENT_BALANCE
INVALID_PAYMENT
PATHFINDING_TIMEOUT
ROUTE_NOT_FOUND
UNKNOWN_FAILURE
}
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