diff --git a/api/resolvers/wallet.js b/api/resolvers/wallet.js
index d758379c..8a8cd9de 100644
--- a/api/resolvers/wallet.js
+++ b/api/resolvers/wallet.js
@@ -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,
diff --git a/api/typeDefs/wallet.js b/api/typeDefs/wallet.js
index 0aa35bee..ae444ab6 100644
--- a/api/typeDefs/wallet.js
+++ b/api/typeDefs/wallet.js
@@ -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 {
diff --git a/components/user-header.js b/components/user-header.js
index fded93ef..84e08104 100644
--- a/components/user-header.js
+++ b/components/user-header.js
@@ -32,6 +32,7 @@ export default function UserHeader ({ user }) {
const [setName] = useMutation(NAME_MUTATION)
const Satistics = () =>
{user.sats} sats \ {user.stacked} stacked
+ const isMe = me?.name === user.name
const UserSchema = Yup.object({
name: Yup.string()
@@ -104,7 +105,7 @@ export default function UserHeader ({ user }) {
: (
@{user.name}
- {me?.name === user.name &&
+ {isMe &&
}
)}
@@ -129,11 +130,12 @@ export default function UserHeader ({ user }) {
{user.ncomments} comments
- {/*
-
- satistics
-
- */}
+ {isMe &&
+
+
+ satistics
+
+ }
>
)
diff --git a/fragments/wallet.js b/fragments/wallet.js
index 1ae330b3..4ccfd425 100644
--- a/fragments/wallet.js
+++ b/fragments/wallet.js
@@ -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) {
diff --git a/lib/apollo.js b/lib/apollo.js
index 96e1e75f..c2ac272d 100644
--- a/lib/apollo.js
+++ b/lib/apollo.js
@@ -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]
+ }
+ }
}
}
}
diff --git a/package-lock.json b/package-lock.json
index c0035260..647ba6e7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index df40637b..ec2284c8 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/pages/satistics.js b/pages/satistics.js
new file mode 100644
index 00000000..9c59afdb
--- /dev/null
+++ b/pages/satistics.js
@@ -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
+ case 'PENDING':
+ return
+ default:
+ return
+ }
+ }
+
+ return (
+
+ {' ' + desc()}
+
+ )
+}
+
+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 = () => (
+
+
+
)
+
+ return (
+
+
+
+
+
+ type |
+ detail |
+ sats |
+
+
+
+ {facts.map((f, i) => (
+
+
+ {f.type} |
+
+
+ {f.description || 'no description'}
+
+
+ |
+ {f.msats / 1000} |
+
+
+ ))}
+
+
+
+
+ )
+}
diff --git a/styles/globals.scss b/styles/globals.scss
index c25efbb7..89b9eeba 100644
--- a/styles/globals.scss
+++ b/styles/globals.scss
@@ -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);
diff --git a/styles/satistics.module.css b/styles/satistics.module.css
new file mode 100644
index 00000000..ef99be1b
--- /dev/null
+++ b/styles/satistics.module.css
@@ -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;
+}
\ No newline at end of file