diff --git a/api/resolvers/wallet.js b/api/resolvers/wallet.js index d034ec7d..9ea1270c 100644 --- a/api/resolvers/wallet.js +++ b/api/resolvers/wallet.js @@ -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 }) } diff --git a/api/typeDefs/wallet.js b/api/typeDefs/wallet.js index 06f03045..456c5339 100644 --- a/api/typeDefs/wallet.js +++ b/api/typeDefs/wallet.js @@ -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 } diff --git a/components/wallet/index.js b/components/wallet/index.js index 68034fa8..8b49c0fa 100644 --- a/components/wallet/index.js +++ b/components/wallet/index.js @@ -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', diff --git a/components/wallet/lightning-address.js b/components/wallet/lightning-address.js new file mode 100644 index 00000000..3ca77cda --- /dev/null +++ b/components/wallet/lightning-address.js @@ -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 + } +} diff --git a/components/wallet/lnd.js b/components/wallet/lnd.js index 0e9f6b69..62d647b7 100644 --- a/components/wallet/lnd.js +++ b/components/wallet/lnd.js @@ -87,7 +87,7 @@ export const server = { } }, createInvoice: async ( - amount, + { amount }, { cert, macaroon, socket }, { me, lnService: { authenticatedLndGrpc, createInvoice } } ) => { diff --git a/fragments/wallet.js b/fragments/wallet.js index 24b352c6..c33e1660 100644 --- a/fragments/wallet.js +++ b/fragments/wallet.js @@ -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!) { diff --git a/lib/wallet.js b/lib/wallet.js new file mode 100644 index 00000000..e7ee6c7b --- /dev/null +++ b/lib/wallet.js @@ -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 +} diff --git a/worker/autowithdraw.js b/worker/autowithdraw.js index c99b63b1..b64d242c 100644 --- a/worker/autowithdraw.js +++ b/worker/autowithdraw.js @@ -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')