diff --git a/api/resolvers/wallet.js b/api/resolvers/wallet.js index 32d6e981..3e427dc1 100644 --- a/api/resolvers/wallet.js +++ b/api/resolvers/wallet.js @@ -1,4 +1,4 @@ -import { createHodlInvoice, createInvoice, decodePaymentRequest, payViaPaymentRequest, cancelHodlInvoice, getInvoice as getInvoiceFromLnd, getNode } from 'ln-service' +import { getIdentity, createHodlInvoice, createInvoice, decodePaymentRequest, payViaPaymentRequest, cancelHodlInvoice, getInvoice as getInvoiceFromLnd, getNode } from 'ln-service' import { GraphQLError } from 'graphql' import crypto from 'crypto' import serialize from './serial' @@ -475,6 +475,19 @@ export async function sendToLnAddr (parent, { addr, amount, maxFee, comment, ... // decode invoice try { const decoded = await decodePaymentRequest({ lnd, request: res.pr }) + const ourPubkey = (await getIdentity({ lnd })).public_key + if (autoWithdraw && decoded.destination === ourPubkey) { + // unset lnaddr so we don't trigger another withdrawal with same destination + await models.user.update({ + where: { id: me.id }, + data: { + lnAddr: null, + autoWithdrawThreshold: null, + autoWithdrawMaxFeePercent: null + } + }) + throw new Error('automated withdrawals to other stackers are not allowed') + } if (decoded.description_hash !== lnurlPayDescriptionHash(`${options.metadata}${stringifiedPayerData}`)) { throw new Error('description hash does not match') } diff --git a/lib/lnurl.js b/lib/lnurl.js index e1584008..a550cb81 100644 --- a/lib/lnurl.js +++ b/lib/lnurl.js @@ -28,7 +28,11 @@ export function lnurlPayDescriptionHash (data) { export async function lnAddrOptions (addr) { await lnAddrSchema().fields.addr.validate(addr) const [name, domain] = addr.split('@') - const protocol = domain.includes(':') && process.env.NODE_ENV === 'development' ? 'http' : 'https' + let protocol = 'https' + if (process.env.NODE_ENV === 'development') { + // support HTTP and HTTPS during development + protocol = process.env.PUBLIC_URL.split('://')[0] + } const req = await fetch(`${protocol}://${domain}/.well-known/lnurlp/${name}`) const res = await req.json() if (res.status === 'ERROR') { diff --git a/lib/validate.js b/lib/validate.js index dc840298..30d8dc0f 100644 --- a/lib/validate.js +++ b/lib/validate.js @@ -208,10 +208,8 @@ export function lnAddrAutowithdrawSchema ({ me } = {}) { return object({ lnAddr: lightningAddressValidator.required('required').test({ name: 'lnAddr', - test: async addr => { - return addr !== `${me.name}@stacker.news` && !addr.startsWith(`${me.name}@localhost`) - }, - message: 'cannot send to yourself' + test: addr => !addr.endsWith('@stacker.news'), + message: 'automated withdrawals must be external' }), autoWithdrawThreshold: intValidator.required('required').min(0, 'must be at least 0').max(msatsToSats(BALANCE_LIMIT_MSATS), `must be at most ${abbrNum(msatsToSats(BALANCE_LIMIT_MSATS))}`), autoWithdrawMaxFeePercent: floatValidator.required('required').min(0, 'must be at least 0').max(50, 'must not exceed 50') @@ -444,3 +442,7 @@ export const lud18PayerDataSchema = (k1) => object({ email: string().email('bad email address'), identifier: string() }) + +// check if something is _really_ a number. +// returns true for every number in this range: [-Infinity, ..., 0, ..., Infinity] +export const isNumber = x => typeof x === 'number' && !Number.isNaN(x) diff --git a/pages/settings/wallets/lightning-address.js b/pages/settings/wallets/lightning-address.js index 8fd04f50..bec0f74a 100644 --- a/pages/settings/wallets/lightning-address.js +++ b/pages/settings/wallets/lightning-address.js @@ -7,7 +7,7 @@ import { WalletButtonBar, WalletCard } from '../../../components/wallet-card' import { useMutation } from '@apollo/client' import { REMOVE_AUTOWITHDRAW, SET_AUTOWITHDRAW } from '../../../fragments/users' import { useToast } from '../../../components/toast' -import { lnAddrAutowithdrawSchema } from '../../../lib/validate' +import { lnAddrAutowithdrawSchema, isNumber } from '../../../lib/validate' import { useRouter } from 'next/router' import { useEffect, useState } from 'react' @@ -15,7 +15,7 @@ export const getServerSideProps = getGetServerSideProps({ authRequired: true }) function useAutoWithdrawEnabled () { const me = useMe() - return me?.privates?.lnAddr && !isNaN(me?.privates?.autoWithdrawThreshold) && !isNaN(me?.privates?.autoWithdrawMaxFeePercent) + return me?.privates?.lnAddr && isNumber(me?.privates?.autoWithdrawThreshold) && isNumber(me?.privates?.autoWithdrawMaxFeePercent) } export default function LightningAddress () { @@ -25,7 +25,7 @@ export default function LightningAddress () { const [setAutoWithdraw] = useMutation(SET_AUTOWITHDRAW) const enabled = useAutoWithdrawEnabled() const [removeAutoWithdraw] = useMutation(REMOVE_AUTOWITHDRAW) - const autoWithdrawThreshold = isNaN(me?.privates?.autoWithdrawThreshold) ? 10000 : me?.privates?.autoWithdrawThreshold + const autoWithdrawThreshold = isNumber(me?.privates?.autoWithdrawThreshold) ? me?.privates?.autoWithdrawThreshold : 10000 const [sendThreshold, setSendThreshold] = useState(Math.max(Math.floor(autoWithdrawThreshold / 10), 1)) useEffect(() => { @@ -39,8 +39,8 @@ export default function LightningAddress () {
{ @@ -73,7 +73,7 @@ export default function LightningAddress () { const value = e.target.value setSendThreshold(Math.max(Math.floor(value / 10), 1)) }} - hint={isNaN(sendThreshold) ? undefined : `note: will attempt withdrawal when desired balance is exceeded by ${sendThreshold} sats`} + hint={isNumber(sendThreshold) ? `note: will attempt withdraw when threshold is exceeded by ${sendThreshold} sats` : undefined} append={sats} />