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 { UserInputError, AuthenticationError } from 'apollo-server-micro'
import serialize from './serial' import serialize from './serial'
import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor' import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
import lnpr from 'bolt11'
export default { export default {
Query: { Query: {
@ -50,19 +51,19 @@ export default {
}, },
walletHistory: async (parent, { cursor }, { me, models, lnd }) => { walletHistory: async (parent, { cursor }, { me, models, lnd }) => {
const decodedCursor = decodeCursor(cursor) const decodedCursor = decodeCursor(cursor)
// if (!me) { if (!me) {
// throw new AuthenticationError('you must be logged in') throw new AuthenticationError('you must be logged in')
// } }
// TODO // TODO
// 1. union invoices and withdrawals // 1. union invoices and withdrawals (check)
// 2. add to union spending and receiving // 2. add to union spending and receiving
const history = await models.$queryRaw(` let history = await models.$queryRaw(`
(SELECT id, bolt11, created_at as "createdAt", (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' CASE WHEN "confirmedAt" IS NOT NULL THEN 'CONFIRMED'
WHEN "expiresAt" IS NOT NULL THEN 'EXPIRED' WHEN "expiresAt" <= $2 THEN 'EXPIRED'
WHEN cancelled THEN 'CANCELLED' WHEN cancelled THEN 'CANCELLED'
ELSE 'PENDING' END as status, ELSE 'PENDING' END as status,
'invoice' as type 'invoice' as type
@ -86,7 +87,31 @@ export default {
LIMIT ${LIMIT}+$3) LIMIT ${LIMIT}+$3)
ORDER BY "createdAt" DESC ORDER BY "createdAt" DESC
OFFSET $3 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 { return {
cursor: history.length === LIMIT ? nextCursorEncoded(decodedCursor) : null, cursor: history.length === LIMIT ? nextCursorEncoded(decodedCursor) : null,

View File

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

View File

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

View File

@ -64,6 +64,19 @@ export default function getApolloClient () {
lastChecked: incoming.lastChecked 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", "async-retry": "^1.3.1",
"babel-plugin-inline-react-svg": "^2.0.1", "babel-plugin-inline-react-svg": "^2.0.1",
"bech32": "^2.0.0", "bech32": "^2.0.0",
"bolt11": "^1.3.4",
"bootstrap": "^4.6.0", "bootstrap": "^4.6.0",
"clipboard-copy": "^4.0.1", "clipboard-copy": "^4.0.1",
"domino": "^2.1.6", "domino": "^2.1.6",
@ -910,6 +911,14 @@
"@types/node": "*" "@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": { "node_modules/@types/body-parser": {
"version": "1.19.1", "version": "1.19.1",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.1.tgz", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.1.tgz",
@ -2096,6 +2105,53 @@
"node": ">=10.4.0" "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": { "node_modules/boolbase": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
@ -12218,6 +12274,14 @@
"@types/node": "*" "@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": { "@types/body-parser": {
"version": "1.19.1", "version": "1.19.1",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.1.tgz", "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", "resolved": "https://registry.npmjs.org/bolt09/-/bolt09-0.1.5.tgz",
"integrity": "sha512-oT1+erg21vat55oXNd7nNEkCO0FQnmaraFZuyXFyeVk7dZCm/3vgic0qK1VuUSV+ksYXJfRKYC4AqfYrtHNPZg==" "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": { "boolbase": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",

View File

@ -15,6 +15,7 @@
"async-retry": "^1.3.1", "async-retry": "^1.3.1",
"babel-plugin-inline-react-svg": "^2.0.1", "babel-plugin-inline-react-svg": "^2.0.1",
"bech32": "^2.0.0", "bech32": "^2.0.0",
"bolt11": "^1.3.4",
"bootstrap": "^4.6.0", "bootstrap": "^4.6.0",
"clipboard-copy": "^4.0.1", "clipboard-copy": "^4.0.1",
"domino": "^2.1.6", "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); 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 { body {
background: var(--theme-body); background: var(--theme-body);
color: var(--theme-color); 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;
}