Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#824 Core Lightning Auto Withdraw #865

Closed
wants to merge 26 commits into from
Closed
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
ffc1f22
added type to enum and mirrored LND functionality. Not working yet
dillon-co Feb 18, 2024
ac857ac
updated macaroon to rune and added autowithdraw functionality
dillon-co Feb 20, 2024
3b3cd6c
merged in master
dillon-co Feb 20, 2024
e8dfac6
removed accidental copy/paste + some linter things
dillon-co Feb 20, 2024
e7d2fca
linter fixes
dillon-co Feb 20, 2024
31a98b5
linter fixes
dillon-co Feb 20, 2024
c080981
updated macaroon to rune
dillon-co Feb 20, 2024
4571c23
updated input name
dillon-co Feb 22, 2024
23fc178
Merge branch 'master' of github.com:stackernews/stacker.news into cor…
dillon-co Feb 22, 2024
1e05fe6
updated validation to check if rune is invoice only
dillon-co Feb 23, 2024
952dfa3
reverted docker-compose.yaml
dillon-co Feb 23, 2024
8b97469
linter fixes
dillon-co Feb 23, 2024
e5f6540
updated rune restriction check
dillon-co Feb 23, 2024
a0bc1a0
updated core lightning withdrawl to use bolt11
dillon-co Feb 25, 2024
d2fd823
updated some type defs
dillon-co Feb 25, 2024
0975003
updated test connect
dillon-co Feb 25, 2024
faa2873
updated resolvers to return correct wallet type
dillon-co Feb 26, 2024
8aeb41a
invoice rune check to client side only
dillon-co Feb 26, 2024
9c9af9d
added autowithdrawSchemaMembers to validate
dillon-co Feb 26, 2024
6ff4bd7
fixed typo
dillon-co Feb 26, 2024
8189259
linter
dillon-co Feb 26, 2024
71de2d8
linter
dillon-co Feb 26, 2024
9687ebf
linter fixes and switched invoice rune check to node that we know is …
dillon-co Feb 26, 2024
be06681
linter
dillon-co Feb 26, 2024
33dbbcb
linter
dillon-co Feb 26, 2024
016c929
Merge branch 'master' of github.com:stackernews/stacker.news into cor…
dillon-co Feb 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion api/resolvers/wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -432,6 +432,34 @@ 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({ string: rune })
}

await fetch(`${socket}/v1/decode`, options).then((response) => {
const requiredResponse = 'method (of command) equal to \'invoice\''
if (requiredResponse !== response.restrictions[0].alternatives[0].summary && response.restrictions.length > 1) {
throw new Error('rune is not for invoice only')
}
})
}
},
{ settings, data }, { me, models })
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I mentioned in the prior review, we do not want credentials that can spend from a stacker's node to be sent to the server (even if we don't store them).

This check should be done on the client like we do with LND and be part of validation.

Copy link
Contributor Author

@dillon-co dillon-co Feb 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed and moved it to the validator. I'm also using the demo node url for the rune check to decouple the socket and rune from the validation

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be sending runes to blockstream for all of our users? Or is this a placeholder while you work on something else?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Originally it was a placeholder to make sure it worked so I didn't have to spin up my own core lightning node again. I had to do that because Polar was giving me grief trying to get the rest api working. But I kind of feel like it would be good to decouple the 2. I just pulled that URL from their rest api example page here. But idk what do you think? Also I am working on the statistics page at the moment. I've been doing these 2 at the same time

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's this library but it hasn't been touched in a couple years and the health score looks pretty low

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But idk what do you think?

As a user I'd be pissed the application I'm using is sending credentials for my money to other people/companies. Wouldn't you?

There's this library but it hasn't been touched in a couple years and the health score looks pretty low

The healthscore is low for non-security reasons. It's a really simple package with a single function calling a few utility functions that easy to audit.


upsertWalletLNAddr: async (parent, { settings, ...data }, { me, models }) => {
return await upsertWallet(
{
Expand Down
1 change: 1 addition & 0 deletions api/typeDefs/wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
11 changes: 11 additions & 0 deletions fragments/wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -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!) {
Expand Down Expand Up @@ -135,6 +142,10 @@ export const WALLET_BY_TYPE = gql`
macaroon
cert
}
... on WalletCoreLIghtning {
socket
rune
}
}
}
}
Expand Down
7 changes: 7 additions & 0 deletions lib/validate.js
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,13 @@ export function LNDAutowithdrawSchema ({ me } = {}) {
})
}

export function CoreLightningAutowithdrawSchema ({ me } = {}) {
return object({
socket: string().socket().required('required'),
rune: hexOrBase64Validator.required('required')
})
}

export function autowithdrawSchemaMembers ({ me } = {}) {
return {
priority: boolean(),
Expand Down
114 changes: 114 additions & 0 deletions pages/settings/wallets/core-lightning.js
Original file line number Diff line number Diff line change
@@ -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 || {}
// removeWallet()
return (
<CenterLayout>
<h2 className='pb-2'>Core Lightning</h2>
<h6 className='text-muted text-center pb-3'>autowithdraw to your Core Lightning node</h6>
<h6 className='text-muted text-center pb-3'> You must have CLNRest working on your node. <a href='https://docs.corelightning.org/docs/rest\n\n'>More info here.</a></h6>
Comment on lines +29 to +31
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!


<Form
initial={{
socket: wallet?.wallet?.socket || '',
rune: wallet?.wallet?.rune || '',
...autowithdrawInitial({ me, priority: wallet?.priority })
}}
schema={CoreLightningAutowithdrawSchema({ me })}
onSubmit={async ({ socket, rune, ...settings }) => {
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?.())
}
}}
>
<Input
label='grpc host and port'
name='socket'
hint='tor or clearnet'
placeholder='55.5.555.55:10001'
clear
required
autoFocus
/>
<Input
label={
<div className='d-flex align-items-center'>Invoice Only Rune
<Info label='privacy tip'>
<Text>
{'***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```'}
</Text>
</Info>
</div>
}
name='rune'
clear
hint='base64 encoded'
placeholder='AgEDbG5kAlgDChCn7YgfWX7uTkQQgXZ2uahNEgEwGhYKB2FkZHJlc3MSBHJlYWQSBXdyaXRlGhcKCGludm9pY2VzEgRyZWFkEgV3cml0ZRoPCgdvbmNoYWluEgRyZWFkAAAGIJkMBrrDV0npU90JV0TGNJPrqUD8m2QYoTDjolaL6eBs'
required
/>
<AutowithdrawSettings />
<WalletButtonBar
enabled={!!wallet} onDelete={async () => {
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?.())
}
}}
/>
</Form>
</CenterLayout>
)
}

export function CoreLightningCard ({ wallet }) {
return (
<WalletCard
title='Core Lightning'
badges={['receive only', 'non-custodial']}
provider='core-lightning'
enabled={wallet !== undefined || undefined}
/>
)
}
4 changes: 3 additions & 1 deletion pages/settings/wallets/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 })

Expand All @@ -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 (
<Layout>
Expand All @@ -30,7 +32,7 @@ export default function Wallet ({ ssrData }) {
<LNDCard wallet={lnd} />
<LNbitsCard />
<NWCCard />
<WalletCard title='coming soon' badges={['probably']} />
<CoreLightningCard wallet={coreLightning} />
<WalletCard title='coming soon' badges={['we hope']} />
<WalletCard title='coming soon' badges={['tm']} />
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
12 changes: 12 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ model User {
enum WalletType {
LIGHTNING_ADDRESS
LND
CORE_LIGHTNING
}

model Wallet {
Expand All @@ -145,6 +146,7 @@ model Wallet {
wallet Json? @db.JsonB
walletLightningAddress WalletLightningAddress?
walletLND WalletLND?
walletCoreLightning WalletCoreLightning?

@@index([userId])
}
Expand All @@ -169,6 +171,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
Expand Down
42 changes: 42 additions & 0 deletions worker/autowithdraw.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.payment_hash, maxFee }, { me, models, autoWithdraw: true })
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

haven't looked at all the code but you can't pay an invoice hash

}
Loading