CLN autowithdrawal (#1042)
* Add CLN node to docker-compose.yml * Attach CLN wallet via CLNRest * Remove leading space * Implement autowithdrawal to CLN in worker * Fix UnhandledSchemeError during build See https://github.com/vercel/next.js/discussions/33982 * Refactor CLN invoice code into @/lib/cln * Fix missing env vars * Fix validation error if rune invalid * Update header * Add rune placeholder * Fix missing expiry for test invoice * Remove nonsensical comment * Remove unnecessary async * Show level SUCCESS as OK in logs * Add stacker_cln commands to sndev * fix sndev posix compliance, add cln_withdraw * give stacker_cln larger channels --------- Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com> Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
This commit is contained in:
parent
3aee93ee16
commit
9f4d5e13aa
@ -137,6 +137,12 @@ STACKER_LND_GRPC_PORT=10010
|
|||||||
STACKER_LND_ADDR=bcrt1qfqau4ug9e6rtrvxrgclg58e0r93wshucumm9vu
|
STACKER_LND_ADDR=bcrt1qfqau4ug9e6rtrvxrgclg58e0r93wshucumm9vu
|
||||||
STACKER_LND_PUBKEY=028093ae52e011d45b3e67f2e0f2cb6c3a1d7f88d2920d408f3ac6db3a56dc4b35
|
STACKER_LND_PUBKEY=028093ae52e011d45b3e67f2e0f2cb6c3a1d7f88d2920d408f3ac6db3a56dc4b35
|
||||||
|
|
||||||
|
# stacker cln container stuff
|
||||||
|
STACKER_CLN_REST_PORT=9092
|
||||||
|
# docker exec -u clightning stacker_cln lightning-cli newaddr bech32
|
||||||
|
STACKER_CLN_ADDR=bcrt1q02sqd74l4pxedy24fg0qtjz4y2jq7x4lxlgzrx
|
||||||
|
STACKER_CLN_PUBKEY=03ca7acec181dbf5e427c682c4261a46a0dd9ea5f35d97acb094e399f727835b90
|
||||||
|
|
||||||
LNCLI_NETWORK=regtest
|
LNCLI_NETWORK=regtest
|
||||||
|
|
||||||
# localstack container stuff
|
# localstack container stuff
|
||||||
|
@ -7,11 +7,12 @@ import lnpr from 'bolt11'
|
|||||||
import { SELECT } from './item'
|
import { SELECT } from './item'
|
||||||
import { lnAddrOptions } from '@/lib/lnurl'
|
import { lnAddrOptions } from '@/lib/lnurl'
|
||||||
import { msatsToSats, msatsToSatsDecimal, ensureB64 } from '@/lib/format'
|
import { msatsToSats, msatsToSatsDecimal, ensureB64 } from '@/lib/format'
|
||||||
import { LNDAutowithdrawSchema, amountSchema, lnAddrAutowithdrawSchema, lnAddrSchema, ssValidate, withdrawlSchema } from '@/lib/validate'
|
import { CLNAutowithdrawSchema, LNDAutowithdrawSchema, amountSchema, lnAddrAutowithdrawSchema, lnAddrSchema, ssValidate, withdrawlSchema } 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 { 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 { datePivot } from '@/lib/time'
|
||||||
import assertGofacYourself from './ofac'
|
import assertGofacYourself from './ofac'
|
||||||
import assertApiKeyNotPermitted from './apiKey'
|
import assertApiKeyNotPermitted from './apiKey'
|
||||||
|
import { createInvoice as createInvoiceCLN } from '@/lib/cln'
|
||||||
|
|
||||||
export async function getInvoice (parent, { id }, { me, models, lnd }) {
|
export async function getInvoice (parent, { id }, { me, models, lnd }) {
|
||||||
const inv = await models.invoice.findUnique({
|
const inv = await models.invoice.findUnique({
|
||||||
@ -323,7 +324,7 @@ export default {
|
|||||||
},
|
},
|
||||||
WalletDetails: {
|
WalletDetails: {
|
||||||
__resolveType (wallet) {
|
__resolveType (wallet) {
|
||||||
return wallet.address ? 'WalletLNAddr' : 'WalletLND'
|
return wallet.address ? 'WalletLNAddr' : wallet.macaroon ? 'WalletLND' : 'WalletCLN'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
@ -466,6 +467,36 @@ export default {
|
|||||||
},
|
},
|
||||||
{ settings, data }, { me, models })
|
{ settings, data }, { me, models })
|
||||||
},
|
},
|
||||||
|
upsertWalletCLN: async (parent, { settings, ...data }, { me, models }) => {
|
||||||
|
data.cert = ensureB64(data.cert)
|
||||||
|
|
||||||
|
const wallet = 'walletCLN'
|
||||||
|
return await upsertWallet(
|
||||||
|
{
|
||||||
|
schema: CLNAutowithdrawSchema,
|
||||||
|
walletName: wallet,
|
||||||
|
walletType: 'CLN',
|
||||||
|
testConnect: async ({ socket, rune, cert }) => {
|
||||||
|
try {
|
||||||
|
const inv = await createInvoiceCLN({
|
||||||
|
socket,
|
||||||
|
rune,
|
||||||
|
cert,
|
||||||
|
description: 'SN connection test',
|
||||||
|
msats: 'any',
|
||||||
|
expiry: 0
|
||||||
|
})
|
||||||
|
await addWalletLog({ wallet, level: 'SUCCESS', message: 'connected to CLN' }, { me, models })
|
||||||
|
return inv
|
||||||
|
} catch (err) {
|
||||||
|
const details = err.details || err.message || err.toString?.()
|
||||||
|
await addWalletLog({ wallet, level: 'ERROR', message: `could not connect to CLN: ${details}` }, { me, models })
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ settings, data }, { me, models })
|
||||||
|
},
|
||||||
upsertWalletLNAddr: async (parent, { settings, ...data }, { me, models }) => {
|
upsertWalletLNAddr: async (parent, { settings, ...data }, { me, models }) => {
|
||||||
const wallet = 'walletLightningAddress'
|
const wallet = 'walletLightningAddress'
|
||||||
return await upsertWallet(
|
return await upsertWallet(
|
||||||
@ -495,6 +526,8 @@ export default {
|
|||||||
let walletName = ''
|
let walletName = ''
|
||||||
if (wallet.type === 'LND') {
|
if (wallet.type === 'LND') {
|
||||||
walletName = 'walletLND'
|
walletName = 'walletLND'
|
||||||
|
} else if (wallet.type === 'CLN') {
|
||||||
|
walletName = 'walletCLN'
|
||||||
} else if (wallet.type === 'LIGHTNING_ADDRESS') {
|
} else if (wallet.type === 'LIGHTNING_ADDRESS') {
|
||||||
walletName = 'walletLightningAddress'
|
walletName = 'walletLightningAddress'
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ export default gql`
|
|||||||
cancelInvoice(hash: String!, hmac: String!): Invoice!
|
cancelInvoice(hash: String!, hmac: String!): Invoice!
|
||||||
dropBolt11(id: ID): Withdrawl
|
dropBolt11(id: ID): Withdrawl
|
||||||
upsertWalletLND(id: ID, socket: String!, macaroon: String!, cert: String, settings: AutowithdrawSettings!): Boolean
|
upsertWalletLND(id: ID, socket: String!, macaroon: String!, cert: String, settings: AutowithdrawSettings!): Boolean
|
||||||
|
upsertWalletCLN(id: ID, socket: String!, rune: String!, cert: String, settings: AutowithdrawSettings!): Boolean
|
||||||
upsertWalletLNAddr(id: ID, address: String!, settings: AutowithdrawSettings!): Boolean
|
upsertWalletLNAddr(id: ID, address: String!, settings: AutowithdrawSettings!): Boolean
|
||||||
removeWallet(id: ID!): Boolean
|
removeWallet(id: ID!): Boolean
|
||||||
}
|
}
|
||||||
@ -42,7 +43,13 @@ export default gql`
|
|||||||
cert: String
|
cert: String
|
||||||
}
|
}
|
||||||
|
|
||||||
union WalletDetails = WalletLNAddr | WalletLND
|
type WalletCLN {
|
||||||
|
socket: String!
|
||||||
|
rune: String!
|
||||||
|
cert: String
|
||||||
|
}
|
||||||
|
|
||||||
|
union WalletDetails = WalletLNAddr | WalletLND | WalletCLN
|
||||||
|
|
||||||
input AutowithdrawSettings {
|
input AutowithdrawSettings {
|
||||||
autoWithdrawThreshold: Int!
|
autoWithdrawThreshold: Int!
|
||||||
|
@ -8,7 +8,7 @@ export default function LogMessage ({ wallet, level, message, ts }) {
|
|||||||
<tr className={styles.line}>
|
<tr className={styles.line}>
|
||||||
<td className={styles.timestamp}>{timeSince(new Date(ts))}</td>
|
<td className={styles.timestamp}>{timeSince(new Date(ts))}</td>
|
||||||
<td className={styles.wallet}>[{wallet}]</td>
|
<td className={styles.wallet}>[{wallet}]</td>
|
||||||
<td className={`${styles.level} ${levelClassName}`}>{level}</td>
|
<td className={`${styles.level} ${levelClassName}`}>{level === 'success' ? 'ok' : level}</td>
|
||||||
<td>{message}</td>
|
<td>{message}</td>
|
||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
|
@ -160,6 +160,7 @@ const initIndexedDB = async (storeName) => {
|
|||||||
const renameWallet = (wallet) => {
|
const renameWallet = (wallet) => {
|
||||||
if (wallet === 'walletLightningAddress') return 'lnAddr'
|
if (wallet === 'walletLightningAddress') return 'lnAddr'
|
||||||
if (wallet === 'walletLND') return 'lnd'
|
if (wallet === 'walletLND') return 'lnd'
|
||||||
|
if (wallet === 'walletCLN') return 'cln'
|
||||||
return wallet
|
return wallet
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -253,14 +253,17 @@ services:
|
|||||||
bash -c '
|
bash -c '
|
||||||
blockcount=$$(bitcoin-cli -chain=regtest -rpcport=${RPC_PORT} -rpcuser=${RPC_USER} -rpcpassword=${RPC_PASS} getblockcount 2>/dev/null)
|
blockcount=$$(bitcoin-cli -chain=regtest -rpcport=${RPC_PORT} -rpcuser=${RPC_USER} -rpcpassword=${RPC_PASS} getblockcount 2>/dev/null)
|
||||||
if (( blockcount <= 0 )); then
|
if (( blockcount <= 0 )); then
|
||||||
echo "Mining 10 blocks to sn_lnd and stacker_lnd..."
|
echo "Mining 10 blocks to sn_lnd, stacker_lnd, stacker_cln..."
|
||||||
bitcoin-cli -chain=regtest -rpcport=${RPC_PORT} -rpcuser=${RPC_USER} -rpcpassword=${RPC_PASS} generatetoaddress 100 ${LND_ADDR}
|
bitcoin-cli -chain=regtest -rpcport=${RPC_PORT} -rpcuser=${RPC_USER} -rpcpassword=${RPC_PASS} generatetoaddress 100 ${LND_ADDR}
|
||||||
bitcoin-cli -chain=regtest -rpcport=${RPC_PORT} -rpcuser=${RPC_USER} -rpcpassword=${RPC_PASS} generatetoaddress 100 ${STACKER_LND_ADDR}
|
bitcoin-cli -chain=regtest -rpcport=${RPC_PORT} -rpcuser=${RPC_USER} -rpcpassword=${RPC_PASS} generatetoaddress 100 ${STACKER_LND_ADDR}
|
||||||
|
bitcoin-cli -chain=regtest -rpcport=${RPC_PORT} -rpcuser=${RPC_USER} -rpcpassword=${RPC_PASS} generatetoaddress 100 ${STACKER_CLN_ADDR}
|
||||||
else
|
else
|
||||||
echo "Mining a block to sn_lnd... ${LND_ADDR}"
|
echo "Mining a block to sn_lnd... ${LND_ADDR}"
|
||||||
bitcoin-cli -chain=regtest -rpcport=${RPC_PORT} -rpcuser=${RPC_USER} -rpcpassword=${RPC_PASS} generatetoaddress 1 ${LND_ADDR}
|
bitcoin-cli -chain=regtest -rpcport=${RPC_PORT} -rpcuser=${RPC_USER} -rpcpassword=${RPC_PASS} generatetoaddress 1 ${LND_ADDR}
|
||||||
echo "Mining a block to stacker_lnd... ${STACKER_LND_ADDR}"
|
echo "Mining a block to stacker_lnd... ${STACKER_LND_ADDR}"
|
||||||
bitcoin-cli -chain=regtest -rpcport=${RPC_PORT} -rpcuser=${RPC_USER} -rpcpassword=${RPC_PASS} generatetoaddress 1 ${STACKER_LND_ADDR}
|
bitcoin-cli -chain=regtest -rpcport=${RPC_PORT} -rpcuser=${RPC_USER} -rpcpassword=${RPC_PASS} generatetoaddress 1 ${STACKER_LND_ADDR}
|
||||||
|
echo "Mining a block to stacker_cln... ${STACKER_CLN_ADDR}"
|
||||||
|
bitcoin-cli -chain=regtest -rpcport=${RPC_PORT} -rpcuser=${RPC_USER} -rpcpassword=${RPC_PASS} generatetoaddress 1 ${STACKER_CLN_ADDR}
|
||||||
fi
|
fi
|
||||||
'
|
'
|
||||||
sn_lnd:
|
sn_lnd:
|
||||||
@ -381,8 +384,8 @@ services:
|
|||||||
- stacker_lnd:/home/lnd/.lnd
|
- stacker_lnd:/home/lnd/.lnd
|
||||||
labels:
|
labels:
|
||||||
ofelia.enabled: "true"
|
ofelia.enabled: "true"
|
||||||
ofelia.job-exec.stacker_channel_cron.schedule: "@every 1m"
|
ofelia.job-exec.stacker_lnd_channel_cron.schedule: "@every 1m"
|
||||||
ofelia.job-exec.stacker_channel_cron.command: >
|
ofelia.job-exec.stacker_lnd_channel_cron.command: >
|
||||||
su lnd -c bash -c "
|
su lnd -c bash -c "
|
||||||
if [ $$(lncli getinfo | jq '.num_active_channels + .num_pending_channels') -ge 3 ]; then
|
if [ $$(lncli getinfo | jq '.num_active_channels + .num_pending_channels') -ge 3 ]; then
|
||||||
exit 0
|
exit 0
|
||||||
@ -391,6 +394,55 @@ services:
|
|||||||
--min_confs 0 --local_amt=1000000000 --push_amt=500000000
|
--min_confs 0 --local_amt=1000000000 --push_amt=500000000
|
||||||
fi
|
fi
|
||||||
"
|
"
|
||||||
|
stacker_cln:
|
||||||
|
build:
|
||||||
|
context: ./docker/cln
|
||||||
|
container_name: stacker_cln
|
||||||
|
restart: unless-stopped
|
||||||
|
profiles:
|
||||||
|
- payments
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "su clightning -c 'lightning-cli --network=regtest getinfo'"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 10
|
||||||
|
start_period: 1m
|
||||||
|
depends_on:
|
||||||
|
bitcoin:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: true
|
||||||
|
env_file:
|
||||||
|
- .env.development
|
||||||
|
command:
|
||||||
|
- 'lightningd'
|
||||||
|
- '--network=regtest'
|
||||||
|
- '--alias=stacker_cln'
|
||||||
|
- '--bitcoin-rpcconnect=bitcoin'
|
||||||
|
- '--bitcoin-rpcuser=${RPC_USER}'
|
||||||
|
- '--bitcoin-rpcpassword=${RPC_PASS}'
|
||||||
|
- '--large-channels'
|
||||||
|
- '--rest-port=3010'
|
||||||
|
- '--rest-host=0.0.0.0'
|
||||||
|
- '--log-file=/home/clightning/.lightning/debug.log'
|
||||||
|
expose:
|
||||||
|
- "9735"
|
||||||
|
ports:
|
||||||
|
- "${STACKER_CLN_REST_PORT}:3010"
|
||||||
|
volumes:
|
||||||
|
- stacker_cln:/home/clightning/.lightning
|
||||||
|
labels:
|
||||||
|
ofelia.enabled: "true"
|
||||||
|
ofelia.job-exec.stacker_cln_channel_cron.schedule: "@every 1m"
|
||||||
|
ofelia.job-exec.stacker_cln_channel_cron.command: >
|
||||||
|
su clightning -c bash -c "
|
||||||
|
if [ $$(lightning-cli --regtest getinfo | jq '.num_active_channels + .num_pending_channels') -ge 3 ]; then
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
lightning-cli --regtest connect $LND_PUBKEY@sn_lnd:9735
|
||||||
|
lightning-cli --regtest fundchannel id=$LND_PUBKEY feerate=1000perkb \\
|
||||||
|
amount=1000000000 push_msat=500000000000 minconf=0
|
||||||
|
fi
|
||||||
|
"
|
||||||
channdler:
|
channdler:
|
||||||
image: mcuadros/ofelia:latest
|
image: mcuadros/ofelia:latest
|
||||||
container_name: channdler
|
container_name: channdler
|
||||||
@ -427,4 +479,5 @@ volumes:
|
|||||||
bitcoin:
|
bitcoin:
|
||||||
sn_lnd:
|
sn_lnd:
|
||||||
stacker_lnd:
|
stacker_lnd:
|
||||||
|
stacker_cln:
|
||||||
s3:
|
s3:
|
||||||
|
16
docker/cln/Dockerfile
Normal file
16
docker/cln/Dockerfile
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
FROM polarlightning/clightning:23.08
|
||||||
|
|
||||||
|
RUN apt-get update -y \
|
||||||
|
&& apt-get install -y jq wget \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
|
RUN wget https://raw.githubusercontent.com/ElementsProject/lightning/v23.08/plugins/clnrest/requirements.txt \
|
||||||
|
&& pip install -r requirements.txt
|
||||||
|
|
||||||
|
# make sure that wallet and identity is persisted across rebuilds.
|
||||||
|
# server certificates contain stacker_lnd as a custom domain.
|
||||||
|
# see https://docs.corelightning.org/docs/grpc#generating-custom-certificates-optional
|
||||||
|
# since CLNRest in CLNv23.08 seems to use client certificates, they also contain stacker_lnd as a custom domain.
|
||||||
|
# see https://github.com/ElementsProject/lightning/tree/v23.08/plugins/clnrest#configuration
|
||||||
|
COPY ["./hsm_secret", "./ca-key.pem", "./ca.pem", "./server-key.pem", "./server.pem", "./client-key.pem", "./client.pem", "/home/clightning/.lightning/regtest/"]
|
5
docker/cln/ca-key.pem
Normal file
5
docker/cln/ca-key.pem
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgXhrU/UORcVCRnDbo
|
||||||
|
KF35C+BM3FIlCvBX0Y5J2J9piE+hRANCAAQY5mh7rpXIMEhjp5SwB2EJT0kMBlwz
|
||||||
|
SVsn81a1fYUxWgUXUJNWLnLo00PQKq5xCKxXc9fzs/tl1w+oANsTp2/u
|
||||||
|
-----END PRIVATE KEY-----
|
10
docker/cln/ca.pem
Normal file
10
docker/cln/ca.pem
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIBcjCCARigAwIBAgIJANrSvPZ/Y3KEMAoGCCqGSM49BAMCMBYxFDASBgNVBAMM
|
||||||
|
C2NsbiBSb290IENBMCAXDTc1MDEwMTAwMDAwMFoYDzQwOTYwMTAxMDAwMDAwWjAW
|
||||||
|
MRQwEgYDVQQDDAtjbG4gUm9vdCBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IA
|
||||||
|
BBjmaHuulcgwSGOnlLAHYQlPSQwGXDNJWyfzVrV9hTFaBRdQk1YucujTQ9AqrnEI
|
||||||
|
rFdz1/Oz+2XXD6gA2xOnb+6jTTBLMBkGA1UdEQQSMBCCA2NsboIJbG9jYWxob3N0
|
||||||
|
MB0GA1UdDgQWBBSEcmN/9rzS2hR6G7EIgsX+51N0CjAPBgNVHRMBAf8EBTADAQH/
|
||||||
|
MAoGCCqGSM49BAMCA0gAMEUCIHCePvNSvyiBYawqKgQquw9J2WVyJxn2MIYIqz9S
|
||||||
|
QL18AiEAh8BVDz8pX7Nsll8sb0bO0RZ49cvqQocCgVabqJuSuik=
|
||||||
|
-----END CERTIFICATE-----
|
1
docker/cln/ca.srl
Normal file
1
docker/cln/ca.srl
Normal file
@ -0,0 +1 @@
|
|||||||
|
70A2D30FE991B24B5A6BF85421BE5EF083665E7E
|
28
docker/cln/client-key.pem
Normal file
28
docker/cln/client-key.pem
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDeBF5jfabczcaz
|
||||||
|
VnYFEB9botC3TevcCpyucbLmCPFU3dcXjUdDJFfnQDlbIAm9UqbUwIF0GIRRJ1Il
|
||||||
|
H5mZ3MxXVGMNHlLuLdfVMfJFk72WczPecUPh5jJea+ujdW4I5Q+kgx/CjVatPHz5
|
||||||
|
WYgKC+6d10tawYLR47RRZhOgzAdrCD//L3AE+WPcdWicRSJoYDxdmyQiF9X9Vz6X
|
||||||
|
NQuOExYk3etHzwEf8alnMZbzS7j10//QxaxR/Ujp4tgt+ACwxNLd7TdoYZ8FP4AY
|
||||||
|
1ijabYxLGUA+Mk0rC6Al36EE78dUg1fTDCh+ee1YAnoYgnT7jrUQ+6pbiwmfjrO0
|
||||||
|
lHVDJ+CpAgMBAAECggEABFgj35KezaNjKbEWljkb0U6bO7L/zRxK8AVa+od0KB9b
|
||||||
|
XvGNfSVY9zYrOuyGELWX7nOz33CF0Wh8LJ4fZkKgZvxk5x6lLAC5Tp7vhYR1LNxs
|
||||||
|
VpJzJN+gbMxz6/6CFYaVeQ2RWLHzpmhCUf/dxVJVc0Dk8xTX8jLngN4/972SwLKJ
|
||||||
|
krM2bdAh9crNwi4IKiwnNZKIZA9mZRwSBkpvScV0WSE7JdBT/j4JgTRJY8ya3svo
|
||||||
|
rc7Lb2b/IeUfJDRbGKW5+5ST3FOUpfXtkHKv+Pg0QYAvdnYLbaI/xaGJkerqE9I8
|
||||||
|
gXXm5kc39N/PE3LCCLrst7mk/xbJAR9BVGylU81gRwKBgQD1rW1qBce0M1VuuI+i
|
||||||
|
yoDAYDP7qL4KW/1YnQP377mRThCPu8vKt6EqOUa3JuMWk7o2pYSgUnawWMuVkmDl
|
||||||
|
1szCHojw1YNblRoO5Qodw8duYE81wXAgAZq3NRE1EBffOAMTo61K63MLq2AxepjW
|
||||||
|
visVx4F/5bKZ/lD2kkiQpQwpUwKBgQDnWHGgHX3sL1gHLdiKvnLYFX+b1DvtQ/zm
|
||||||
|
jz5vmDZtWtyyGUD4Hul77RGxrbB3Yhbfulvkgp3yCFb3o1T32XYBHYxhGVQ54YfP
|
||||||
|
JtwYjd5sGNCofEIJpeVBjgdgDeZWF6AKqgQEw+D1/uIE5E2lZT7HeI+0HL6LIWIo
|
||||||
|
gj6W+RCCkwKBgHLqd1Zzc7FPnbOXsuAjtsvFdCtQB+ySkNOlRljwEi3shQSmhDHD
|
||||||
|
aSh1+CTtlKVX3m93Rq0zRX9BWaESAi8gJVDbtZRpWvM4sCKtcejwTdXMSODNJaRi
|
||||||
|
+7qcoPrgFzp7Wb0S/5kevwaDWBBs1xcDhuW+F0365GrxsW9Uh4rZGPIvAoGAewYi
|
||||||
|
bnYgd4/5rN+pbqa2ZciQ8qobMCJeg7EbD7cPAno2MJOTZB70JM2+AhGObP4BkfoF
|
||||||
|
UfBP09yxesEltyOySAeRljUlAB653OQaWQhghnVvyJlDeOP6lTDVJTRfD9tCZUli
|
||||||
|
F7Kel9JyGQ3baJ/9kY/AQ5Shk1UuYMJaTGioafcCgYEA4DgJWHRbih/KGq+KxCmI
|
||||||
|
ebFabuGr/XRKaQ5NlnUtPnSOPnfFxSKU+7egKmM4fA4i+wiKwWrsfxIXsgkDGKDc
|
||||||
|
6T0s9Zf6uGwS+be3g494WoaPKuiwAuutkkjhWQtXAMLsoLl8PAyZsyet4bsyTmPv
|
||||||
|
9BnJIn6TN2lFZl6yNePe5S0=
|
||||||
|
-----END PRIVATE KEY-----
|
17
docker/cln/client.csr
Normal file
17
docker/cln/client.csr
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
-----BEGIN CERTIFICATE REQUEST-----
|
||||||
|
MIICoDCCAYgCAQAwWzELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUx
|
||||||
|
ITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEUMBIGA1UEAwwLc3Rh
|
||||||
|
Y2tlcl9jbG4wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDeBF5jfabc
|
||||||
|
zcazVnYFEB9botC3TevcCpyucbLmCPFU3dcXjUdDJFfnQDlbIAm9UqbUwIF0GIRR
|
||||||
|
J1IlH5mZ3MxXVGMNHlLuLdfVMfJFk72WczPecUPh5jJea+ujdW4I5Q+kgx/CjVat
|
||||||
|
PHz5WYgKC+6d10tawYLR47RRZhOgzAdrCD//L3AE+WPcdWicRSJoYDxdmyQiF9X9
|
||||||
|
Vz6XNQuOExYk3etHzwEf8alnMZbzS7j10//QxaxR/Ujp4tgt+ACwxNLd7TdoYZ8F
|
||||||
|
P4AY1ijabYxLGUA+Mk0rC6Al36EE78dUg1fTDCh+ee1YAnoYgnT7jrUQ+6pbiwmf
|
||||||
|
jrO0lHVDJ+CpAgMBAAGgADANBgkqhkiG9w0BAQsFAAOCAQEAs3mdbEeLipHKeGZO
|
||||||
|
BbZ9vz7Ye0hRMX3q4liSBwGbHm/ByUqtzcXKf+R2T0+5dfc2PJNd7uRxWlRPSDdV
|
||||||
|
ebHm5ctZaDIGUS2XQrj67KoRRtZTLM4IGM2g5ppbgnm9NM6NWYpO9t2hKupIqNa+
|
||||||
|
ATYNOzxLHZQJRTvxhC0kpy196huw/vs4d7TVPF7SxJVsXPmjt1OGso52HL0HjDsD
|
||||||
|
BcNW2Qtd9WcrEwM8HmydBJuSru5gX7HHgKMHUmPtyvFXdTjiiqFnU1apTmF8ptY5
|
||||||
|
tvNtz8pGmb3p0lvkyAUaEurtzNChwIdU5rvkkZGC8sphpMECPEmMgpavkkZK3+EJ
|
||||||
|
aUwdqQ==
|
||||||
|
-----END CERTIFICATE REQUEST-----
|
16
docker/cln/client.pem
Normal file
16
docker/cln/client.pem
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIICfzCCAiagAwIBAgIUcKLTD+mRsktaa/hUIb5e8INmXn4wCgYIKoZIzj0EAwIw
|
||||||
|
FjEUMBIGA1UEAwwLY2xuIFJvb3QgQ0EwHhcNMjQwNDExMTU1ODQ2WhcNMzQwNDA5
|
||||||
|
MTU1ODQ2WjBbMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8G
|
||||||
|
A1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRQwEgYDVQQDDAtzdGFja2Vy
|
||||||
|
X2NsbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN4EXmN9ptzNxrNW
|
||||||
|
dgUQH1ui0LdN69wKnK5xsuYI8VTd1xeNR0MkV+dAOVsgCb1SptTAgXQYhFEnUiUf
|
||||||
|
mZnczFdUYw0eUu4t19Ux8kWTvZZzM95xQ+HmMl5r66N1bgjlD6SDH8KNVq08fPlZ
|
||||||
|
iAoL7p3XS1rBgtHjtFFmE6DMB2sIP/8vcAT5Y9x1aJxFImhgPF2bJCIX1f1XPpc1
|
||||||
|
C44TFiTd60fPAR/xqWcxlvNLuPXT/9DFrFH9SOni2C34ALDE0t3tN2hhnwU/gBjW
|
||||||
|
KNptjEsZQD4yTSsLoCXfoQTvx1SDV9MMKH557VgCehiCdPuOtRD7qluLCZ+Os7SU
|
||||||
|
dUMn4KkCAwEAAaNCMEAwHQYDVR0OBBYEFE15SBJ5Z/50fktS1qs9agwhQ40PMB8G
|
||||||
|
A1UdIwQYMBaAFIRyY3/2vNLaFHobsQiCxf7nU3QKMAoGCCqGSM49BAMCA0cAMEQC
|
||||||
|
IGSPak0NLkIDa1Dyw/NNWeBf+PxLysd9tCIPmMF7YmN3AiAZcdXrWldVW/9RLRyT
|
||||||
|
0RU2Uqr/47R+MADhX961ZlfzfQ==
|
||||||
|
-----END CERTIFICATE-----
|
1
docker/cln/hsm_secret
Normal file
1
docker/cln/hsm_secret
Normal file
@ -0,0 +1 @@
|
|||||||
|
$ְDMBֶװהמ$«bד†y5ת<35>$E0”<30>°P·§)ּ
|
28
docker/cln/server-key.pem
Normal file
28
docker/cln/server-key.pem
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDGJ+Q2predhnnY
|
||||||
|
roMhBgQNWt3OvY49M6WC2u+P2HWj3+pjjt6oNaM8emYqRXTh4WlAyi8G/XNkADLv
|
||||||
|
Zf4HMzBp5kYtCemtzce6Zt1LE45Xd7xdFDca7PLOTFaaNY99AENBbaJf0s5ztJSz
|
||||||
|
IfBisPfhvDwOhbwJB5KZ0ezaa/RItYcOdkZeggbgoYeDA63L+fHTX5Y/Rt9Aooo9
|
||||||
|
VDix6y1pGMa53jVxdGvj1bb5hkqPtr5zi6hwIum1YDWWwp7wMkft0pnvrnwMQ9mk
|
||||||
|
A+Pxd8pMie5I5Ks+RPjYk2W8faEccwgN0DXhOBTnxwmdKxVnq+5nU6neZ2d1pRel
|
||||||
|
rklppay9AgMBAAECggEATWyyz+POZL9xhoeRdurJ1IoHlssb86/lYL640gSq2pAY
|
||||||
|
HjRprWHf2TaeCrA+3i9cF9OoElwfpRgqzr2URy3qIca27swrwRxhiOS+XKJUgLqp
|
||||||
|
H9lROrUQnijXwcNhwF7E6KC0zCorPqx1WZTOP1GUWWBaOvZoJUMPNgj/Oczqkymh
|
||||||
|
W0w1+8cHAlQLs5756xL7lvwUk6sLUXWEEYHv2cW3Q8n7c1y41GQUuVnwrZJGqXDM
|
||||||
|
er0TGDGeBjahl7RyC+Nd7aA6W1HW8pELI0lAkkiZkRoSw4urG5RE2al9SqtkbiUi
|
||||||
|
s8ntp1LUk43/mnWgcJ9dLknNiYogNYOb8EdI4OF+xQKBgQD9BbeReC5cP4L9ZpF9
|
||||||
|
Bzaf1K2dxhJivkzScNjX6ciMV/U8jppHAz7BSFbO7gPczsdk5zQaqPy/A0uhGKhy
|
||||||
|
74nB+0kguHegASjzqGWpRh6W/CRWFuiktoulS7eSMUqEiJ8OeKpE2Am3I+ltlOyH
|
||||||
|
gRWdHiEOmZMlWGcrgY3yELlRewKBgQDIfOClARHERNBhbPKAAaW2NTQekMhJPPSZ
|
||||||
|
aCY6TiIJ+SSWSEF1ythe7HyoIJgcR10UobXrcIuv1LbgqxwLOmsaE7TU8fOGeQPe
|
||||||
|
HJrr589o3MWmS5K6TvxVcL40dKYjoVGVBVdahkWmoZ6JALlfKTiOHN62EW/CHOJA
|
||||||
|
wlSyADvZJwKBgQDc09aIwalEnbHHU3N6+Ya1LDtyzeJSB+CochDvMHz17/Z7KcKA
|
||||||
|
Y9arfmU1KQp59oaUDC2vbvlYBJpHOWwbE/DZOmVyh0zwetKxBbHkcOxVvi5AbLIS
|
||||||
|
v7dVRqYqk5aD4XFggfOpLhwcmN0r5KQjB4hDnn4fbe281FEG6YVnVS1IbQKBgQCP
|
||||||
|
WVKaSEB20Ckab/aX9hWRSUtBy42ZaB8QDPrAV5tY/C3f0jwTx/ybKoYbBGseVRxF
|
||||||
|
ozZa6DbIetRjoZTEpnlrxMlYNMNF1AMi7dsLb8zKEoiz1XdNBSrAwIMPKJSeBzs4
|
||||||
|
zP/fdwAYG5kqJj1kwCly20uWbLM23MYdPZWnTCl+owKBgFC6Vjw1fPqp4yi8dgZ4
|
||||||
|
FYpxniLQFgj3wjQiIZbGlN0d0oghF6TJzhbLtdsTMaZVF8hQuhsMIPRuEYxku9no
|
||||||
|
McqUqX30q+O98h1yCmfJmTtFJrNWPSeHejNAQP0BWmEMZBYD7gaexwDXWvcyQh9f
|
||||||
|
Y9gQQi/itHYOEBy6L+OC3ETD
|
||||||
|
-----END PRIVATE KEY-----
|
17
docker/cln/server.csr
Normal file
17
docker/cln/server.csr
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
-----BEGIN CERTIFICATE REQUEST-----
|
||||||
|
MIICoDCCAYgCAQAwWzELMAkGA1UEBhMCVVMxEzARBgNVBAgMClNvbWUtU3RhdGUx
|
||||||
|
ITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEUMBIGA1UEAwwLc3Rh
|
||||||
|
Y2tlcl9jbG4wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDGJ+Q2pred
|
||||||
|
hnnYroMhBgQNWt3OvY49M6WC2u+P2HWj3+pjjt6oNaM8emYqRXTh4WlAyi8G/XNk
|
||||||
|
ADLvZf4HMzBp5kYtCemtzce6Zt1LE45Xd7xdFDca7PLOTFaaNY99AENBbaJf0s5z
|
||||||
|
tJSzIfBisPfhvDwOhbwJB5KZ0ezaa/RItYcOdkZeggbgoYeDA63L+fHTX5Y/Rt9A
|
||||||
|
ooo9VDix6y1pGMa53jVxdGvj1bb5hkqPtr5zi6hwIum1YDWWwp7wMkft0pnvrnwM
|
||||||
|
Q9mkA+Pxd8pMie5I5Ks+RPjYk2W8faEccwgN0DXhOBTnxwmdKxVnq+5nU6neZ2d1
|
||||||
|
pRelrklppay9AgMBAAGgADANBgkqhkiG9w0BAQsFAAOCAQEAOoXArh2qS9wxSEKx
|
||||||
|
JqgTCSxrocuY5xdpeNCoReIQazT+bG332THdiy8a3HtuNGnCSPqrKffpv/G95oSu
|
||||||
|
eGGxx+n/3sObgzliOk9kDA8XOebuUDb+jM3joLZikGzeFOaMCcwJrhYIyTSmN9Zh
|
||||||
|
k5rQT6ufRA7LCL5FS4sYG1sly/fYlBqp2kLs2piEgvWdjh2cnfJ2UnyIk/a6sUJI
|
||||||
|
x7zfmzFAeU3M+fR1Q3//4ISe4MAPDzn/rV8bpcl5iRqagyfbXHIeLfX68mCG3ABf
|
||||||
|
SKnzUBnfB3g1GjUXIMFzGk23VuKRd6dBHoIMNknkDCM4r4thUtWH7oywWDiYBMX7
|
||||||
|
oKrKwg==
|
||||||
|
-----END CERTIFICATE REQUEST-----
|
16
docker/cln/server.pem
Normal file
16
docker/cln/server.pem
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIICfzCCAiagAwIBAgIUcKLTD+mRsktaa/hUIb5e8INmXn0wCgYIKoZIzj0EAwIw
|
||||||
|
FjEUMBIGA1UEAwwLY2xuIFJvb3QgQ0EwHhcNMjQwNDEwMDk1OTA1WhcNMjUwNDEw
|
||||||
|
MDk1OTA1WjBbMQswCQYDVQQGEwJVUzETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8G
|
||||||
|
A1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRQwEgYDVQQDDAtzdGFja2Vy
|
||||||
|
X2NsbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMYn5Damt52Gediu
|
||||||
|
gyEGBA1a3c69jj0zpYLa74/YdaPf6mOO3qg1ozx6ZipFdOHhaUDKLwb9c2QAMu9l
|
||||||
|
/gczMGnmRi0J6a3Nx7pm3UsTjld3vF0UNxrs8s5MVpo1j30AQ0Ftol/SznO0lLMh
|
||||||
|
8GKw9+G8PA6FvAkHkpnR7Npr9Ei1hw52Rl6CBuChh4MDrcv58dNflj9G30Ciij1U
|
||||||
|
OLHrLWkYxrneNXF0a+PVtvmGSo+2vnOLqHAi6bVgNZbCnvAyR+3Sme+ufAxD2aQD
|
||||||
|
4/F3ykyJ7kjkqz5E+NiTZbx9oRxzCA3QNeE4FOfHCZ0rFWer7mdTqd5nZ3WlF6Wu
|
||||||
|
SWmlrL0CAwEAAaNCMEAwHQYDVR0OBBYEFF4+yBxv8b+F7Jwr1dQbstMRYuO9MB8G
|
||||||
|
A1UdIwQYMBaAFIRyY3/2vNLaFHobsQiCxf7nU3QKMAoGCCqGSM49BAMCA0cAMEQC
|
||||||
|
IHDwzjxMRMztT4mN7tRMAHQsMCMbdIeKzDr7g0so19X/AiBZV4kvDbQSbVoJ2UIA
|
||||||
|
gZiiN5TD+ucMjU4wLzdiBRAFqA==
|
||||||
|
-----END CERTIFICATE-----
|
@ -89,6 +89,13 @@ mutation upsertWalletLND($id: ID, $socket: String!, $macaroon: String!, $cert: S
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
export const UPSERT_WALLET_CLN =
|
||||||
|
gql`
|
||||||
|
mutation upsertWalletCLN($id: ID, $socket: String!, $rune: String!, $cert: String, $settings: AutowithdrawSettings!) {
|
||||||
|
upsertWalletCLN(id: $id, socket: $socket, rune: $rune, cert: $cert, settings: $settings)
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
export const REMOVE_WALLET =
|
export const REMOVE_WALLET =
|
||||||
gql`
|
gql`
|
||||||
mutation removeWallet($id: ID!) {
|
mutation removeWallet($id: ID!) {
|
||||||
@ -113,6 +120,11 @@ export const WALLET = gql`
|
|||||||
macaroon
|
macaroon
|
||||||
cert
|
cert
|
||||||
}
|
}
|
||||||
|
... on WalletCLN {
|
||||||
|
socket
|
||||||
|
rune
|
||||||
|
cert
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -135,6 +147,11 @@ export const WALLET_BY_TYPE = gql`
|
|||||||
macaroon
|
macaroon
|
||||||
cert
|
cert
|
||||||
}
|
}
|
||||||
|
... on WalletCLN {
|
||||||
|
socket
|
||||||
|
rune
|
||||||
|
cert
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
161
lib/cln.js
Normal file
161
lib/cln.js
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
import fetch from 'node-fetch'
|
||||||
|
import https from 'https'
|
||||||
|
|
||||||
|
export const createInvoice = async ({ socket, rune, cert, label, description, msats, expiry }) => {
|
||||||
|
const agent = cert ? new https.Agent({ ca: Buffer.from(cert, 'base64') }) : undefined
|
||||||
|
const url = 'https://' + socket + '/v1/invoice'
|
||||||
|
const randomId = Math.floor(Math.random() * 1000)
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Rune: rune,
|
||||||
|
// can be any node id, only required for CLN v23.08 and below
|
||||||
|
// see https://docs.corelightning.org/docs/rest#server
|
||||||
|
nodeId: '02cb2e2d5a6c5b17fa67b1a883e2973c82e328fb9bd08b2b156a9e23820c87a490'
|
||||||
|
},
|
||||||
|
agent,
|
||||||
|
body: JSON.stringify({
|
||||||
|
// why does CLN require a unique label?
|
||||||
|
label: description ? `${description} ${randomId}` : randomId,
|
||||||
|
description,
|
||||||
|
amount_msat: msats,
|
||||||
|
expiry
|
||||||
|
})
|
||||||
|
})
|
||||||
|
const inv = await res.json()
|
||||||
|
if (inv.error) {
|
||||||
|
throw new Error(inv.error.message)
|
||||||
|
}
|
||||||
|
return inv
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/clams-tech/rune-decoder/blob/57c2e76d1ef9ab7336f565b99de300da1c7b67ce/src/index.ts
|
||||||
|
export const decodeRune = (rune) => {
|
||||||
|
const runeBinary = Base64Binary.decode(rune)
|
||||||
|
const hashBinary = runeBinary.slice(0, 32)
|
||||||
|
const hash = binaryHashToHex(hashBinary)
|
||||||
|
const restBinary = runeBinary.slice(32)
|
||||||
|
|
||||||
|
const [uniqueId, ...restrictionStrings] = new TextDecoder().decode(restBinary).split('&')
|
||||||
|
|
||||||
|
const id = uniqueId.split('=')[1]
|
||||||
|
|
||||||
|
// invalid rune checks
|
||||||
|
if (!id) return null
|
||||||
|
if (restrictionStrings.some(invalidAscii)) return null
|
||||||
|
|
||||||
|
const restrictions = restrictionStrings.map((restriction) => {
|
||||||
|
const alternatives = restriction.split('|')
|
||||||
|
|
||||||
|
const summary = alternatives.reduce((str, alternative) => {
|
||||||
|
const [operator] = alternative.match(runeOperatorRegex) || []
|
||||||
|
if (!operator) return str
|
||||||
|
|
||||||
|
const [name, value] = alternative.split(operator)
|
||||||
|
|
||||||
|
return `${str ? `${str} OR ` : ''}${name} ${operatorToDescription(operator)} ${value}`
|
||||||
|
}, '')
|
||||||
|
|
||||||
|
return {
|
||||||
|
alternatives,
|
||||||
|
summary
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
hash,
|
||||||
|
restrictions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Base64Binary = {
|
||||||
|
_keyStr: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=',
|
||||||
|
|
||||||
|
removePaddingChars: function (input) {
|
||||||
|
const lkey = this._keyStr.indexOf(input.charAt(input.length - 1))
|
||||||
|
|
||||||
|
if (lkey === 64) {
|
||||||
|
return input.substring(0, input.length - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return input
|
||||||
|
},
|
||||||
|
|
||||||
|
decode: function (input) {
|
||||||
|
// get last chars to see if are valid
|
||||||
|
input = this.removePaddingChars(input)
|
||||||
|
input = this.removePaddingChars(input)
|
||||||
|
|
||||||
|
const bytes = parseInt(((input.length / 4) * 3).toString(), 10)
|
||||||
|
|
||||||
|
let chr1, chr2, chr3
|
||||||
|
let enc1, enc2, enc3, enc4
|
||||||
|
let i = 0
|
||||||
|
let j = 0
|
||||||
|
|
||||||
|
const uarray = new Uint8Array(bytes)
|
||||||
|
|
||||||
|
for (i = 0; i < bytes; i += 3) {
|
||||||
|
// get the 3 octects in 4 ascii chars
|
||||||
|
enc1 = this._keyStr.indexOf(input.charAt(j++))
|
||||||
|
enc2 = this._keyStr.indexOf(input.charAt(j++))
|
||||||
|
enc3 = this._keyStr.indexOf(input.charAt(j++))
|
||||||
|
enc4 = this._keyStr.indexOf(input.charAt(j++))
|
||||||
|
|
||||||
|
chr1 = (enc1 << 2) | (enc2 >> 4)
|
||||||
|
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2)
|
||||||
|
chr3 = ((enc3 & 3) << 6) | enc4
|
||||||
|
|
||||||
|
uarray[i] = chr1
|
||||||
|
if (enc3 !== 64) uarray[i + 1] = chr2
|
||||||
|
if (enc4 !== 64) uarray[i + 2] = chr3
|
||||||
|
}
|
||||||
|
|
||||||
|
return uarray
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function i2hex (i) {
|
||||||
|
return ('0' + i.toString(16)).slice(-2)
|
||||||
|
}
|
||||||
|
|
||||||
|
const binaryHashToHex = (hash) => {
|
||||||
|
return hash.reduce(function (memo, i) {
|
||||||
|
return memo + i2hex(i)
|
||||||
|
}, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
const runeOperatorRegex = /[=^$/~<>{}#!]/g
|
||||||
|
|
||||||
|
const operatorToDescription = (operator) => {
|
||||||
|
switch (operator) {
|
||||||
|
case '=':
|
||||||
|
return 'is equal to'
|
||||||
|
case '^':
|
||||||
|
return 'starts with'
|
||||||
|
case '$':
|
||||||
|
return 'ends with'
|
||||||
|
case '/':
|
||||||
|
return 'is not equal to'
|
||||||
|
case '~':
|
||||||
|
return 'contains'
|
||||||
|
case '<':
|
||||||
|
return 'is less than'
|
||||||
|
case '>':
|
||||||
|
return 'is greater than'
|
||||||
|
case '{':
|
||||||
|
return 'sorts before'
|
||||||
|
case '}':
|
||||||
|
return 'sorts after'
|
||||||
|
case '#':
|
||||||
|
return 'comment'
|
||||||
|
case '!':
|
||||||
|
return 'is missing'
|
||||||
|
default:
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const invalidAscii = (str) => !![...str].some((char) => char.charCodeAt(0) > 127)
|
@ -6,12 +6,13 @@ import {
|
|||||||
} from './constants'
|
} from './constants'
|
||||||
import { SUPPORTED_CURRENCIES } from './currency'
|
import { SUPPORTED_CURRENCIES } from './currency'
|
||||||
import { NOSTR_MAX_RELAY_NUM, NOSTR_PUBKEY_BECH32, NOSTR_PUBKEY_HEX } from './nostr'
|
import { NOSTR_MAX_RELAY_NUM, NOSTR_PUBKEY_BECH32, NOSTR_PUBKEY_HEX } from './nostr'
|
||||||
import { msatsToSats, numWithUnits, abbrNum, ensureB64 } from './format'
|
import { msatsToSats, numWithUnits, abbrNum, ensureB64, B64_URL_REGEX } from './format'
|
||||||
import * as usersFragments from '@/fragments/users'
|
import * as usersFragments from '@/fragments/users'
|
||||||
import * as subsFragments from '@/fragments/subs'
|
import * as subsFragments from '@/fragments/subs'
|
||||||
import { isInvoicableMacaroon, isInvoiceMacaroon } from './macaroon'
|
import { isInvoicableMacaroon, isInvoiceMacaroon } from './macaroon'
|
||||||
import { parseNwcUrl } from './url'
|
import { parseNwcUrl } from './url'
|
||||||
import { datePivot } from './time'
|
import { datePivot } from './time'
|
||||||
|
import { decodeRune } from '@/lib/cln'
|
||||||
|
|
||||||
const { SUB } = subsFragments
|
const { SUB } = subsFragments
|
||||||
const { NAME_QUERY } = usersFragments
|
const { NAME_QUERY } = usersFragments
|
||||||
@ -327,6 +328,32 @@ export function LNDAutowithdrawSchema ({ me } = {}) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function CLNAutowithdrawSchema ({ me } = {}) {
|
||||||
|
return object({
|
||||||
|
socket: string().socket().required('required'),
|
||||||
|
rune: string().matches(B64_URL_REGEX, { message: 'invalid rune' }).required('required')
|
||||||
|
.test({
|
||||||
|
name: 'rune',
|
||||||
|
test: (v, context) => {
|
||||||
|
const decoded = decodeRune(v)
|
||||||
|
if (!decoded) return context.createError({ message: 'invalid rune' })
|
||||||
|
if (decoded.restrictions.length === 0) {
|
||||||
|
return context.createError({ message: 'rune must be restricted to method=invoice' })
|
||||||
|
}
|
||||||
|
if (decoded.restrictions.length !== 1 || decoded.restrictions[0].alternatives.length !== 1) {
|
||||||
|
return context.createError({ message: 'rune must be restricted to method=invoice only' })
|
||||||
|
}
|
||||||
|
if (decoded.restrictions[0].alternatives[0] !== 'method=invoice') {
|
||||||
|
return context.createError({ message: 'rune must be restricted to method=invoice only' })
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
cert: hexOrBase64Validator,
|
||||||
|
...autowithdrawSchemaMembers({ me })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function autowithdrawSchemaMembers ({ me } = {}) {
|
export function autowithdrawSchemaMembers ({ me } = {}) {
|
||||||
return {
|
return {
|
||||||
priority: boolean(),
|
priority: boolean(),
|
||||||
|
56
package-lock.json
generated
56
package-lock.json
generated
@ -51,6 +51,7 @@
|
|||||||
"next-auth": "^4.23.2",
|
"next-auth": "^4.23.2",
|
||||||
"next-plausible": "^3.11.1",
|
"next-plausible": "^3.11.1",
|
||||||
"next-seo": "^6.1.0",
|
"next-seo": "^6.1.0",
|
||||||
|
"node-fetch": "^2.6.1",
|
||||||
"node-s3-url-encode": "^0.0.4",
|
"node-s3-url-encode": "^0.0.4",
|
||||||
"nodemailer": "^6.9.6",
|
"nodemailer": "^6.9.6",
|
||||||
"nostr": "^0.2.8",
|
"nostr": "^0.2.8",
|
||||||
@ -371,6 +372,25 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@apollo/server/node_modules/node-fetch": {
|
||||||
|
"version": "2.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||||
|
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||||
|
"dependencies": {
|
||||||
|
"whatwg-url": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "4.x || >=6.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"encoding": "^0.1.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"encoding": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@apollo/server/node_modules/uuid": {
|
"node_modules/@apollo/server/node_modules/uuid": {
|
||||||
"version": "9.0.0",
|
"version": "9.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
|
||||||
@ -7385,6 +7405,25 @@
|
|||||||
"node-fetch": "^2.6.12"
|
"node-fetch": "^2.6.12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cross-fetch/node_modules/node-fetch": {
|
||||||
|
"version": "2.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||||
|
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||||
|
"dependencies": {
|
||||||
|
"whatwg-url": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "4.x || >=6.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"encoding": "^0.1.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"encoding": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.3",
|
"version": "7.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||||
@ -14699,22 +14738,11 @@
|
|||||||
"integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA=="
|
"integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA=="
|
||||||
},
|
},
|
||||||
"node_modules/node-fetch": {
|
"node_modules/node-fetch": {
|
||||||
"version": "2.6.12",
|
"version": "2.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz",
|
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
|
||||||
"integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==",
|
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==",
|
||||||
"dependencies": {
|
|
||||||
"whatwg-url": "^5.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "4.x || >=6.0.0"
|
"node": "4.x || >=6.0.0"
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"encoding": "^0.1.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"encoding": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/node-gyp-build": {
|
"node_modules/node-gyp-build": {
|
||||||
|
@ -56,6 +56,7 @@
|
|||||||
"next-auth": "^4.23.2",
|
"next-auth": "^4.23.2",
|
||||||
"next-plausible": "^3.11.1",
|
"next-plausible": "^3.11.1",
|
||||||
"next-seo": "^6.1.0",
|
"next-seo": "^6.1.0",
|
||||||
|
"node-fetch": "^2.6.1",
|
||||||
"node-s3-url-encode": "^0.0.4",
|
"node-s3-url-encode": "^0.0.4",
|
||||||
"nodemailer": "^6.9.6",
|
"nodemailer": "^6.9.6",
|
||||||
"nostr": "^0.2.8",
|
"nostr": "^0.2.8",
|
||||||
|
136
pages/settings/wallets/cln.js
Normal file
136
pages/settings/wallets/cln.js
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
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 { useApolloClient, useMutation } from '@apollo/client'
|
||||||
|
import { useToast } from '@/components/toast'
|
||||||
|
import { CLNAutowithdrawSchema } from '@/lib/validate'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import { AutowithdrawSettings, autowithdrawInitial } from '@/components/autowithdraw-shared'
|
||||||
|
import { REMOVE_WALLET, UPSERT_WALLET_CLN, WALLET_BY_TYPE } from '@/fragments/wallet'
|
||||||
|
import WalletLogs from '@/components/wallet-logs'
|
||||||
|
import Info from '@/components/info'
|
||||||
|
import Text from '@/components/text'
|
||||||
|
|
||||||
|
const variables = { type: 'CLN' }
|
||||||
|
export const getServerSideProps = getGetServerSideProps({ query: WALLET_BY_TYPE, variables, authRequired: true })
|
||||||
|
|
||||||
|
export default function CLN ({ ssrData }) {
|
||||||
|
const me = useMe()
|
||||||
|
const toaster = useToast()
|
||||||
|
const router = useRouter()
|
||||||
|
const client = useApolloClient()
|
||||||
|
const [upsertWalletCLN] = useMutation(UPSERT_WALLET_CLN, {
|
||||||
|
refetchQueries: ['WalletLogs'],
|
||||||
|
onError: (err) => {
|
||||||
|
client.refetchQueries({ include: ['WalletLogs'] })
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const [removeWallet] = useMutation(REMOVE_WALLET, {
|
||||||
|
refetchQueries: ['WalletLogs'],
|
||||||
|
onError: (err) => {
|
||||||
|
client.refetchQueries({ include: ['WalletLogs'] })
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const { walletByType: wallet } = ssrData || {}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CenterLayout>
|
||||||
|
<h2 className='pb-2'>CLN</h2>
|
||||||
|
<h6 className='text-muted text-center'>autowithdraw to your Core Lightning node via <a href='https://docs.corelightning.org/docs/rest' target='_blank' noreferrer rel='noreferrer'>CLNRest</a></h6>
|
||||||
|
<Form
|
||||||
|
initial={{
|
||||||
|
socket: wallet?.wallet?.socket || '',
|
||||||
|
rune: wallet?.wallet?.rune || '',
|
||||||
|
cert: wallet?.wallet?.cert || '',
|
||||||
|
...autowithdrawInitial({ me, priority: wallet?.priority })
|
||||||
|
}}
|
||||||
|
schema={CLNAutowithdrawSchema({ me })}
|
||||||
|
onSubmit={async ({ socket, rune, cert, ...settings }) => {
|
||||||
|
try {
|
||||||
|
await upsertWalletCLN({
|
||||||
|
variables: {
|
||||||
|
id: wallet?.id,
|
||||||
|
socket,
|
||||||
|
rune,
|
||||||
|
cert,
|
||||||
|
settings: {
|
||||||
|
...settings,
|
||||||
|
autoWithdrawThreshold: Number(settings.autoWithdrawThreshold),
|
||||||
|
autoWithdrawMaxFeePercent: Number(settings.autoWithdrawMaxFeePercent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
toaster.success('saved settings')
|
||||||
|
router.push('/settings/wallets')
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
label='rest host and port'
|
||||||
|
name='socket'
|
||||||
|
hint='tor or clearnet'
|
||||||
|
placeholder='55.5.555.55:3010'
|
||||||
|
clear
|
||||||
|
required
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label={
|
||||||
|
<div className='d-flex align-items-center'>invoice only rune
|
||||||
|
<Info>
|
||||||
|
<Text>
|
||||||
|
{'We only accept runes that *only* allow `method=invoice`.\nRun this to generate one:\n\n```lightning-cli createrune restrictions=\'["method=invoice"]\'```'}
|
||||||
|
</Text>
|
||||||
|
</Info>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
name='rune'
|
||||||
|
clear
|
||||||
|
hint='must be restricted to method=invoice'
|
||||||
|
placeholder='S34KtUW-6gqS_hD_9cc_PNhfF-NinZyBOCgr1aIrark9NCZtZXRob2Q9aW52b2ljZQ=='
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label={<>cert <small className='text-muted ms-2'>optional if from <a href='https://en.wikipedia.org/wiki/Certificate_authority' target='_blank' rel='noreferrer'>CA</a> (e.g. voltage)</small></>}
|
||||||
|
name='cert'
|
||||||
|
clear
|
||||||
|
hint='hex or base64 encoded'
|
||||||
|
placeholder='LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNNVENDQWRpZ0F3SUJBZ0lRSHBFdFdrcGJwZHV4RVF2eVBPc3NWVEFLQmdncWhrak9QUVFEQWpBdk1SOHcKSFFZRFZRUUtFeFpzYm1RZ1lYVjBiMmRsYm1WeVlYUmxaQ0JqWlhKME1Rd3dDZ1lEVlFRREV3TmliMkl3SGhjTgpNalF3TVRBM01qQXhORE0wV2hjTk1qVXdNekF6TWpBeE5ETTBXakF2TVI4d0hRWURWUVFLRXhac2JtUWdZWFYwCmIyZGxibVZ5WVhSbFpDQmpaWEowTVF3d0NnWURWUVFERXdOaWIySXdXVEFUQmdjcWhrak9QUUlCQmdncWhrak8KUFFNQkJ3TkNBQVJUS3NMVk5oZnhqb1FLVDlkVVdDbzUzSmQwTnBuL1BtYi9LUE02M1JxbU52dFYvdFk4NjJJZwpSbE41cmNHRnBEajhUeFc2OVhIK0pTcHpjWDdlN3N0Um80SFZNSUhTTUE0R0ExVWREd0VCL3dRRUF3SUNwREFUCkJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREFUQVBCZ05WSFJNQkFmOEVCVEFEQVFIL01CMEdBMVVkRGdRV0JCVDAKMnh3V25GeHRUNzI0MWxwZlNoNm9FWi9UMWpCN0JnTlZIUkVFZERCeWdnTmliMktDQ1d4dlkyRnNhRzl6ZElJRApZbTlpZ2d4d2IyeGhjaTF1TVMxaWIyS0NGR2h2YzNRdVpHOWphMlZ5TG1sdWRHVnlibUZzZ2dSMWJtbDRnZ3AxCmJtbDRjR0ZqYTJWMGdnZGlkV1pqYjI1dWh3Ui9BQUFCaHhBQUFBQUFBQUFBQUFBQUFBQUFBQUFCaHdTc0VnQUQKTUFvR0NDcUdTTTQ5QkFNQ0EwY0FNRVFDSUEwUTlkRXdoNXpPRnpwL3hYeHNpemh5SkxNVG5yazU1VWx1NHJPRwo4WW52QWlBVGt4U3p3Y3hZZnFscGx0UlNIbmd0NUJFcDBzcXlHL05nenBzb2pmMGNqQT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K'
|
||||||
|
/>
|
||||||
|
<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)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
<div className='mt-3 w-100'>
|
||||||
|
<WalletLogs wallet='cln' embedded />
|
||||||
|
</div>
|
||||||
|
</CenterLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CLNCard ({ wallet }) {
|
||||||
|
return (
|
||||||
|
<WalletCard
|
||||||
|
title='CLN'
|
||||||
|
badges={['receive only', 'non-custodial']}
|
||||||
|
provider='cln'
|
||||||
|
enabled={wallet !== undefined || undefined}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
@ -6,6 +6,7 @@ import { LightningAddressWalletCard } from './lightning-address'
|
|||||||
import { LNbitsCard } from './lnbits'
|
import { LNbitsCard } from './lnbits'
|
||||||
import { NWCCard } from './nwc'
|
import { NWCCard } from './nwc'
|
||||||
import { LNDCard } from './lnd'
|
import { LNDCard } from './lnd'
|
||||||
|
import { CLNCard } from './cln'
|
||||||
import { WALLETS } from '@/fragments/wallet'
|
import { WALLETS } from '@/fragments/wallet'
|
||||||
import { useQuery } from '@apollo/client'
|
import { useQuery } from '@apollo/client'
|
||||||
import PageLoading from '@/components/page-loading'
|
import PageLoading from '@/components/page-loading'
|
||||||
@ -19,6 +20,7 @@ export default function Wallet ({ ssrData }) {
|
|||||||
const { wallets } = data || ssrData
|
const { wallets } = data || ssrData
|
||||||
const lnd = wallets.find(w => w.type === 'LND')
|
const lnd = wallets.find(w => w.type === 'LND')
|
||||||
const lnaddr = wallets.find(w => w.type === 'LIGHTNING_ADDRESS')
|
const lnaddr = wallets.find(w => w.type === 'LIGHTNING_ADDRESS')
|
||||||
|
const cln = wallets.find(w => w.type === 'CLN')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
@ -28,6 +30,7 @@ export default function Wallet ({ ssrData }) {
|
|||||||
<div className={styles.walletGrid}>
|
<div className={styles.walletGrid}>
|
||||||
<LightningAddressWalletCard wallet={lnaddr} />
|
<LightningAddressWalletCard wallet={lnaddr} />
|
||||||
<LNDCard wallet={lnd} />
|
<LNDCard wallet={lnd} />
|
||||||
|
<CLNCard wallet={cln} />
|
||||||
<LNbitsCard />
|
<LNbitsCard />
|
||||||
<NWCCard />
|
<NWCCard />
|
||||||
<WalletCard title='coming soon' badges={['probably']} />
|
<WalletCard title='coming soon' badges={['probably']} />
|
||||||
|
25
prisma/migrations/20240410060146_wallet_cln/migration.sql
Normal file
25
prisma/migrations/20240410060146_wallet_cln/migration.sql
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
-- AlterEnum
|
||||||
|
ALTER TYPE "WalletType" ADD VALUE 'CLN';
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "WalletCLN" (
|
||||||
|
"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,
|
||||||
|
"cert" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "WalletCLN_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "WalletCLN_walletId_key" ON "WalletCLN"("walletId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "WalletCLN" ADD CONSTRAINT "WalletCLN_walletId_fkey" FOREIGN KEY ("walletId") REFERENCES "Wallet"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
CREATE TRIGGER wallet_cln_as_jsonb
|
||||||
|
AFTER INSERT OR UPDATE ON "WalletCLN"
|
||||||
|
FOR EACH ROW EXECUTE PROCEDURE wallet_wallet_type_as_jsonb();
|
@ -133,6 +133,7 @@ model User {
|
|||||||
enum WalletType {
|
enum WalletType {
|
||||||
LIGHTNING_ADDRESS
|
LIGHTNING_ADDRESS
|
||||||
LND
|
LND
|
||||||
|
CLN
|
||||||
}
|
}
|
||||||
|
|
||||||
model Wallet {
|
model Wallet {
|
||||||
@ -154,6 +155,7 @@ model Wallet {
|
|||||||
wallet Json? @db.JsonB
|
wallet Json? @db.JsonB
|
||||||
walletLightningAddress WalletLightningAddress?
|
walletLightningAddress WalletLightningAddress?
|
||||||
walletLND WalletLND?
|
walletLND WalletLND?
|
||||||
|
walletCLN WalletCLN?
|
||||||
|
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
}
|
}
|
||||||
@ -190,6 +192,17 @@ model WalletLND {
|
|||||||
cert String?
|
cert String?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model WalletCLN {
|
||||||
|
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
|
||||||
|
cert String?
|
||||||
|
}
|
||||||
|
|
||||||
model Mute {
|
model Mute {
|
||||||
muterId Int
|
muterId Int
|
||||||
mutedId Int
|
mutedId Int
|
||||||
|
68
sndev
68
sndev
@ -48,6 +48,17 @@ docker__stacker_lnd() {
|
|||||||
docker__exec $t -u lnd stacker_lnd lncli "$@"
|
docker__exec $t -u lnd stacker_lnd lncli "$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
docker__stacker_cln() {
|
||||||
|
t=$1
|
||||||
|
if [ "$t" = "-t" ]; then
|
||||||
|
shift
|
||||||
|
else
|
||||||
|
t=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker__exec $t -u clightning stacker_cln lightning-cli --regtest "$@"
|
||||||
|
}
|
||||||
|
|
||||||
sndev__start() {
|
sndev__start() {
|
||||||
shift
|
shift
|
||||||
|
|
||||||
@ -207,6 +218,35 @@ OPTIONS"
|
|||||||
docker__stacker_lnd payinvoice -h | awk '/OPTIONS:/{y=1;next}y' | awk '!/^[\t ]+--pay_req value/'
|
docker__stacker_lnd payinvoice -h | awk '/OPTIONS:/{y=1;next}y' | awk '!/^[\t ]+--pay_req value/'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sndev__cln_fund() {
|
||||||
|
shift
|
||||||
|
docker__stacker_cln -t pay "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
sndev__help_cln_fund() {
|
||||||
|
help="
|
||||||
|
pay a bolt11 for funding with CLN
|
||||||
|
|
||||||
|
USAGE
|
||||||
|
$ sndev cln_fund <bolt11>"
|
||||||
|
echo "$help"
|
||||||
|
}
|
||||||
|
|
||||||
|
sndev__cln_withdraw() {
|
||||||
|
shift
|
||||||
|
label=$(date +%s)
|
||||||
|
docker__stacker_cln -t invoice "$1" "$label" sndev | jq -r '.bolt11'
|
||||||
|
}
|
||||||
|
|
||||||
|
sndev__help_cln_withdraw() {
|
||||||
|
help="
|
||||||
|
create a bolt11 for withdrawal with CLN
|
||||||
|
|
||||||
|
USAGE
|
||||||
|
$ sndev cln_withdraw <amount msats>"
|
||||||
|
echo "$help"
|
||||||
|
}
|
||||||
|
|
||||||
sndev__withdraw() {
|
sndev__withdraw() {
|
||||||
shift
|
shift
|
||||||
docker__stacker_lnd addinvoice --amt "$@" | jq -r '.payment_request'
|
docker__stacker_lnd addinvoice --amt "$@" | jq -r '.payment_request'
|
||||||
@ -287,24 +327,33 @@ sndev__help_compose() {
|
|||||||
docker__compose --help
|
docker__compose --help
|
||||||
}
|
}
|
||||||
|
|
||||||
sndev__sn_lncli() {
|
sndev__sn_lndcli() {
|
||||||
shift
|
shift
|
||||||
docker__sn_lnd -t "$@"
|
docker__sn_lnd -t "$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
sndev__help_sn_lncli() {
|
sndev__help_sn_lndcli() {
|
||||||
docker__sn_lnd --help
|
docker__sn_lnd --help
|
||||||
}
|
}
|
||||||
|
|
||||||
sndev__stacker_lncli() {
|
sndev__stacker_lndcli() {
|
||||||
shift
|
shift
|
||||||
docker__stacker_lnd -t "$@"
|
docker__stacker_lnd -t "$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
sndev__help_stacker_lncli() {
|
sndev__help_stacker_lndcli() {
|
||||||
docker__stacker_lnd --help
|
docker__stacker_lnd --help
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sndev__stacker_clncli() {
|
||||||
|
shift
|
||||||
|
docker__stacker_cln -t "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
sndev__help_stacker_clncli() {
|
||||||
|
docker__stacker_cln help
|
||||||
|
}
|
||||||
|
|
||||||
__sndev__pr_track() {
|
__sndev__pr_track() {
|
||||||
json=$(curl -fsSH "Accept: application/vnd.github.v3+json" "https://api.github.com/repos/stackernews/stacker.news/pulls/$1")
|
json=$(curl -fsSH "Accept: application/vnd.github.v3+json" "https://api.github.com/repos/stackernews/stacker.news/pulls/$1")
|
||||||
case $(git config --get remote.origin.url) in
|
case $(git config --get remote.origin.url) in
|
||||||
@ -441,6 +490,10 @@ COMMANDS
|
|||||||
fund pay a bolt11 for funding
|
fund pay a bolt11 for funding
|
||||||
withdraw create a bolt11 for withdrawal
|
withdraw create a bolt11 for withdrawal
|
||||||
|
|
||||||
|
cln:
|
||||||
|
cln_fund pay a bolt11 for funding with CLN
|
||||||
|
cln_withdraw create a bolt11 for withdrawal with CLN
|
||||||
|
|
||||||
db:
|
db:
|
||||||
psql open psql on db
|
psql open psql on db
|
||||||
prisma run prisma commands
|
prisma run prisma commands
|
||||||
@ -450,9 +503,10 @@ COMMANDS
|
|||||||
lint run linters
|
lint run linters
|
||||||
|
|
||||||
other:
|
other:
|
||||||
compose docker compose passthrough
|
compose docker compose passthrough
|
||||||
sn_lncli lncli passthrough on sn_lnd
|
sn_lndcli lncli passthrough on sn_lnd
|
||||||
stacker_lncli lncli passthrough on stacker_lnd
|
stacker_lndcli lncli passthrough on stacker_lnd
|
||||||
|
stacker_clncli lightning-cli passthrough on stacker_cln
|
||||||
"
|
"
|
||||||
echo "$help"
|
echo "$help"
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import { authenticatedLndGrpc, createInvoice } from 'ln-service'
|
|||||||
import { msatsToSats, numWithUnits, satsToMsats } from '@/lib/format'
|
import { msatsToSats, numWithUnits, satsToMsats } from '@/lib/format'
|
||||||
import { datePivot } from '@/lib/time'
|
import { datePivot } from '@/lib/time'
|
||||||
import { createWithdrawal, sendToLnAddr, addWalletLog } from '@/api/resolvers/wallet'
|
import { createWithdrawal, sendToLnAddr, addWalletLog } from '@/api/resolvers/wallet'
|
||||||
|
import { createInvoice as createInvoiceCLN } from '@/lib/cln'
|
||||||
|
|
||||||
export async function autoWithdraw ({ data: { id }, models, lnd }) {
|
export async function autoWithdraw ({ data: { id }, models, lnd }) {
|
||||||
const user = await models.user.findUnique({ where: { id } })
|
const user = await models.user.findUnique({ where: { id } })
|
||||||
@ -55,6 +56,15 @@ export async function autoWithdraw ({ data: { id }, models, lnd }) {
|
|||||||
level: 'SUCCESS',
|
level: 'SUCCESS',
|
||||||
message
|
message
|
||||||
}, { me: user, models })
|
}, { me: user, models })
|
||||||
|
} else if (wallet.type === 'CLN') {
|
||||||
|
await autowithdrawCLN(
|
||||||
|
{ amount, maxFee },
|
||||||
|
{ models, me: user, lnd })
|
||||||
|
await addWalletLog({
|
||||||
|
wallet: 'walletCLN',
|
||||||
|
level: 'SUCCESS',
|
||||||
|
message
|
||||||
|
}, { me: user, models })
|
||||||
} else if (wallet.type === 'LIGHTNING_ADDRESS') {
|
} else if (wallet.type === 'LIGHTNING_ADDRESS') {
|
||||||
await autowithdrawLNAddr(
|
await autowithdrawLNAddr(
|
||||||
{ amount, maxFee },
|
{ amount, maxFee },
|
||||||
@ -72,7 +82,9 @@ export async function autoWithdraw ({ data: { id }, models, lnd }) {
|
|||||||
// LND errors are in this shape: [code, type, { err: { code, details, metadata } }]
|
// LND errors are in this shape: [code, type, { err: { code, details, metadata } }]
|
||||||
const details = error[2]?.err?.details || error.message || error.toString?.()
|
const details = error[2]?.err?.details || error.message || error.toString?.()
|
||||||
await addWalletLog({
|
await addWalletLog({
|
||||||
wallet: wallet.type === 'LND' ? 'walletLND' : 'walletLightningAddress',
|
wallet: wallet.type === 'LND'
|
||||||
|
? 'walletLND'
|
||||||
|
: wallet.type === 'CLN' ? 'walletCLN' : 'walletLightningAddress',
|
||||||
level: 'ERROR',
|
level: 'ERROR',
|
||||||
message: 'autowithdrawal failed: ' + details
|
message: 'autowithdrawal failed: ' + details
|
||||||
})
|
})
|
||||||
@ -142,3 +154,36 @@ async function autowithdrawLND ({ amount, maxFee }, { me, models, lnd }) {
|
|||||||
|
|
||||||
return await createWithdrawal(null, { invoice: invoice.request, maxFee }, { me, models, lnd, autoWithdraw: true })
|
return await createWithdrawal(null, { invoice: invoice.request, maxFee }, { me, models, lnd, autoWithdraw: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function autowithdrawCLN ({ amount, maxFee }, { me, models, lnd }) {
|
||||||
|
if (!me) {
|
||||||
|
throw new Error('me not specified')
|
||||||
|
}
|
||||||
|
|
||||||
|
const wallet = await models.wallet.findFirst({
|
||||||
|
where: {
|
||||||
|
userId: me.id,
|
||||||
|
type: 'CLN'
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
walletCLN: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!wallet || !wallet.walletCLN) {
|
||||||
|
throw new Error('no cln wallet found')
|
||||||
|
}
|
||||||
|
|
||||||
|
const { walletCLN: { cert, rune, socket } } = wallet
|
||||||
|
|
||||||
|
const inv = await createInvoiceCLN({
|
||||||
|
socket,
|
||||||
|
rune,
|
||||||
|
cert,
|
||||||
|
description: me.hideInvoiceDesc ? undefined : 'autowithdraw to CLN from SN',
|
||||||
|
msats: amount + 'sat',
|
||||||
|
expiry: 360
|
||||||
|
})
|
||||||
|
|
||||||
|
return await createWithdrawal(null, { invoice: inv.bolt11, maxFee }, { me, models, lnd, autoWithdraw: true })
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user