diff --git a/api/resolvers/wallet.js b/api/resolvers/wallet.js index e7d974934..a69860de9 100644 --- a/api/resolvers/wallet.js +++ b/api/resolvers/wallet.js @@ -7,7 +7,7 @@ import lnpr from 'bolt11' import { SELECT } from './item' import { lnAddrOptions } from '../../lib/lnurl' import { msatsToSats, msatsToSatsDecimal, ensureB64 } from '../../lib/format' -import { LNDAutowithdrawSchema, amountSchema, lnAddrAutowithdrawSchema, lnAddrSchema, ssValidate, withdrawlSchema } from '../../lib/validate' +import { LNDAutowithdrawSchema, amountSchema, lnAddrAutowithdrawSchema, lnAddrSchema, ssValidate, withdrawlSchema, CoreLightningAutowithdrawSchema } from '../../lib/validate' import { ANON_BALANCE_LIMIT_MSATS, ANON_INV_PENDING_LIMIT, ANON_USER_ID, BALANCE_LIMIT_MSATS, INVOICE_RETENTION_DAYS, INV_PENDING_LIMIT, USER_IDS_BALANCE_NO_LIMIT } from '../../lib/constants' import { datePivot } from '../../lib/time' import assertGofacYourself from './ofac' @@ -304,7 +304,13 @@ export default { }, WalletDetails: { __resolveType (wallet) { - return wallet.address ? 'WalletLNAddr' : 'WalletLND' + if (wallet.address) { + return 'WalletLNAddr' + } else if (wallet.type === 'LND') { + return 'WalletLND' + } else { + return 'WalletCoreLightning' + } } }, Mutation: { @@ -432,6 +438,29 @@ export default { }, { settings, data }, { me, models }) }, + + upsertWalletCoreLightning: async (parent, { settings, ...data }, { me, models }) => { + return await upsertWallet( + { + schema: CoreLightningAutowithdrawSchema, + walletName: 'walletCoreLightning', + walletType: 'CORE_LIGHTNING', + testConnect: async ({ rune, socket }) => { + const options = { + method: 'POST', + headers: { + 'content-type': 'application/json', + Rune: rune + }, + body: JSON.stringify({ amount_msat: 0, label: 'SN connection test', description: 'SN connection test' }) + } + + return await fetch(`${socket}/v1/invoice`, options) + } + }, + { settings, data }, { me, models }) + }, + upsertWalletLNAddr: async (parent, { settings, ...data }, { me, models }) => { return await upsertWallet( { diff --git a/api/typeDefs/wallet.js b/api/typeDefs/wallet.js index c2cca6aef..7bf4d5304 100644 --- a/api/typeDefs/wallet.js +++ b/api/typeDefs/wallet.js @@ -19,6 +19,7 @@ export default gql` cancelInvoice(hash: String!, hmac: String!): Invoice! dropBolt11(id: ID): Withdrawl upsertWalletLND(id: ID, socket: String!, macaroon: String!, cert: String, settings: AutowithdrawSettings!): Boolean + upsertWalletCoreLightning(id: ID, socket: String!, rune: String!, settings: AutowithdrawSettings!): Boolean upsertWalletLNAddr(id: ID, address: String!, settings: AutowithdrawSettings!): Boolean removeWallet(id: ID!): Boolean } @@ -41,7 +42,12 @@ export default gql` cert: String } - union WalletDetails = WalletLNAddr | WalletLND + type WalletCoreLightning { + socket: String! + rune: String! + } + + union WalletDetails = WalletLNAddr | WalletLND | WalletCoreLightning input AutowithdrawSettings { autoWithdrawThreshold: Int! diff --git a/fragments/wallet.js b/fragments/wallet.js index f1a3c09e3..1bf3e4048 100644 --- a/fragments/wallet.js +++ b/fragments/wallet.js @@ -89,6 +89,13 @@ mutation upsertWalletLND($id: ID, $socket: String!, $macaroon: String!, $cert: S } ` +export const UPSERT_WALLET_CORE_LIGHTNING = +gql` +mutation upsertWalletCoreLightning($id: ID, $socket: String!, $rune: String!, $settings: AutowithdrawSettings!) { + upsertWalletCoreLightning(id: $id, socket: $socket, rune: $rune, settings: $settings) +} +` + export const REMOVE_WALLET = gql` mutation removeWallet($id: ID!) { @@ -113,6 +120,10 @@ export const WALLET = gql` macaroon cert } + ... on WalletCoreLightning { + socket + rune + } } } } @@ -135,6 +146,10 @@ export const WALLET_BY_TYPE = gql` macaroon cert } + ... on WalletCoreLightning { + socket + rune + } } } } diff --git a/lib/validate.js b/lib/validate.js index 571741e94..77cb596d0 100644 --- a/lib/validate.js +++ b/lib/validate.js @@ -329,6 +329,39 @@ export function LNDAutowithdrawSchema ({ me } = {}) { }) } +async function isInvoiceOnlyRune (socket, rune) { + const url = 'https://cln-regtest-demo.blockstream.com/v1/decode' + const options = { + method: 'POST', + headers: { + 'content-type': 'application/json', + Rune: 'YuJqHOZLvCCiL0bY3mT75V_KX_dYNdhTlspcAkGZL5w9MA==' + }, + body: JSON.stringify({ string: 'zwcfm5tup11kf2fEObNDDPAcbNE-c_0m4KzbOpvt9gs9MTQmbWV0aG9kPWludm9pY2U=' }) + } + + fetch(url, options).then(res => res.json()).then((response) => { + const requiredResponse = 'method (of command) equal to \'invoice\'' + if (!response.restrictions && requiredResponse !== response.restrictions[0].alternatives[0].summary && response.restrictions.length > 1) { + return false + } else { + return true + } + }) +} + +export function CoreLightningAutowithdrawSchema ({ me, socket } = {}) { + return object({ + socket: string().socket().required('required'), + rune: hexOrBase64Validator.required('required').test({ + name: 'rune', + test: v => isInvoiceOnlyRune(string().socket(), v), + message: 'rune is not for invoice only' + }), + ...autowithdrawSchemaMembers({ me }) + }) +} + export function autowithdrawSchemaMembers ({ me } = {}) { return { priority: boolean(), diff --git a/pages/settings/wallets/core-lightning.js b/pages/settings/wallets/core-lightning.js new file mode 100644 index 000000000..b51b0477d --- /dev/null +++ b/pages/settings/wallets/core-lightning.js @@ -0,0 +1,114 @@ +import { getGetServerSideProps } from '../../../api/ssrApollo' +import { Form, Input } from '../../../components/form' +import { CenterLayout } from '../../../components/layout' +import { useMe } from '../../../components/me' +import { WalletButtonBar, WalletCard } from '../../../components/wallet-card' +import { useMutation } from '@apollo/client' +import { useToast } from '../../../components/toast' +import { CoreLightningAutowithdrawSchema } from '../../../lib/validate' +import { useRouter } from 'next/router' +import { AutowithdrawSettings, autowithdrawInitial } from '../../../components/autowithdraw-shared' +import { REMOVE_WALLET, UPSERT_WALLET_CORE_LIGHTNING, WALLET_BY_TYPE } from '../../../fragments/wallet' +import Info from '../../../components/info' +import Text from '../../../components/text' + +const variables = { type: 'CORE_LIGHTNING' } +export const getServerSideProps = getGetServerSideProps({ query: WALLET_BY_TYPE, variables, authRequired: true }) + +export default function CoreLightning ({ ssrData }) { + const me = useMe() + const toaster = useToast() + const router = useRouter() + const [upsertWalletCoreLightning] = useMutation(UPSERT_WALLET_CORE_LIGHTNING) + const [removeWallet] = useMutation(REMOVE_WALLET) + + const { walletByType: wallet } = ssrData || {} + + return ( + +

Core Lightning

+
autowithdraw to your Core Lightning node
+
You must have CLNRest working on your node. More info here.
+ +
{ + try { + await upsertWalletCoreLightning({ + variables: { + id: wallet?.id, + socket, + rune, + settings: { + ...settings, + autoWithdrawThreshold: Number(settings.autoWithdrawThreshold), + autoWithdrawMaxFeePercent: Number(settings.autoWithdrawMaxFeePercent) + } + } + }) + toaster.success('saved settings') + router.push('/settings/wallets') + } catch (err) { + console.error(err) + toaster.danger('failed to attach: ' + err.message || err.toString?.()) + } + }} + > + + Invoice Only Rune + + + {'***invoice only rune*** for your convenience. To gain better privacy, generate a new rune as follows:\n\n```lightning-cli createrune restrictions=invoice```\n\nfor older core lightning versions use ```lightning-cli commando-rune restrictions=method=invoice```'} + + + + } + name='rune' + clear + hint='base64 encoded' + placeholder='AgEDbG5kAlgDChCn7YgfWX7uTkQQgXZ2uahNEgEwGhYKB2FkZHJlc3MSBHJlYWQSBXdyaXRlGhcKCGludm9pY2VzEgRyZWFkEgV3cml0ZRoPCgdvbmNoYWluEgRyZWFkAAAGIJkMBrrDV0npU90JV0TGNJPrqUD8m2QYoTDjolaL6eBs' + required + /> + + { + try { + await removeWallet({ variables: { id: wallet?.id } }) + toaster.success('saved settings') + router.push('/settings/wallets') + } catch (err) { + console.error(err) + toaster.danger('failed to unattach:' + err.message || err.toString?.()) + } + }} + /> + +
+ ) +} + +export function CoreLightningCard ({ wallet }) { + return ( + + ) +} diff --git a/pages/settings/wallets/index.js b/pages/settings/wallets/index.js index a9ffe44ce..1019cb575 100644 --- a/pages/settings/wallets/index.js +++ b/pages/settings/wallets/index.js @@ -9,6 +9,7 @@ import { LNDCard } from './lnd' import { WALLETS } from '../../../fragments/wallet' import { useQuery } from '@apollo/client' import PageLoading from '../../../components/page-loading' +import { CoreLightningCard } from './core-lightning' export const getServerSideProps = getGetServerSideProps({ query: WALLETS, authRequired: true }) @@ -19,6 +20,7 @@ export default function Wallet ({ ssrData }) { const { wallets } = data || ssrData const lnd = wallets.find(w => w.type === 'LND') const lnaddr = wallets.find(w => w.type === 'LIGHTNING_ADDRESS') + const coreLightning = wallets.find(w => w.type === 'CORE_LIGHTNING') return ( @@ -30,7 +32,7 @@ export default function Wallet ({ ssrData }) { - + diff --git a/prisma/migrations/20240216221439_add_core_lightning_to_attached_wallets/migration.sql b/prisma/migrations/20240216221439_add_core_lightning_to_attached_wallets/migration.sql new file mode 100644 index 000000000..e50221ab6 --- /dev/null +++ b/prisma/migrations/20240216221439_add_core_lightning_to_attached_wallets/migration.sql @@ -0,0 +1,20 @@ +ALTER TYPE "WalletType" ADD VALUE 'CORE_LIGHTNING'; + +CREATE TABLE "WalletCoreLightning" ( + "id" SERIAL NOT NULL, + "walletId" INTEGER NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "socket" TEXT NOT NULL, + "rune" TEXT NOT NULL, + + CONSTRAINT "WalletCoreLightning_pkey" PRIMARY KEY ("id") +); + +CREATE UNIQUE INDEX "WalletCoreLightning_walletId_key" ON "WalletCoreLightning"("walletId"); + +ALTER TABLE "WalletCoreLightning" ADD CONSTRAINT "WalletCoreLightning_walletId_fkey" FOREIGN KEY ("walletId") REFERENCES "Wallet"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +CREATE TRIGGER wallet_core_lightning_as_jsonb +AFTER INSERT OR UPDATE ON "WalletCoreLightning" +FOR EACH ROW EXECUTE PROCEDURE wallet_wallet_type_as_jsonb(); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 22e520bbb..9183fb40c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -125,6 +125,7 @@ model User { enum WalletType { LIGHTNING_ADDRESS LND + CORE_LIGHTNING } model Wallet { @@ -146,6 +147,7 @@ model Wallet { wallet Json? @db.JsonB walletLightningAddress WalletLightningAddress? walletLND WalletLND? + walletCoreLightning WalletCoreLightning? @@index([userId]) } @@ -170,6 +172,16 @@ model WalletLND { cert String? } +model WalletCoreLightning { + id Int @id @default(autoincrement()) + walletId Int @unique + wallet Wallet @relation(fields: [walletId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @default(now()) @updatedAt @map("updated_at") + socket String + rune String +} + model Mute { muterId Int mutedId Int diff --git a/worker/autowithdraw.js b/worker/autowithdraw.js index 55010aeff..00ab055ba 100644 --- a/worker/autowithdraw.js +++ b/worker/autowithdraw.js @@ -53,6 +53,10 @@ export async function autoWithdraw ({ data: { id }, models, lnd }) { await autowithdrawLNAddr( { amount, maxFee }, { models, me: user, lnd }) + } else if (wallet.type === 'CORE_LIGHTNING') { + await autowithdrawCoreLightning( + { amount, maxFee }, + { models, me: user }) } return @@ -124,3 +128,41 @@ async function autowithdrawLND ({ amount, maxFee }, { me, models, lnd }) { return await createWithdrawal(null, { invoice: invoice.request, maxFee }, { me, models, lnd, autoWithdraw: true }) } + +async function autowithdrawCoreLightning ({ amount, maxFee }, { me, models }) { + if (!me) { + throw new Error('me not specified') + } + + const wallet = await models.wallet.findFirst({ + where: { + userId: me.id, + type: 'CORE_LIGHTNING' + }, + include: { + walletCoreLightning: true + } + }) + + if (!wallet || !wallet.walletCoreLightning) { + throw new Error('no lightning address wallet found') + } + + const { walletCoreLightning: { rune, socket } } = wallet + const options = { + method: 'POST', + headers: { + 'content-type': 'application/json', + Rune: rune + }, + body: JSON.stringify({ + amount_msat: '20', + label: 'Stacker.News AutoWithdrawal', + description: 'Autowithdraw to Core Lightning from SN' + }) + } + + const invoice = await fetch(`${socket}/v1/invoice`, options) + + return await createWithdrawal(null, { invoice: invoice.bolt11, maxFee }, { me, models, autoWithdraw: true }) +}