Merge pull request #748 from stackernews/524-autowithdraw-lnaddr-review
Small changes regarding automated withdrawals
This commit is contained in:
commit
11b05b0f8a
|
@ -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 { GraphQLError } from 'graphql'
|
||||||
import crypto from 'crypto'
|
import crypto from 'crypto'
|
||||||
import serialize from './serial'
|
import serialize from './serial'
|
||||||
|
@ -475,6 +475,19 @@ export async function sendToLnAddr (parent, { addr, amount, maxFee, comment, ...
|
||||||
// decode invoice
|
// decode invoice
|
||||||
try {
|
try {
|
||||||
const decoded = await decodePaymentRequest({ lnd, request: res.pr })
|
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}`)) {
|
if (decoded.description_hash !== lnurlPayDescriptionHash(`${options.metadata}${stringifiedPayerData}`)) {
|
||||||
throw new Error('description hash does not match')
|
throw new Error('description hash does not match')
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,11 @@ export function lnurlPayDescriptionHash (data) {
|
||||||
export async function lnAddrOptions (addr) {
|
export async function lnAddrOptions (addr) {
|
||||||
await lnAddrSchema().fields.addr.validate(addr)
|
await lnAddrSchema().fields.addr.validate(addr)
|
||||||
const [name, domain] = addr.split('@')
|
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 req = await fetch(`${protocol}://${domain}/.well-known/lnurlp/${name}`)
|
||||||
const res = await req.json()
|
const res = await req.json()
|
||||||
if (res.status === 'ERROR') {
|
if (res.status === 'ERROR') {
|
||||||
|
|
|
@ -208,10 +208,8 @@ export function lnAddrAutowithdrawSchema ({ me } = {}) {
|
||||||
return object({
|
return object({
|
||||||
lnAddr: lightningAddressValidator.required('required').test({
|
lnAddr: lightningAddressValidator.required('required').test({
|
||||||
name: 'lnAddr',
|
name: 'lnAddr',
|
||||||
test: async addr => {
|
test: addr => !addr.endsWith('@stacker.news'),
|
||||||
return addr !== `${me.name}@stacker.news` && !addr.startsWith(`${me.name}@localhost`)
|
message: 'automated withdrawals must be external'
|
||||||
},
|
|
||||||
message: 'cannot send to yourself'
|
|
||||||
}),
|
}),
|
||||||
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))}`),
|
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')
|
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'),
|
email: string().email('bad email address'),
|
||||||
identifier: string()
|
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)
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { WalletButtonBar, WalletCard } from '../../../components/wallet-card'
|
||||||
import { useMutation } from '@apollo/client'
|
import { useMutation } from '@apollo/client'
|
||||||
import { REMOVE_AUTOWITHDRAW, SET_AUTOWITHDRAW } from '../../../fragments/users'
|
import { REMOVE_AUTOWITHDRAW, SET_AUTOWITHDRAW } from '../../../fragments/users'
|
||||||
import { useToast } from '../../../components/toast'
|
import { useToast } from '../../../components/toast'
|
||||||
import { lnAddrAutowithdrawSchema } from '../../../lib/validate'
|
import { lnAddrAutowithdrawSchema, isNumber } from '../../../lib/validate'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ export const getServerSideProps = getGetServerSideProps({ authRequired: true })
|
||||||
|
|
||||||
function useAutoWithdrawEnabled () {
|
function useAutoWithdrawEnabled () {
|
||||||
const me = useMe()
|
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 () {
|
export default function LightningAddress () {
|
||||||
|
@ -25,7 +25,7 @@ export default function LightningAddress () {
|
||||||
const [setAutoWithdraw] = useMutation(SET_AUTOWITHDRAW)
|
const [setAutoWithdraw] = useMutation(SET_AUTOWITHDRAW)
|
||||||
const enabled = useAutoWithdrawEnabled()
|
const enabled = useAutoWithdrawEnabled()
|
||||||
const [removeAutoWithdraw] = useMutation(REMOVE_AUTOWITHDRAW)
|
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))
|
const [sendThreshold, setSendThreshold] = useState(Math.max(Math.floor(autoWithdrawThreshold / 10), 1))
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -39,8 +39,8 @@ export default function LightningAddress () {
|
||||||
<Form
|
<Form
|
||||||
initial={{
|
initial={{
|
||||||
lnAddr: me?.privates?.lnAddr || '',
|
lnAddr: me?.privates?.lnAddr || '',
|
||||||
autoWithdrawThreshold: isNaN(me?.privates?.autoWithdrawThreshold) ? 10000 : me?.privates?.autoWithdrawThreshold,
|
autoWithdrawThreshold,
|
||||||
autoWithdrawMaxFeePercent: isNaN(me?.privates?.autoWithdrawMaxFeePercent) ? 1 : me?.privates?.autoWithdrawMaxFeePercent
|
autoWithdrawMaxFeePercent: isNumber(me?.privates?.autoWithdrawMaxFeePercent) ? me?.privates?.autoWithdrawMaxFeePercent : 1
|
||||||
}}
|
}}
|
||||||
schema={lnAddrAutowithdrawSchema({ me })}
|
schema={lnAddrAutowithdrawSchema({ me })}
|
||||||
onSubmit={async ({ autoWithdrawThreshold, autoWithdrawMaxFeePercent, ...values }) => {
|
onSubmit={async ({ autoWithdrawThreshold, autoWithdrawMaxFeePercent, ...values }) => {
|
||||||
|
@ -73,7 +73,7 @@ export default function LightningAddress () {
|
||||||
const value = e.target.value
|
const value = e.target.value
|
||||||
setSendThreshold(Math.max(Math.floor(value / 10), 1))
|
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={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
|
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { INVOICE_RETENTION_DAYS } from '../lib/constants'
|
||||||
import { sleep } from '../lib/time.js'
|
import { sleep } from '../lib/time.js'
|
||||||
import { sendToLnAddr } from '../api/resolvers/wallet.js'
|
import { sendToLnAddr } from '../api/resolvers/wallet.js'
|
||||||
import retry from 'async-retry'
|
import retry from 'async-retry'
|
||||||
|
import { isNumber } from '../lib/validate.js'
|
||||||
|
|
||||||
export async function subscribeToWallet (args) {
|
export async function subscribeToWallet (args) {
|
||||||
await subscribeToDeposits(args)
|
await subscribeToDeposits(args)
|
||||||
|
@ -292,8 +293,8 @@ export async function autoWithdraw ({ data: { id }, models, lnd }) {
|
||||||
const user = await models.user.findUnique({ where: { id } })
|
const user = await models.user.findUnique({ where: { id } })
|
||||||
if (!user ||
|
if (!user ||
|
||||||
!user.lnAddr ||
|
!user.lnAddr ||
|
||||||
isNaN(user.autoWithdrawThreshold) ||
|
!isNumber(user.autoWithdrawThreshold) ||
|
||||||
isNaN(user.autoWithdrawMaxFeePercent)) return
|
!isNumber(user.autoWithdrawMaxFeePercent)) return
|
||||||
|
|
||||||
const threshold = satsToMsats(user.autoWithdrawThreshold)
|
const threshold = satsToMsats(user.autoWithdrawThreshold)
|
||||||
const excess = Number(user.msats - threshold)
|
const excess = Number(user.msats - threshold)
|
||||||
|
|
Loading…
Reference in New Issue