half done with wallets

This commit is contained in:
keyan 2021-05-12 18:04:19 -05:00
parent 67d1605666
commit d92fc12187
7 changed files with 141 additions and 22 deletions

View File

@ -1,4 +1,4 @@
import { createInvoice } from 'ln-service' import { createInvoice, decodePaymentRequest, payViaPaymentRequest } from 'ln-service'
import { UserInputError, AuthenticationError } from 'apollo-server-micro' import { UserInputError, AuthenticationError } from 'apollo-server-micro'
export default { export default {
@ -18,17 +18,6 @@ export default {
throw new UserInputError('Amount must be positive', { argumentName: 'amount' }) throw new UserInputError('Amount must be positive', { argumentName: 'amount' })
} }
/*
chain_address: undefined,
created_at: '2021-05-06T22:16:28.000Z',
description: 'hi there',
id: '30946d6ff432933e30f6c180cce982c92b509a80bf6c2e896e6579cbda4c1677',
mtokens: '1000',
payment: 'e3deb7a0471bf050aa5dd0ef9b546887ab1fdf0306a7cb67d9dda8473f9542f2',
request: 'lnbcrt10n1psfg64upp5xz2x6ml5x2fnuv8kcxqve6vzey44px5qhakzaztwv4uuhkjvzemsdqddp5jqargv4ex2cqzpgxqr23ssp5u00t0gz8r0c9p2ja6rhek4rgs743lhcrq6nuke7emk5yw0u4gteq9q8zqqyssq92epsvsap3pyfcj4kex5vysew4tqg6c8vxux5nfmc7yqx36l6dk49pafs62dlr92lm5ekzftl7nq6r4wvjhwydtekg6lpj0xgjm5auqpwflxyk',
secret: '82abf620f82dc9a61cf3921f77432e31d4a11e1dc066ccc177d31937c473eb30',
tokens: 1
*/
// set expires at to 3 hours into future // set expires at to 3 hours into future
const expiresAt = new Date(new Date().setHours(new Date().getHours() + 3)) const expiresAt = new Date(new Date().setHours(new Date().getHours() + 3))
const description = `${amount} sats for @${me.name} on stacker.news` const description = `${amount} sats for @${me.name} on stacker.news`
@ -47,6 +36,34 @@ export default {
} }
return await models.invoice.create({ data }) return await models.invoice.create({ data })
},
createWithdrawl: async (parent, { invoice, maxFee }, { me, models, lnd }) => {
if (!me) {
throw new AuthenticationError('You must be logged in')
}
// decode invoice to get amount
const decoded = await decodePaymentRequest({ lnd, request: invoice })
// create withdrawl transactionally (id, bolt11, amount, fee)
const withdrawl =
await models.$queryRaw`SELECT confirm_withdrawl(${decoded.id}, ${invoice},
${decoded.mtokens}, ${Number(maxFee)}, ${me.name})`
// create the payment, subscribing to its status
const sub = subscribeToPayViaRequest({ lnd, request: invoice, max_fee_mtokens: maxFee, pathfinding_timeout: 30000 })
// if it's confirmed, update confirmed
sub.on('confirmed', recordStatus)
// 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', recordStatus)
// in walletd
// for each payment that hasn't failed or succeede
return 0
} }
} }
} }

View File

@ -14,5 +14,6 @@ export default gql`
ncomments: Int! ncomments: Int!
stacked: Int! stacked: Int!
sats: Int! sats: Int!
msats: Int!
} }
` `

View File

@ -7,6 +7,7 @@ export default gql`
extend type Mutation { extend type Mutation {
createInvoice(amount: Int!): Invoice! createInvoice(amount: Int!): Invoice!
createWithdrawl(invoice: String!, maxFee: Int!): Int
} }
type Invoice { type Invoice {

View File

@ -91,7 +91,7 @@ export default function Header () {
<Nav.Item className='d-md-flex d-none'> <Nav.Item className='d-md-flex d-none'>
<Nav.Link href='https://bitcoinerjobs.co' target='_blank' className={styles.navLink}>jobs</Nav.Link> <Nav.Link href='https://bitcoinerjobs.co' target='_blank' className={styles.navLink}>jobs</Nav.Link>
</Nav.Item> </Nav.Item>
<Nav.Item style={{ fontFamily: 'monospace', opacity: '.5' }}> <Nav.Item className='text-monospace' style={{ opacity: '.5' }}>
<Price /> <Price />
</Nav.Item> </Nav.Item>
<Corner /> <Corner />

View File

@ -3,7 +3,7 @@ import { Form, Input, SubmitButton } from '../components/form'
import Link from 'next/link' import Link from 'next/link'
import Button from 'react-bootstrap/Button' import Button from 'react-bootstrap/Button'
import * as Yup from 'yup' import * as Yup from 'yup'
import { gql, useMutation } from '@apollo/client' 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'
@ -79,6 +79,56 @@ export function FundForm () {
) )
} }
export const WithdrawlSchema = Yup.object({
invoice: Yup.string().required('required'),
maxFee: Yup.number('must be a number').required('required').positive('must be positive').integer('must be whole')
})
export function WithdrawlForm () { export function WithdrawlForm () {
return <div>withdrawl</div> const query = gql`
{
me {
msats
}
}`
const { data } = useQuery(query, { pollInterval: 1000 })
const [createWithdrawl] = useMutation(gql`
mutation createWithdrawl($invoice: String!, $maxFee: Int!) {
createWithdrawl(invoice: $invoice, maxFee: $maxFee)
}`)
return (
<>
<h2 className={`${data ?? 'invisible'} text-success pb-5`}>
you have <span className='text-monospace'>{data && data.me.msats}</span> millisats
</h2>
<Form
className='pt-3'
initial={{
destination: '',
maxFee: 0,
amount: 0
}}
schema={WithdrawlSchema}
onSubmit={async ({ invoice, maxFee }) => {
await createWithdrawl({ variables: { invoice, maxFee: Number(maxFee) } })
}}
>
<Input
label='invoice'
name='invoice'
required
autoFocus
/>
<Input
label='max fee'
name='maxFee'
required
append='millisats'
/>
<SubmitButton variant='success' className='mt-2'>withdrawl</SubmitButton>
</Form>
</>
)
} }

View File

@ -66,3 +66,26 @@ BEGIN
RETURN 0; RETURN 0;
END; END;
$$; $$;
CREATE OR REPLACE FUNCTION create_withdrawl(lnd_id TEXT, bolt11 TEXT, msats_amount INTEGER, msats_max_fee INTEGER, username TEXT)
RETURNS INTEGER
LANGUAGE plpgsql
AS $$
DECLARE
user_id INTEGER;
user_msats INTEGER;
withdrawl "Withdrawl";
BEGIN
SELECT msats, id INTO user_msats, user_id FROM users WHERE name = username;
IF msats_amount + msats_max_fee > user_msats THEN
RAISE EXCEPTION 'SN_INSUFFICIENT_FUNDS';
END IF;
INSERT INTO "Withdrawl" (hash, bolt11, "msatsPaying", "msatsFeePaying", "userId", updated_at)
VALUES (lnd_id, bolt11, msats_amount, msats_max_fee, user_id, 'now') RETURNING * INTO withdrawl;
UPDATE users SET msats = msats - msats_amount - msats_max_fee WHERE id = user_id;
RETURN withdrawl;
END;
$$;

View File

@ -11,18 +11,19 @@ generator client {
} }
model User { model User {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map(name: "created_at") createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @updatedAt @map(name: "updated_at") updatedAt DateTime @updatedAt @map(name: "updated_at")
name String? @unique name String? @unique
email String? @unique email String? @unique
emailVerified DateTime? @map(name: "email_verified") emailVerified DateTime? @map(name: "email_verified")
image String? image String?
items Item[] items Item[]
messages Message[] messages Message[]
votes Vote[] votes Vote[]
invoices Invoice[] invoices Invoice[]
msats Int @default(0) withdrawls Withdrawl[]
msats Int @default(0)
@@map(name: "users") @@map(name: "users")
} }
@ -86,6 +87,32 @@ model Invoice {
@@index([userId]) @@index([userId])
} }
enum WithdrawlStatus {
INSUFFICIENT_BALANCE
INVALID_PAYMENT
PATHFINDING_TIMEOUT
ROUTE_NOT_FOUND
}
model Withdrawl {
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @updatedAt @map(name: "updated_at")
user User @relation(fields: [userId], references: [id])
userId Int
hash String @unique
bolt11 String
msatsPaying Int
msatsPaid Int?
msatsFeePaying Int
msatsFeePaid Int?
status WithdrawlStatus?
@@index([userId])
}
model Account { model Account {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map(name: "created_at") createdAt DateTime @default(now()) @map(name: "created_at")