invoiced ... WIP transactions
This commit is contained in:
parent
764a683500
commit
bc0389e622
@ -107,7 +107,7 @@ export default {
|
|||||||
throw new UserInputError('Sats must be positive', { argumentName: 'sats' })
|
throw new UserInputError('Sats must be positive', { argumentName: 'sats' })
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if we've already voted for the item
|
// TODO: check if we've already voted for the item ... XXX this isn't transactional
|
||||||
const boosted = await models.vote.findFirst({
|
const boosted = await models.vote.findFirst({
|
||||||
where: {
|
where: {
|
||||||
itemId: parseInt(id),
|
itemId: parseInt(id),
|
||||||
@ -143,7 +143,7 @@ export default {
|
|||||||
SELECT count(*)
|
SELECT count(*)
|
||||||
FROM "Item"
|
FROM "Item"
|
||||||
WHERE path <@ text2ltree(${item.path}) AND id != ${item.id}`
|
WHERE path <@ text2ltree(${item.path}) AND id != ${item.id}`
|
||||||
return count
|
return count || 0
|
||||||
},
|
},
|
||||||
sats: async (item, args, { models }) => {
|
sats: async (item, args, { models }) => {
|
||||||
const { sum: { sats } } = await models.vote.aggregate({
|
const { sum: { sats } } = await models.vote.aggregate({
|
||||||
@ -156,7 +156,7 @@ export default {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return sats
|
return sats || 0
|
||||||
},
|
},
|
||||||
boost: async (item, args, { models }) => {
|
boost: async (item, args, { models }) => {
|
||||||
const { sum: { sats } } = await models.vote.aggregate({
|
const { sum: { sats } } = await models.vote.aggregate({
|
||||||
@ -169,7 +169,7 @@ export default {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return sats
|
return sats || 0
|
||||||
},
|
},
|
||||||
meSats: async (item, args, { me, models }) => {
|
meSats: async (item, args, { me, models }) => {
|
||||||
if (!me) return 0
|
if (!me) return 0
|
||||||
@ -184,7 +184,7 @@ export default {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return sats
|
return sats || 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
export default {
|
export default {
|
||||||
Query: {
|
Query: {
|
||||||
me: async (parent, args, { models, me }) =>
|
me: async (parent, args, { models, me }) =>
|
||||||
me ? await models.user.findUnique({ where: { id: me.id } }) : null,
|
me ? await models.user.findUnique({ where: { name: me.name } }) : null,
|
||||||
user: async (parent, { name }, { models }) => {
|
user: async (parent, { name }, { models }) => {
|
||||||
return await models.user.findUnique({ where: { name } })
|
return await models.user.findUnique({ where: { name } })
|
||||||
},
|
},
|
||||||
@ -22,8 +22,10 @@ export default {
|
|||||||
FROM "Item"
|
FROM "Item"
|
||||||
LEFT JOIN "Vote" on "Vote"."itemId" = "Item".id
|
LEFT JOIN "Vote" on "Vote"."itemId" = "Item".id
|
||||||
WHERE "Item"."userId" = ${user.id}`
|
WHERE "Item"."userId" = ${user.id}`
|
||||||
return sum
|
return sum || 0
|
||||||
},
|
},
|
||||||
sats: () => 0
|
sats: async (user, args, { models }) => {
|
||||||
|
return Math.floor(user.msats / 1000)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,52 @@
|
|||||||
|
import { createInvoice } from 'ln-service'
|
||||||
|
import { UserInputError, AuthenticationError } from 'apollo-server-micro'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Query: {
|
Query: {
|
||||||
invoice: async (parent, { id }, { me, models, lnd }) => {
|
invoice: async (parent, { id }, { me, models, lnd }) => {
|
||||||
return 'lnbc1500n1psfxyaypp5tmlgpudspqed4qf32xxmc7dhlqrd4glc09x794exz4t2pw8ms38sdpa2fjkzep6yptks7fqt9hh2gzwv4jkggz5dus9gatjdcsyzmrvypvk7atjypzx7cqzpgxqr23ssp529tup4vaxlxnst0lwh9kljpl9n6zg6n6vma5hw78lmnws32x278s9qyyssqxe73jclrlz3u7v7ruwee3n7h70ktsdsfmvpfjkccqxq5wg5h6njhqxar0a9fef5hd09ethwhvsj0dha2qy4tjjdxu08nkqymfs8wghqp6d7kth'
|
return await models.invoice.findUnique({ where: { id: Number(id) } })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
Mutation: {
|
Mutation: {
|
||||||
createInvoice: async (parent, { amount }, { me, models, lnd }) => {
|
createInvoice: async (parent, { amount }, { me, models, lnd }) => {
|
||||||
return 'lnbc1500n1psfxyaypp5tmlgpudspqed4qf32xxmc7dhlqrd4glc09x794exz4t2pw8ms38sdpa2fjkzep6yptks7fqt9hh2gzwv4jkggz5dus9gatjdcsyzmrvypvk7atjypzx7cqzpgxqr23ssp529tup4vaxlxnst0lwh9kljpl9n6zg6n6vma5hw78lmnws32x278s9qyyssqxe73jclrlz3u7v7ruwee3n7h70ktsdsfmvpfjkccqxq5wg5h6njhqxar0a9fef5hd09ethwhvsj0dha2qy4tjjdxu08nkqymfs8wghqp6d7kth'
|
if (!me) {
|
||||||
|
throw new AuthenticationError('You must be logged in')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!amount || amount <= 0) {
|
||||||
|
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
|
||||||
|
const expiresAt = new Date(new Date().setHours(new Date().getHours() + 3))
|
||||||
|
const description = `${amount} sats for @${me.name} on stacker.news`
|
||||||
|
const invoice = await createInvoice({ description, lnd, tokens: amount, expires_at: expiresAt })
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
hash: invoice.id,
|
||||||
|
bolt11: invoice.request,
|
||||||
|
expiresAt: expiresAt,
|
||||||
|
msatsRequested: amount * 1000,
|
||||||
|
user: {
|
||||||
|
connect: {
|
||||||
|
name: me.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await models.invoice.create({ data })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,20 @@ import { gql } from 'apollo-server-micro'
|
|||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
extend type Query {
|
extend type Query {
|
||||||
invoice(id: ID!): String!
|
invoice(id: ID!): Invoice!
|
||||||
}
|
}
|
||||||
|
|
||||||
extend type Mutation {
|
extend type Mutation {
|
||||||
createInvoice(amount: Int!): String!
|
createInvoice(amount: Int!): Invoice!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Invoice {
|
||||||
|
id: ID!
|
||||||
|
createdAt: String!
|
||||||
|
bolt11: String!
|
||||||
|
expiresAt: String!
|
||||||
|
cancelled: Boolean!
|
||||||
|
confirmedAt: String
|
||||||
|
msatsReceived: Int
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
@ -6,6 +6,21 @@ import styles from './header.module.css'
|
|||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { Button, Container, NavDropdown } from 'react-bootstrap'
|
import { Button, Container, NavDropdown } from 'react-bootstrap'
|
||||||
import Price from './price'
|
import Price from './price'
|
||||||
|
import { gql, useQuery } from '@apollo/client'
|
||||||
|
|
||||||
|
function WalletSummary () {
|
||||||
|
const query = gql`
|
||||||
|
{
|
||||||
|
me {
|
||||||
|
sats
|
||||||
|
stacked
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
const { data } = useQuery(query, { pollInterval: 1000 })
|
||||||
|
if (!data) return null
|
||||||
|
|
||||||
|
return `[${data.me.stacked},${data.me.sats}]`
|
||||||
|
}
|
||||||
|
|
||||||
export default function Header () {
|
export default function Header () {
|
||||||
const [session, loading] = useSession()
|
const [session, loading] = useSession()
|
||||||
@ -42,7 +57,7 @@ export default function Header () {
|
|||||||
</NavDropdown>
|
</NavDropdown>
|
||||||
<Nav.Item>
|
<Nav.Item>
|
||||||
<Link href='/wallet' passHref>
|
<Link href='/wallet' passHref>
|
||||||
<Nav.Link className='text-success px-0'>[0,0]</Nav.Link>
|
<Nav.Link className='text-success px-0'><WalletSummary /></Nav.Link>
|
||||||
</Link>
|
</Link>
|
||||||
</Nav.Item>
|
</Nav.Item>
|
||||||
</div>
|
</div>
|
||||||
|
@ -6,51 +6,84 @@ import Thumb from '../svgs/thumb-up-fill.svg'
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import BootstrapForm from 'react-bootstrap/Form'
|
import BootstrapForm from 'react-bootstrap/Form'
|
||||||
import Button from 'react-bootstrap/Button'
|
import Button from 'react-bootstrap/Button'
|
||||||
|
import Check from '../svgs/check-double-line.svg'
|
||||||
|
import Fail from '../svgs/close-line.svg'
|
||||||
|
|
||||||
export function Invoice ({ invoice }) {
|
export function Invoice ({ invoice }) {
|
||||||
const [copied, setCopied] = useState(false)
|
const [copied, setCopied] = useState(false)
|
||||||
const qrValue = 'lightning:' + invoice.toUpperCase()
|
const qrValue = 'lightning:' + invoice.bolt11.toUpperCase()
|
||||||
|
|
||||||
|
let InvoiceStatus = InvoiceDefaultStatus
|
||||||
|
let status = 'waiting for you'
|
||||||
|
if (invoice.confirmedAt) {
|
||||||
|
InvoiceStatus = InvoiceConfirmedStatus
|
||||||
|
status = `${invoice.msatsReceived / 1000} sats deposited`
|
||||||
|
} else if (invoice.cancelled) {
|
||||||
|
InvoiceStatus = InvoiceFailedStatus
|
||||||
|
status = 'cancelled'
|
||||||
|
} else if (invoice.expiresAt <= new Date()) {
|
||||||
|
InvoiceStatus = InvoiceFailedStatus
|
||||||
|
status = 'expired'
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<QRCode className='h-auto mw-100' value={qrValue} size={300} />
|
<QRCode className='h-auto mw-100' value={qrValue} renderAs='svg' size={300} />
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-3 w-100'>
|
<div className='mt-3 w-100'>
|
||||||
<InputGroup onClick={() => {
|
<InputGroup onClick={() => {
|
||||||
copy(invoice)
|
copy(invoice.bolt11)
|
||||||
setCopied(true)
|
setCopied(true)
|
||||||
setTimeout(() => setCopied(false), 1500)
|
setTimeout(() => setCopied(false), 1500)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BootstrapForm.Control type='text' placeholder={invoice} readOnly />
|
<BootstrapForm.Control type='text' placeholder={invoice.bolt11} readOnly />
|
||||||
<InputGroup.Append>
|
<InputGroup.Append>
|
||||||
<Button>{copied ? <Thumb width={20} height={20} /> : 'copy'}</Button>
|
<Button>{copied ? <Thumb width={20} height={20} /> : 'copy'}</Button>
|
||||||
</InputGroup.Append>
|
</InputGroup.Append>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
</div>
|
</div>
|
||||||
<InvoiceStatus />
|
<InvoiceStatus status={status} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InvoiceStatus ({ skeleton }) {
|
export function InvoiceDefaultStatus ({ status }) {
|
||||||
return (
|
return (
|
||||||
<div className='d-flex mt-4'>
|
<div className='d-flex mt-4'>
|
||||||
<Moon className='spin fill-grey' />
|
<Moon className='spin fill-grey' />
|
||||||
<div className='ml-3 text-muted' style={{ fontWeight: '600' }}>{skeleton ? 'generating' : 'waiting for you'}</div>
|
<div className='ml-3 text-muted' style={{ fontWeight: '600' }}>{status}</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InvoiceSkeleton () {
|
export function InvoiceConfirmedStatus ({ status }) {
|
||||||
|
return (
|
||||||
|
<div className='d-flex mt-4'>
|
||||||
|
<Check className='fill-success' />
|
||||||
|
<div className='ml-3 text-success' style={{ fontWeight: '600' }}>{status}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InvoiceFailedStatus ({ status }) {
|
||||||
|
return (
|
||||||
|
<div className='d-flex mt-4'>
|
||||||
|
<Fail className='fill-danger' />
|
||||||
|
<div className='ml-3 text-danger' style={{ fontWeight: '600' }}>{status}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InvoiceSkeleton ({ status }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='h-auto w-100 clouds' style={{ paddingTop: 'min(300px, 100%)', maxWidth: '300px' }} />
|
<div className='h-auto w-100 clouds' style={{ paddingTop: 'min(300px, 100%)', maxWidth: '300px' }} />
|
||||||
<div className='mt-3 w-100'>
|
<div className='mt-3 w-100'>
|
||||||
<div className='w-100 clouds form-control' />
|
<div className='w-100 clouds form-control' />
|
||||||
</div>
|
</div>
|
||||||
<InvoiceStatus skeleton />
|
<InvoiceDefaultStatus status={status} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -20,11 +20,11 @@ export default function UserHeader ({ user }) {
|
|||||||
<Nav.Link>{user.ncomments} comments</Nav.Link>
|
<Nav.Link>{user.ncomments} comments</Nav.Link>
|
||||||
</Link>
|
</Link>
|
||||||
</Nav.Item>
|
</Nav.Item>
|
||||||
<Nav.Item>
|
{/* <Nav.Item>
|
||||||
<Link href={'/' + user.name + '/sativity'} passHref>
|
<Link href={'/' + user.name + '/sativity'} passHref>
|
||||||
<Nav.Link>sativity</Nav.Link>
|
<Nav.Link>sativity</Nav.Link>
|
||||||
</Link>
|
</Link>
|
||||||
</Nav.Item>
|
</Nav.Item> */}
|
||||||
</Nav>
|
</Nav>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
101
invoiced/index.js
Normal file
101
invoiced/index.js
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
const { PrismaClient } = require('@prisma/client')
|
||||||
|
const { authenticatedLndGrpc, subscribeToInvoices, getInvoice } = require('ln-service')
|
||||||
|
const dotenv = require('dotenv')
|
||||||
|
|
||||||
|
dotenv.config({ path: '..' })
|
||||||
|
|
||||||
|
const { lnd } = authenticatedLndGrpc({
|
||||||
|
cert: process.env.LND_CERT,
|
||||||
|
macaroon: process.env.LND_MACAROON,
|
||||||
|
socket: process.env.LND_SOCKET
|
||||||
|
})
|
||||||
|
|
||||||
|
const models = new PrismaClient()
|
||||||
|
|
||||||
|
async function recordStatus (inv) {
|
||||||
|
console.log(inv)
|
||||||
|
if (inv.is_confirmed) {
|
||||||
|
const received = Number(inv.received_mtokens)
|
||||||
|
|
||||||
|
// only increment iff this invoice has not yet confirmed
|
||||||
|
const updateUser = models.user.updateMany({
|
||||||
|
where: {
|
||||||
|
invoices: {
|
||||||
|
some: {
|
||||||
|
hash: inv.id,
|
||||||
|
confirmedAt: {
|
||||||
|
equals: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
msats: {
|
||||||
|
increment: received
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// ATOMICALLY (with above) mark the invoice as confirmed
|
||||||
|
const updateInvoice = models.invoice.updateMany({
|
||||||
|
where: {
|
||||||
|
hash: inv.id,
|
||||||
|
AND: [
|
||||||
|
{
|
||||||
|
confirmedAt: {
|
||||||
|
equals: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
confirmedAt: inv.confirmed_at,
|
||||||
|
msatsReceived: received
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
models.$transaction([updateUser, updateInvoice])
|
||||||
|
} else if (inv.is_canceled) {
|
||||||
|
// mark as cancelled
|
||||||
|
models.invoice.update({
|
||||||
|
where: {
|
||||||
|
hash: inv.id
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
cancelled: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. subscribe to all invoices async
|
||||||
|
const sub = subscribeToInvoices({ lnd })
|
||||||
|
sub.on('invoice_updated', recordStatus)
|
||||||
|
|
||||||
|
// 2. check all pending invoices from db in lnd
|
||||||
|
async function checkPending () {
|
||||||
|
const now = new Date()
|
||||||
|
const active = await models.invoice.findMany({
|
||||||
|
where: {
|
||||||
|
expiresAt: {
|
||||||
|
gt: now
|
||||||
|
},
|
||||||
|
cancelled: false,
|
||||||
|
confirmedAt: {
|
||||||
|
equals: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
active.forEach(async invoice => {
|
||||||
|
try {
|
||||||
|
const inv = await getInvoice({ id: invoice.hash, lnd })
|
||||||
|
recordStatus(inv)
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
checkPending()
|
@ -9,7 +9,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^3.3.13",
|
"@apollo/client": "^3.3.13",
|
||||||
"@prisma/client": "^2.19.0",
|
"@prisma/client": "^2.22.1",
|
||||||
"apollo-server-micro": "^2.21.2",
|
"apollo-server-micro": "^2.21.2",
|
||||||
"bootstrap": "^4.6.0",
|
"bootstrap": "^4.6.0",
|
||||||
"clipboard-copy": "^4.0.1",
|
"clipboard-copy": "^4.0.1",
|
||||||
@ -43,7 +43,7 @@
|
|||||||
"babel-plugin-inline-react-svg": "^2.0.1",
|
"babel-plugin-inline-react-svg": "^2.0.1",
|
||||||
"eslint": "^7.22.0",
|
"eslint": "^7.22.0",
|
||||||
"eslint-plugin-compat": "^3.9.0",
|
"eslint-plugin-compat": "^3.9.0",
|
||||||
"prisma": "2.19.0",
|
"prisma": "^2.22.1",
|
||||||
"standard": "^16.0.3"
|
"standard": "^16.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,14 @@ export async function getServerSideProps ({ params: { id } }) {
|
|||||||
export default function FullInvoice ({ id }) {
|
export default function FullInvoice ({ id }) {
|
||||||
const query = gql`
|
const query = gql`
|
||||||
{
|
{
|
||||||
invoice(id: ${id})
|
invoice(id: ${id}) {
|
||||||
|
id
|
||||||
|
bolt11
|
||||||
|
msatsReceived
|
||||||
|
cancelled
|
||||||
|
confirmedAt
|
||||||
|
expiresAt
|
||||||
|
}
|
||||||
}`
|
}`
|
||||||
return (
|
return (
|
||||||
<LayoutCenter>
|
<LayoutCenter>
|
||||||
@ -24,10 +31,10 @@ export default function FullInvoice ({ id }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function LoadInvoice ({ query }) {
|
function LoadInvoice ({ query }) {
|
||||||
const { loading, error, data } = useQuery(query)
|
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 <InvoiceSkeleton />
|
return <InvoiceSkeleton status='loading' />
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Invoice invoice={data.invoice} />
|
return <Invoice invoice={data.invoice} />
|
||||||
|
@ -47,11 +47,13 @@ export function FundForm () {
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [createInvoice, { called }] = useMutation(gql`
|
const [createInvoice, { called }] = useMutation(gql`
|
||||||
mutation createInvoice($amount: Int!) {
|
mutation createInvoice($amount: Int!) {
|
||||||
createInvoice(amount: $amount)
|
createInvoice(amount: $amount) {
|
||||||
|
id
|
||||||
|
}
|
||||||
}`)
|
}`)
|
||||||
|
|
||||||
if (called) {
|
if (called) {
|
||||||
return <InvoiceSkeleton />
|
return <InvoiceSkeleton status='generating' />
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -61,8 +63,8 @@ export function FundForm () {
|
|||||||
}}
|
}}
|
||||||
schema={FundSchema}
|
schema={FundSchema}
|
||||||
onSubmit={async ({ amount }) => {
|
onSubmit={async ({ amount }) => {
|
||||||
await createInvoice({ variables: { amount } })
|
const { data } = await createInvoice({ variables: { amount: Number(amount) } })
|
||||||
router.push('/invoices/1')
|
router.push(`/invoices/${data.createInvoice.id}`)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
|
25
prisma/migrations/20210507005624_invoices/migration.sql
Normal file
25
prisma/migrations/20210507005624_invoices/migration.sql
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "item_gist_path_index";
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Invoice" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||||
|
"userId" INTEGER NOT NULL,
|
||||||
|
"hash" TEXT NOT NULL,
|
||||||
|
"bolt11" TEXT NOT NULL,
|
||||||
|
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"confirmedAt" TIMESTAMP(3),
|
||||||
|
"requested" INTEGER NOT NULL,
|
||||||
|
"received" INTEGER,
|
||||||
|
"cancelled" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Invoice.userId_index" ON "Invoice"("userId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Invoice" ADD FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
16
prisma/migrations/20210510175729_msats/migration.sql
Normal file
16
prisma/migrations/20210510175729_msats/migration.sql
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `requested` on the `Invoice` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `received` on the `Invoice` table. All the data in the column will be lost.
|
||||||
|
- Added the required column `msatsRequested` to the `Invoice` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Invoice" DROP COLUMN "requested",
|
||||||
|
DROP COLUMN "received",
|
||||||
|
ADD COLUMN "msatsRequested" INTEGER NOT NULL,
|
||||||
|
ADD COLUMN "msatsReceived" INTEGER;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "users" ADD COLUMN "msats" INTEGER NOT NULL DEFAULT 0;
|
@ -21,6 +21,8 @@ model User {
|
|||||||
items Item[]
|
items Item[]
|
||||||
messages Message[]
|
messages Message[]
|
||||||
votes Vote[]
|
votes Vote[]
|
||||||
|
invoices Invoice[]
|
||||||
|
msats Int @default(0)
|
||||||
|
|
||||||
@@map(name: "users")
|
@@map(name: "users")
|
||||||
}
|
}
|
||||||
@ -66,6 +68,24 @@ model Vote {
|
|||||||
@@index([userId])
|
@@index([userId])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Invoice {
|
||||||
|
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
|
||||||
|
expiresAt DateTime
|
||||||
|
confirmedAt DateTime?
|
||||||
|
msatsRequested Int
|
||||||
|
msatsReceived Int?
|
||||||
|
cancelled Boolean @default(false)
|
||||||
|
|
||||||
|
@@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")
|
||||||
|
@ -117,6 +117,14 @@ body {
|
|||||||
fill: grey;
|
fill: grey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fill-success {
|
||||||
|
fill: #5c8001;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fill-danger {
|
||||||
|
fill: #c03221;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes flash {
|
@keyframes flash {
|
||||||
from { filter: brightness(1);}
|
from { filter: brightness(1);}
|
||||||
2% { filter: brightness(2.3); }
|
2% { filter: brightness(2.3); }
|
||||||
@ -155,3 +163,29 @@ body {
|
|||||||
background-attachment: fixed;
|
background-attachment: fixed;
|
||||||
opacity: .1;
|
opacity: .1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes flipX{
|
||||||
|
from {
|
||||||
|
transform: rotateX(180deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotateX(-180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.flipX {
|
||||||
|
animation: flipX 2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes flipY{
|
||||||
|
from {
|
||||||
|
transform: rotateY(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotateY(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.flipY {
|
||||||
|
animation: flipY 4s linear infinite;
|
||||||
|
}
|
1
svgs/check-double-line.svg
Normal file
1
svgs/check-double-line.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="M11.602 13.76l1.412 1.412 8.466-8.466 1.414 1.414-9.88 9.88-6.364-6.364 1.414-1.414 2.125 2.125 1.413 1.412zm.002-2.828l4.952-4.953 1.41 1.41-4.952 4.953-1.41-1.41zm-2.827 5.655L7.364 18 1 11.636l1.414-1.414 1.413 1.413-.001.001 4.951 4.951z"/></svg>
|
After Width: | Height: | Size: 379 B |
1
svgs/close-line.svg
Normal file
1
svgs/close-line.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="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z"/></svg>
|
After Width: | Height: | Size: 266 B |
36
yarn.lock
36
yarn.lock
@ -340,22 +340,22 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.9.2.tgz#adea7b6953cbb34651766b0548468e743c6a2353"
|
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.9.2.tgz#adea7b6953cbb34651766b0548468e743c6a2353"
|
||||||
integrity sha512-VZMYa7+fXHdwIq1TDhSXoVmSPEGM/aa+6Aiq3nVVJ9bXr24zScr+NlKFKC3iPljA7ho/GAZr+d2jOf5GIRC30Q==
|
integrity sha512-VZMYa7+fXHdwIq1TDhSXoVmSPEGM/aa+6Aiq3nVVJ9bXr24zScr+NlKFKC3iPljA7ho/GAZr+d2jOf5GIRC30Q==
|
||||||
|
|
||||||
"@prisma/client@^2.19.0":
|
"@prisma/client@^2.22.1":
|
||||||
version "2.19.0"
|
version "2.22.1"
|
||||||
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-2.19.0.tgz#a45f17a59fd109e95b61bf4b56d4a7642169ec0e"
|
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-2.22.1.tgz#10fdcd1532a6baf46dd1c464cad9a54af0532bc8"
|
||||||
integrity sha512-QK4M8TjJh1QesyO9aLM7DeAQUi5+UnNHpEAm5kwqBO1cq/4Ag5yU9ladctJFJleEE5BLewXHwV2t9A+VfCZslg==
|
integrity sha512-JQjbsY6QSfFiovXHEp5WeJHa5p2CuR1ZFPAeYXmUsOAQOaMCrhgQmKAL6w2Q3SRA7ALqPjrKywN9/QfBc4Kp1A==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@prisma/engines-version" "2.19.0-39.c1455d0b443d66b0d9db9bcb1bb9ee0d5bbc511d"
|
"@prisma/engines-version" "2.22.0-21.60cc71d884972ab4e897f0277c4b84383dddaf6c"
|
||||||
|
|
||||||
"@prisma/engines-version@2.19.0-39.c1455d0b443d66b0d9db9bcb1bb9ee0d5bbc511d":
|
"@prisma/engines-version@2.22.0-21.60cc71d884972ab4e897f0277c4b84383dddaf6c":
|
||||||
version "2.19.0-39.c1455d0b443d66b0d9db9bcb1bb9ee0d5bbc511d"
|
version "2.22.0-21.60cc71d884972ab4e897f0277c4b84383dddaf6c"
|
||||||
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-2.19.0-39.c1455d0b443d66b0d9db9bcb1bb9ee0d5bbc511d.tgz#a7f80d481ec6cb8e2975ab530664d4ca5fc9eba6"
|
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-2.22.0-21.60cc71d884972ab4e897f0277c4b84383dddaf6c.tgz#e98ee17217a0ebb54f2f9314fbbfd610b05e6e31"
|
||||||
integrity sha512-NzhbwC4iMbRQwJxdhNQX6eaVcOuNGtHRk6aesWE4KMf/YmlW5kfi3HDy7WZ/C4P0Iyn9oURDuk+xZV6QDUVjTw==
|
integrity sha512-OkkVwk6iTzTbwwl8JIKAENyxmh4TFORal55QMKQzrHEY8UzbD0M90mQnmziz3PAopQUZgTFFMlaPAq1WNrLMtA==
|
||||||
|
|
||||||
"@prisma/engines@2.19.0-39.c1455d0b443d66b0d9db9bcb1bb9ee0d5bbc511d":
|
"@prisma/engines@2.22.0-21.60cc71d884972ab4e897f0277c4b84383dddaf6c":
|
||||||
version "2.19.0-39.c1455d0b443d66b0d9db9bcb1bb9ee0d5bbc511d"
|
version "2.22.0-21.60cc71d884972ab4e897f0277c4b84383dddaf6c"
|
||||||
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-2.19.0-39.c1455d0b443d66b0d9db9bcb1bb9ee0d5bbc511d.tgz#db2809a6f7f18584e3ca89b1f5bad884155629ec"
|
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-2.22.0-21.60cc71d884972ab4e897f0277c4b84383dddaf6c.tgz#4ccd255e0823605db3d8387a5195b6fdabe3b0c0"
|
||||||
integrity sha512-rEWpaG7wZvPuWJC5SwkBB/Iwue//oC5yv58Mse7r+ibtgkA7vGdWc1bFDQ32DT9tDL5WSC6bBwqEASGV/1Gm1Q==
|
integrity sha512-KmWdogrsfsSLYvfqY3cS3QcDGzaEFklE+T6dNJf+k/KPQum4A29IwDalafMwh5cMN8ivZobUbowNSwWJrMT08Q==
|
||||||
|
|
||||||
"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2":
|
"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2":
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
@ -4234,12 +4234,12 @@ pretty-format@^3.8.0:
|
|||||||
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-3.8.0.tgz#bfbed56d5e9a776645f4b1ff7aa1a3ac4fa3c385"
|
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-3.8.0.tgz#bfbed56d5e9a776645f4b1ff7aa1a3ac4fa3c385"
|
||||||
integrity sha1-v77VbV6ad2ZF9LH/eqGjrE+jw4U=
|
integrity sha1-v77VbV6ad2ZF9LH/eqGjrE+jw4U=
|
||||||
|
|
||||||
prisma@2.19.0:
|
prisma@^2.22.1:
|
||||||
version "2.19.0"
|
version "2.22.1"
|
||||||
resolved "https://registry.yarnpkg.com/prisma/-/prisma-2.19.0.tgz#2c14f9cbbfb0ab69c8a9473e16736759713d29ad"
|
resolved "https://registry.yarnpkg.com/prisma/-/prisma-2.22.1.tgz#884687a90c7b797b34c6110ea413049078c8da6e"
|
||||||
integrity sha512-iartCNVrtR4XT20ABN3zrSi3R/pCBe75Y0ZH8681QIGm8qjRQzf3DnbscPZgZ9iY4KFuVxL8ZrBQVDmRhpN0EQ==
|
integrity sha512-hwvCM3zyxgSda/+/p+GW7nz93jRebtMU01wAG7YVVnl0OKU+dpw1wPvPFmQRldkZHk8fTCleYmjc24WaSdVPZQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@prisma/engines" "2.19.0-39.c1455d0b443d66b0d9db9bcb1bb9ee0d5bbc511d"
|
"@prisma/engines" "2.22.0-21.60cc71d884972ab4e897f0277c4b84383dddaf6c"
|
||||||
|
|
||||||
process-nextick-args@~2.0.0:
|
process-nextick-args@~2.0.0:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user