satisitics with invoice & withdrawal

This commit is contained in:
keyan 2021-12-15 10:50:11 -06:00
parent 8cdeb18216
commit 06f5ed731e
10 changed files with 348 additions and 16 deletions

View File

@ -2,6 +2,7 @@ import { createInvoice, decodePaymentRequest, subscribeToPayViaRequest } from 'l
import { UserInputError, AuthenticationError } from 'apollo-server-micro'
import serialize from './serial'
import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
import lnpr from 'bolt11'
export default {
Query: {
@ -50,19 +51,19 @@ export default {
},
walletHistory: async (parent, { cursor }, { me, models, lnd }) => {
const decodedCursor = decodeCursor(cursor)
// if (!me) {
// throw new AuthenticationError('you must be logged in')
// }
if (!me) {
throw new AuthenticationError('you must be logged in')
}
// TODO
// 1. union invoices and withdrawals
// 1. union invoices and withdrawals (check)
// 2. add to union spending and receiving
const history = await models.$queryRaw(`
let history = await models.$queryRaw(`
(SELECT id, bolt11, created_at as "createdAt",
"msatsReceived" as msats, NULL as "msatsFee",
COALESCE("msatsReceived", "msatsRequested") as msats, NULL as "msatsFee",
CASE WHEN "confirmedAt" IS NOT NULL THEN 'CONFIRMED'
WHEN "expiresAt" IS NOT NULL THEN 'EXPIRED'
WHEN "expiresAt" <= $2 THEN 'EXPIRED'
WHEN cancelled THEN 'CANCELLED'
ELSE 'PENDING' END as status,
'invoice' as type
@ -86,7 +87,31 @@ export default {
LIMIT ${LIMIT}+$3)
ORDER BY "createdAt" DESC
OFFSET $3
LIMIT ${LIMIT}`, 624, decodedCursor.time, decodedCursor.offset)
LIMIT ${LIMIT}`, me.id, decodedCursor.time, decodedCursor.offset)
history = history.map(f => {
if (f.bolt11) {
const inv = lnpr.decode(f.bolt11)
if (inv) {
const { tags } = inv
for (const tag of tags) {
if (tag.tagName === 'description') {
f.description = tag.data
break
}
}
}
}
switch (f.type) {
case 'withdrawal':
f.msats *= -1
break
default:
break
}
return f
})
return {
cursor: history.length === LIMIT ? nextCursorEncoded(decodedCursor) : null,

View File

@ -41,12 +41,13 @@ export default gql`
type Fact {
id: ID!
bolt11: String!
bolt11: String
createdAt: String!
msats: Int!
msatsFee: Int!
msatsFee: Int
status: String!
type: String!
description: String
}
type History {

View File

@ -32,6 +32,7 @@ export default function UserHeader ({ user }) {
const [setName] = useMutation(NAME_MUTATION)
const Satistics = () => <h1 className='mb-0'><small className='text-success'>{user.sats} sats \ {user.stacked} stacked</small></h1>
const isMe = me?.name === user.name
const UserSchema = Yup.object({
name: Yup.string()
@ -104,7 +105,7 @@ export default function UserHeader ({ user }) {
: (
<div className='d-flex align-items-center'>
<h2 className='mb-0'>@{user.name}</h2>
{me?.name === user.name &&
{isMe &&
<Button variant='link' onClick={() => setEditting(true)}>edit nym</Button>}
</div>
)}
@ -129,11 +130,12 @@ export default function UserHeader ({ user }) {
<Nav.Link>{user.ncomments} comments</Nav.Link>
</Link>
</Nav.Item>
{/* <Nav.Item>
<Link href={'/' + user.name + '/sativity'} passHref>
<Nav.Link>satistics</Nav.Link>
</Link>
</Nav.Item> */}
{isMe &&
<Nav.Item>
<Link href='/satistics' passHref>
<Nav.Link>satistics</Nav.Link>
</Link>
</Nav.Item>}
</Nav>
</>
)

View File

@ -24,6 +24,24 @@ export const WITHDRAWL = gql`
}
}`
export const WALLET_HISTORY = gql`
query WalletHistory($cursor: String) {
walletHistory(cursor: $cursor) {
facts {
id
type
createdAt
msats
msatsFee
status
type
description
}
cursor
}
}
`
export const CREATE_WITHDRAWL = gql`
mutation createWithdrawl($invoice: String!, $maxFee: Int!) {
createWithdrawl(invoice: $invoice, maxFee: $maxFee) {

View File

@ -64,6 +64,19 @@ export default function getApolloClient () {
lastChecked: incoming.lastChecked
}
}
},
walletHistory: {
keyArgs: false,
merge (existing, incoming) {
if (isFirstPage(incoming.cursor, existing?.facts)) {
return incoming
}
return {
cursor: incoming.cursor,
facts: [...(existing?.facts || []), ...incoming.facts]
}
}
}
}
}

112
package-lock.json generated
View File

@ -14,6 +14,7 @@
"async-retry": "^1.3.1",
"babel-plugin-inline-react-svg": "^2.0.1",
"bech32": "^2.0.0",
"bolt11": "^1.3.4",
"bootstrap": "^4.6.0",
"clipboard-copy": "^4.0.1",
"domino": "^2.1.6",
@ -910,6 +911,14 @@
"@types/node": "*"
}
},
"node_modules/@types/bn.js": {
"version": "4.11.6",
"resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-4.11.6.tgz",
"integrity": "sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/body-parser": {
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.1.tgz",
@ -2096,6 +2105,53 @@
"node": ">=10.4.0"
}
},
"node_modules/bolt11": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/bolt11/-/bolt11-1.3.4.tgz",
"integrity": "sha512-x4lHDv0oid13lGlZU7cl/5gx9nRwjB2vgK/uB3c50802Wh+9WjWQMwzD2PCETHylUijx2iBAqUQYbx3ZgwF06Q==",
"dependencies": {
"@types/bn.js": "^4.11.3",
"bech32": "^1.1.2",
"bitcoinjs-lib": "^6.0.0",
"bn.js": "^4.11.8",
"create-hash": "^1.2.0",
"lodash": "^4.17.11",
"safe-buffer": "^5.1.1",
"secp256k1": "^4.0.2"
}
},
"node_modules/bolt11/node_modules/bech32": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz",
"integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ=="
},
"node_modules/bolt11/node_modules/bitcoinjs-lib": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.0.1.tgz",
"integrity": "sha512-x/7D4jDj/MMkmO6t3p2CSDXTqpwZ/jRsRiJDmaiXabrR9XRo7jwby8HRn7EyK1h24rKFFI7vI0ay4czl6bDOZQ==",
"dependencies": {
"bech32": "^2.0.0",
"bip174": "^2.0.1",
"bs58check": "^2.1.2",
"create-hash": "^1.1.0",
"typeforce": "^1.11.3",
"varuint-bitcoin": "^1.1.2",
"wif": "^2.0.1"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/bolt11/node_modules/bitcoinjs-lib/node_modules/bech32": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz",
"integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg=="
},
"node_modules/bolt11/node_modules/bn.js": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
},
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
@ -12218,6 +12274,14 @@
"@types/node": "*"
}
},
"@types/bn.js": {
"version": "4.11.6",
"resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-4.11.6.tgz",
"integrity": "sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg==",
"requires": {
"@types/node": "*"
}
},
"@types/body-parser": {
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.1.tgz",
@ -13215,6 +13279,54 @@
"resolved": "https://registry.npmjs.org/bolt09/-/bolt09-0.1.5.tgz",
"integrity": "sha512-oT1+erg21vat55oXNd7nNEkCO0FQnmaraFZuyXFyeVk7dZCm/3vgic0qK1VuUSV+ksYXJfRKYC4AqfYrtHNPZg=="
},
"bolt11": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/bolt11/-/bolt11-1.3.4.tgz",
"integrity": "sha512-x4lHDv0oid13lGlZU7cl/5gx9nRwjB2vgK/uB3c50802Wh+9WjWQMwzD2PCETHylUijx2iBAqUQYbx3ZgwF06Q==",
"requires": {
"@types/bn.js": "^4.11.3",
"bech32": "^1.1.2",
"bitcoinjs-lib": "^6.0.0",
"bn.js": "^4.11.8",
"create-hash": "^1.2.0",
"lodash": "^4.17.11",
"safe-buffer": "^5.1.1",
"secp256k1": "^4.0.2"
},
"dependencies": {
"bech32": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz",
"integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ=="
},
"bitcoinjs-lib": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.0.1.tgz",
"integrity": "sha512-x/7D4jDj/MMkmO6t3p2CSDXTqpwZ/jRsRiJDmaiXabrR9XRo7jwby8HRn7EyK1h24rKFFI7vI0ay4czl6bDOZQ==",
"requires": {
"bech32": "^2.0.0",
"bip174": "^2.0.1",
"bs58check": "^2.1.2",
"create-hash": "^1.1.0",
"typeforce": "^1.11.3",
"varuint-bitcoin": "^1.1.2",
"wif": "^2.0.1"
},
"dependencies": {
"bech32": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz",
"integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg=="
}
}
},
"bn.js": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
}
}
},
"boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",

View File

@ -15,6 +15,7 @@
"async-retry": "^1.3.1",
"babel-plugin-inline-react-svg": "^2.0.1",
"bech32": "^2.0.0",
"bolt11": "^1.3.4",
"bootstrap": "^4.6.0",
"clipboard-copy": "^4.0.1",
"domino": "^2.1.6",

128
pages/satistics.js Normal file
View File

@ -0,0 +1,128 @@
import { useQuery } from '@apollo/client'
import Link from 'next/link'
import { Table } from 'react-bootstrap'
import useDarkMode from 'use-dark-mode'
import { getGetServerSideProps } from '../api/ssrApollo'
import Layout from '../components/layout'
import { useMe } from '../components/me'
import MoreFooter from '../components/more-footer'
import UserHeader from '../components/user-header'
import { WALLET_HISTORY } from '../fragments/wallet'
import styles from '../styles/satistics.module.css'
import Moon from '../svgs/moon-fill.svg'
import Check from '../svgs/check-double-line.svg'
import ThumbDown from '../svgs/thumb-down-fill.svg'
export const getServerSideProps = getGetServerSideProps(WALLET_HISTORY)
function satusClass (status) {
switch (status) {
case 'CONFIRMED':
return ''
case 'PENDING':
return 'text-muted'
default:
return styles.failed
}
}
function Satus ({ status }) {
if (!status) {
return null
}
const desc = () => {
switch (status) {
case 'CONFIRMED':
return 'confirmed'
case 'EXPIRED':
return 'expired'
case 'INSUFFICIENT_BALANCE':
return "you didn't have enough sats"
case 'INVALID_PAYMENT':
return 'invalid payment'
case 'PATHFINDING_TIMEOUT':
case 'ROUTE_NOT_FOUND':
return 'no route found'
case 'PENDING':
return 'pending'
default:
return 'unknown failure'
}
}
const color = () => {
switch (status) {
case 'CONFIRMED':
return 'success'
case 'PENDING':
return 'muted'
default:
return 'danger'
}
}
const Icon = () => {
switch (status) {
case 'CONFIRMED':
return <Check width='14' height='14' className='fill-success' />
case 'PENDING':
return <Moon width='14' height='14' className='spin fill-grey' />
default:
return <ThumbDown width='14' height='14' className='fill-danger' />
}
}
return (
<div>
<Icon /><small className={`text-${color()}`}>{' ' + desc()}</small>
</div>
)
}
export default function Satistics ({ data: { walletHistory: { facts, cursor } } }) {
const me = useMe()
const { value: darkMode } = useDarkMode()
const { data, fetchMore } = useQuery(WALLET_HISTORY)
if (data) {
({ walletHistory: { facts, cursor } } = data)
}
const SatisticsSkeleton = () => (
<div className='d-flex justify-content-center mt-3 mb-1'>
<Moon className='spin fill-grey' />
</div>)
return (
<Layout noSeo>
<UserHeader user={me} />
<Table className='mt-3 mb-0' bordered hover size='sm' variant={darkMode ? 'dark' : undefined}>
<thead>
<tr>
<th className={styles.type}>type</th>
<th>detail</th>
<th className={styles.sats}>sats</th>
</tr>
</thead>
<tbody>
{facts.map((f, i) => (
<Link href={`${f.type}s/${f.id}`} key={`${f.type}-${f.id}`}>
<tr className={styles.row}>
<td className={`${styles.type} ${satusClass(f.status)}`}>{f.type}</td>
<td className={styles.description}>
<div className={satusClass(f.status)}>
{f.description || 'no description'}
</div>
<Satus status={f.status} />
</td>
<td className={`${styles.sats} ${satusClass(f.status)}`}>{f.msats / 1000}</td>
</tr>
</Link>
))}
</tbody>
</Table>
<MoreFooter cursor={cursor} fetchMore={fetchMore} Skeleton={SatisticsSkeleton} />
</Layout>
)
}

View File

@ -59,6 +59,20 @@ $tooltip-bg: #5c8001;
src: url(/Lightningvolt-xoqm.ttf);
}
.table-sm th, .table-sm td {
padding: .3rem .75rem;
line-height: 1.2rem;
}
.table {
color: var(--theme-color);
background-color: var(--theme-body);
}
.table th, .table td, .table thead th {
border-color: var(--theme-borderColor);
}
body {
background: var(--theme-body);
color: var(--theme-color);

View File

@ -0,0 +1,18 @@
.type {
width: 1px;
white-space: nowrap;
}
.sats {
width: 1px;
white-space: nowrap;
text-align: right;
}
.failed {
text-decoration: line-through;
}
.row {
cursor: pointer;
}