Add autowithdrawal to lightning address
This commit is contained in:
parent
8dac53d7d5
commit
1ce09051b1
|
@ -4,9 +4,8 @@ import crypto, { timingSafeEqual } from 'crypto'
|
|||
import serialize from './serial'
|
||||
import { decodeCursor, LIMIT, nextCursorEncoded } from '@/lib/cursor'
|
||||
import { SELECT, itemQueryWithMeta } from './item'
|
||||
import { lnAddrOptions } from '@/lib/lnurl'
|
||||
import { msatsToSats, msatsToSatsDecimal, ensureB64 } from '@/lib/format'
|
||||
import { CLNAutowithdrawSchema, amountSchema, lnAddrAutowithdrawSchema, lnAddrSchema, ssValidate, withdrawlSchema } from '@/lib/validate'
|
||||
import { CLNAutowithdrawSchema, amountSchema, ssValidate, withdrawlSchema } from '@/lib/validate'
|
||||
import { ANON_BALANCE_LIMIT_MSATS, ANON_INV_PENDING_LIMIT, USER_ID, BALANCE_LIMIT_MSATS, INVOICE_RETENTION_DAYS, INV_PENDING_LIMIT, USER_IDS_BALANCE_NO_LIMIT, Wallet } from '@/lib/constants'
|
||||
import { datePivot } from '@/lib/time'
|
||||
import assertGofacYourself from './ofac'
|
||||
|
@ -15,8 +14,10 @@ import { createInvoice as createInvoiceCLN } from '@/lib/cln'
|
|||
import { bolt11Tags } from '@/lib/bolt11'
|
||||
import { checkInvoice } from 'worker/wallet'
|
||||
import * as lnd from '@/components/wallet/lnd'
|
||||
import * as lnAddr from '@/components/wallet/lightning-address'
|
||||
import { fetchLnAddrInvoice } from '@/lib/wallet'
|
||||
|
||||
export const SERVER_WALLET_DEFS = [lnd]
|
||||
export const SERVER_WALLET_DEFS = [lnd, lnAddr]
|
||||
|
||||
function walletResolvers () {
|
||||
const resolvers = {}
|
||||
|
@ -481,20 +482,6 @@ export default {
|
|||
},
|
||||
{ settings, data }, { me, models })
|
||||
},
|
||||
upsertWalletLNAddr: async (parent, { settings, ...data }, { me, models }) => {
|
||||
const wallet = Wallet.LnAddr
|
||||
return await upsertWallet(
|
||||
{
|
||||
schema: lnAddrAutowithdrawSchema,
|
||||
wallet,
|
||||
testConnect: async ({ address }) => {
|
||||
const options = await lnAddrOptions(address)
|
||||
await addWalletLog({ wallet, level: 'SUCCESS', message: 'fetched payment details' }, { me, models })
|
||||
return options
|
||||
}
|
||||
},
|
||||
{ settings, data }, { me, models })
|
||||
},
|
||||
removeWallet: async (parent, { id }, { me, models }) => {
|
||||
if (!me) {
|
||||
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
|
||||
|
@ -746,64 +733,20 @@ export async function createWithdrawal (parent, { invoice, maxFee }, { me, model
|
|||
}
|
||||
|
||||
export async function sendToLnAddr (parent, { addr, amount, maxFee, comment, ...payer },
|
||||
{ me, models, lnd, headers, walletId }) {
|
||||
{ me, models, lnd, headers }) {
|
||||
if (!me) {
|
||||
throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
|
||||
}
|
||||
assertApiKeyNotPermitted({ me })
|
||||
|
||||
const options = await lnAddrOptions(addr)
|
||||
await ssValidate(lnAddrSchema, { addr, amount, maxFee, comment, ...payer }, options)
|
||||
|
||||
if (payer) {
|
||||
payer = {
|
||||
...payer,
|
||||
identifier: payer.identifier ? `${me.name}@stacker.news` : undefined
|
||||
}
|
||||
payer = Object.fromEntries(
|
||||
Object.entries(payer).filter(([, value]) => !!value)
|
||||
)
|
||||
}
|
||||
|
||||
const milliamount = 1000 * amount
|
||||
const callback = new URL(options.callback)
|
||||
callback.searchParams.append('amount', milliamount)
|
||||
|
||||
if (comment?.length) {
|
||||
callback.searchParams.append('comment', comment)
|
||||
}
|
||||
|
||||
let stringifiedPayerData = ''
|
||||
if (payer && Object.entries(payer).length) {
|
||||
stringifiedPayerData = JSON.stringify(payer)
|
||||
callback.searchParams.append('payerdata', stringifiedPayerData)
|
||||
}
|
||||
|
||||
// call callback with amount and conditionally comment
|
||||
const res = await (await fetch(callback.toString())).json()
|
||||
if (res.status === 'ERROR') {
|
||||
throw new Error(res.reason)
|
||||
}
|
||||
|
||||
// decode invoice
|
||||
try {
|
||||
const decoded = await decodePaymentRequest({ lnd, request: res.pr })
|
||||
const ourPubkey = (await getIdentity({ lnd })).public_key
|
||||
if (walletId && decoded.destination === ourPubkey && process.env.NODE_ENV === 'production') {
|
||||
// unset lnaddr so we don't trigger another withdrawal with same destination
|
||||
await models.wallet.deleteMany({
|
||||
where: { userId: me.id, type: Wallet.LnAddr.type }
|
||||
})
|
||||
throw new Error('automated withdrawals to other stackers are not allowed')
|
||||
}
|
||||
if (!decoded.mtokens || BigInt(decoded.mtokens) !== BigInt(milliamount)) {
|
||||
throw new Error('invoice has incorrect amount')
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
throw e
|
||||
}
|
||||
const res = await fetchLnAddrInvoice({ addr, amount, maxFee, comment, ...payer },
|
||||
{
|
||||
me,
|
||||
models,
|
||||
lnd,
|
||||
lnService: { decodePaymentRequest, getIdentity }
|
||||
})
|
||||
|
||||
// take pr and createWithdrawl
|
||||
return await createWithdrawal(parent, { invoice: res.pr, maxFee }, { me, models, lnd, headers, walletId })
|
||||
return await createWithdrawal(parent, { invoice: res.pr, maxFee }, { me, models, lnd, headers })
|
||||
}
|
||||
|
|
|
@ -39,7 +39,6 @@ export default gql`
|
|||
dropBolt11(id: ID): Withdrawl
|
||||
${walletTypeDefs()}
|
||||
upsertWalletCLN(id: ID, socket: String!, rune: String!, cert: String, settings: AutowithdrawSettings!): Boolean
|
||||
upsertWalletLNAddr(id: ID, address: String!, settings: AutowithdrawSettings!): Boolean
|
||||
removeWallet(id: ID!): Boolean
|
||||
deleteWalletLogs(wallet: String): Boolean
|
||||
}
|
||||
|
|
|
@ -9,12 +9,13 @@ import * as lnbits from '@/components/wallet/lnbits'
|
|||
import * as nwc from '@/components/wallet/nwc'
|
||||
import * as lnc from '@/components/wallet/lnc'
|
||||
import * as lnd from '@/components/wallet/lnd'
|
||||
import * as lnAddr from '@/components/wallet/lightning-address'
|
||||
import { gql, useApolloClient, useQuery } from '@apollo/client'
|
||||
import { REMOVE_WALLET, WALLET_BY_TYPE } from '@/fragments/wallet'
|
||||
import { autowithdrawInitial } from '../autowithdraw-shared'
|
||||
|
||||
// wallet definitions
|
||||
export const WALLET_DEFS = [lnbits, nwc, lnc, lnd]
|
||||
export const WALLET_DEFS = [lnbits, nwc, lnc, lnd, lnAddr]
|
||||
|
||||
export const Status = {
|
||||
Initialized: 'Initialized',
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
import { lnAddrOptions } from '@/lib/lnurl'
|
||||
import { lnAddrAutowithdrawSchema } from '@/lib/validate'
|
||||
import { fetchLnAddrInvoice } from '@/lib/wallet'
|
||||
|
||||
export const name = 'lightning-address'
|
||||
|
||||
export const fields = [
|
||||
{
|
||||
name: 'address',
|
||||
label: 'lightning address',
|
||||
type: 'text',
|
||||
hint: 'tor or clearnet',
|
||||
autoComplete: 'off'
|
||||
}
|
||||
]
|
||||
|
||||
export const card = {
|
||||
title: 'lightning address',
|
||||
subtitle: 'autowithdraw to a lightning address',
|
||||
badges: ['receive only', 'non-custodialish']
|
||||
}
|
||||
|
||||
export const schema = lnAddrAutowithdrawSchema
|
||||
|
||||
export const server = {
|
||||
walletType: 'LIGHTNING_ADDRESS',
|
||||
walletField: 'walletLightningAddress',
|
||||
resolverName: 'upsertWalletLNAddr',
|
||||
testConnect: async (
|
||||
{ address },
|
||||
{ me, models, addWalletLog }
|
||||
) => {
|
||||
const options = await lnAddrOptions(address)
|
||||
await addWalletLog({ wallet: { type: 'LIGHTNING_ADDRESS' }, level: 'SUCCESS', message: 'fetched payment details' }, { me, models })
|
||||
return options
|
||||
},
|
||||
createInvoice: async (
|
||||
{ amount, maxFee },
|
||||
{ address },
|
||||
{ me, models, lnd, lnService }
|
||||
) => {
|
||||
const res = await fetchLnAddrInvoice({ addr: address, amount, maxFee }, {
|
||||
me,
|
||||
models,
|
||||
lnd,
|
||||
lnService,
|
||||
autoWithdraw: true
|
||||
})
|
||||
return res.pr
|
||||
}
|
||||
}
|
|
@ -87,7 +87,7 @@ export const server = {
|
|||
}
|
||||
},
|
||||
createInvoice: async (
|
||||
amount,
|
||||
{ amount },
|
||||
{ cert, macaroon, socket },
|
||||
{ me, lnService: { authenticatedLndGrpc, createInvoice } }
|
||||
) => {
|
||||
|
|
|
@ -100,13 +100,6 @@ export const SEND_TO_LNADDR = gql`
|
|||
}
|
||||
}`
|
||||
|
||||
export const UPSERT_WALLET_LNADDR =
|
||||
gql`
|
||||
mutation upsertWalletLNAddr($id: ID, $address: String!, $settings: AutowithdrawSettings!) {
|
||||
upsertWalletLNAddr(id: $id, address: $address, settings: $settings)
|
||||
}
|
||||
`
|
||||
|
||||
export const UPSERT_WALLET_CLN =
|
||||
gql`
|
||||
mutation upsertWalletCLN($id: ID, $socket: String!, $rune: String!, $cert: String, $settings: AutowithdrawSettings!) {
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
import { lnAddrOptions } from './lnurl'
|
||||
import { lnAddrSchema, ssValidate } from './validate'
|
||||
|
||||
export async function fetchLnAddrInvoice ({ addr, amount, maxFee, comment, ...payer },
|
||||
{
|
||||
me, models, lnd, autoWithdraw = false,
|
||||
lnService: { decodePaymentRequest, getIdentity }
|
||||
}) {
|
||||
const options = await lnAddrOptions(addr)
|
||||
await ssValidate(lnAddrSchema, { addr, amount, maxFee, comment, ...payer }, options)
|
||||
|
||||
if (payer) {
|
||||
payer = {
|
||||
...payer,
|
||||
identifier: payer.identifier ? `${me.name}@stacker.news` : undefined
|
||||
}
|
||||
payer = Object.fromEntries(
|
||||
Object.entries(payer).filter(([, value]) => !!value)
|
||||
)
|
||||
}
|
||||
|
||||
const milliamount = 1000 * amount
|
||||
const callback = new URL(options.callback)
|
||||
callback.searchParams.append('amount', milliamount)
|
||||
|
||||
if (comment?.length) {
|
||||
callback.searchParams.append('comment', comment)
|
||||
}
|
||||
|
||||
let stringifiedPayerData = ''
|
||||
if (payer && Object.entries(payer).length) {
|
||||
stringifiedPayerData = JSON.stringify(payer)
|
||||
callback.searchParams.append('payerdata', stringifiedPayerData)
|
||||
}
|
||||
|
||||
// call callback with amount and conditionally comment
|
||||
const res = await (await fetch(callback.toString())).json()
|
||||
if (res.status === 'ERROR') {
|
||||
throw new Error(res.reason)
|
||||
}
|
||||
|
||||
// decode invoice
|
||||
try {
|
||||
const decoded = await decodePaymentRequest({ lnd, request: res.pr })
|
||||
const ourPubkey = (await getIdentity({ lnd })).public_key
|
||||
if (autoWithdraw && decoded.destination === ourPubkey && process.env.NODE_ENV === 'production') {
|
||||
// unset lnaddr so we don't trigger another withdrawal with same destination
|
||||
await models.wallet.deleteMany({
|
||||
// TODO: replace hardcoded 'LIGHTNING_ADDRESS' with wallet.type
|
||||
where: { userId: me.id, type: 'LIGHTNING_ADDRESS' }
|
||||
})
|
||||
throw new Error('automated withdrawals to other stackers are not allowed')
|
||||
}
|
||||
if (!decoded.mtokens || BigInt(decoded.mtokens) !== BigInt(milliamount)) {
|
||||
throw new Error('invoice has incorrect amount')
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
throw e
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { authenticatedLndGrpc, createInvoice as lndCreateInvoice } from 'ln-service'
|
||||
import { authenticatedLndGrpc, createInvoice as lndCreateInvoice, getIdentity, decodePaymentRequest } from 'ln-service'
|
||||
import { msatsToSats, satsToMsats } from '@/lib/format'
|
||||
// import { datePivot } from '@/lib/time'
|
||||
import { createWithdrawal, /* sendToLnAddr, */ addWalletLog, SERVER_WALLET_DEFS } from '@/api/resolvers/wallet'
|
||||
|
@ -61,20 +61,12 @@ export async function autoWithdraw ({ data: { id }, models, lnd }) {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: implement CLN and LnAddr wallets
|
||||
// TODO: implement CLN autowithdrawal
|
||||
// ------
|
||||
// if (wallet.type === Wallet.LND.type) {
|
||||
// await autowithdrawLND(
|
||||
// { amount, maxFee },
|
||||
// { models, me: user, lnd })
|
||||
// } else if (wallet.type === Wallet.CLN.type) {
|
||||
// if (wallet.type === Wallet.CLN.type) {
|
||||
// await autowithdrawCLN(
|
||||
// { amount, maxFee },
|
||||
// { models, me: user, lnd })
|
||||
// } else if (wallet.type === Wallet.LnAddr.type) {
|
||||
// await autowithdrawLNAddr(
|
||||
// { amount, maxFee },
|
||||
// { models, me: user, lnd })
|
||||
// }
|
||||
|
||||
return
|
||||
|
@ -115,36 +107,24 @@ async function autowithdraw (
|
|||
throw new Error(`no ${walletType} wallet found`)
|
||||
}
|
||||
|
||||
const bolt11 = await walletCreateInvoice(amount, wallet[walletField], { me, lnService: { authenticatedLndGrpc, createInvoice: lndCreateInvoice } })
|
||||
const bolt11 = await walletCreateInvoice(
|
||||
{ amount, maxFee },
|
||||
wallet[walletField],
|
||||
{
|
||||
me,
|
||||
models,
|
||||
lnd,
|
||||
lnService: {
|
||||
authenticatedLndGrpc,
|
||||
createInvoice: lndCreateInvoice,
|
||||
getIdentity,
|
||||
decodePaymentRequest
|
||||
}
|
||||
})
|
||||
|
||||
return await createWithdrawal(null, { invoice: bolt11, maxFee }, { me, models, lnd, walletId: wallet.id })
|
||||
}
|
||||
|
||||
// async function autowithdrawLNAddr (
|
||||
// { amount, maxFee },
|
||||
// { me, models, lnd, headers, autoWithdraw = false }) {
|
||||
// if (!me) {
|
||||
// throw new Error('me not specified')
|
||||
// }
|
||||
//
|
||||
// const wallet = await models.wallet.findFirst({
|
||||
// where: {
|
||||
// userId: me.id,
|
||||
// type: Wallet.LnAddr.type
|
||||
// },
|
||||
// include: {
|
||||
// walletLightningAddress: true
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// if (!wallet || !wallet.walletLightningAddress) {
|
||||
// throw new Error('no lightning address wallet found')
|
||||
// }
|
||||
//
|
||||
// const { walletLightningAddress: { address } } = wallet
|
||||
// return await sendToLnAddr(null, { addr: address, amount, maxFee }, { me, models, lnd, walletId: wallet.id })
|
||||
// }
|
||||
|
||||
// async function autowithdrawCLN ({ amount, maxFee }, { me, models, lnd }) {
|
||||
// if (!me) {
|
||||
// throw new Error('me not specified')
|
||||
|
|
Loading…
Reference in New Issue