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 ( + + + + + + + + + + + + {facts.map((f, i) => ( + + + + + + + + ))} + +
typedetailsats
{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