half done with wallets
This commit is contained in:
parent
67d1605666
commit
d92fc12187
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,5 +14,6 @@ export default gql`
|
||||||
ncomments: Int!
|
ncomments: Int!
|
||||||
stacked: Int!
|
stacked: Int!
|
||||||
sats: Int!
|
sats: Int!
|
||||||
|
msats: Int!
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,4 +65,27 @@ BEGIN
|
||||||
END IF;
|
END IF;
|
||||||
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;
|
||||||
$$;
|
$$;
|
|
@ -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")
|
||||||
|
|
Loading…
Reference in New Issue