make withdrawls mostly work
This commit is contained in:
parent
ce55fdfe9c
commit
157488ea5d
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
$$;
|
$$;
|
2
prisma/migrations/20210513191840_enum/migration.sql
Normal file
2
prisma/migrations/20210513191840_enum/migration.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterEnum
|
||||||
|
ALTER TYPE "WithdrawlStatus" ADD VALUE 'CONFIRMED';
|
@ -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
1
svgs/thumb-down-fill.svg
Normal 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 |
Loading…
x
Reference in New Issue
Block a user