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_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
|
||||
|
||||
# localstack container stuff
|
||||
|
|
|
@ -7,11 +7,12 @@ 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 { 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 { datePivot } from '@/lib/time'
|
||||
import assertGofacYourself from './ofac'
|
||||
import assertApiKeyNotPermitted from './apiKey'
|
||||
import { createInvoice as createInvoiceCLN } from '@/lib/cln'
|
||||
|
||||
export async function getInvoice (parent, { id }, { me, models, lnd }) {
|
||||
const inv = await models.invoice.findUnique({
|
||||
|
@ -323,7 +324,7 @@ export default {
|
|||
},
|
||||
WalletDetails: {
|
||||
__resolveType (wallet) {
|
||||
return wallet.address ? 'WalletLNAddr' : 'WalletLND'
|
||||
return wallet.address ? 'WalletLNAddr' : wallet.macaroon ? 'WalletLND' : 'WalletCLN'
|
||||
}
|
||||
},
|
||||
Mutation: {
|
||||
|
@ -466,6 +467,36 @@ export default {
|
|||
},
|
||||
{ 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 }) => {
|
||||
const wallet = 'walletLightningAddress'
|
||||
return await upsertWallet(
|
||||
|
@ -495,6 +526,8 @@ export default {
|
|||
let walletName = ''
|
||||
if (wallet.type === 'LND') {
|
||||
walletName = 'walletLND'
|
||||
} else if (wallet.type === 'CLN') {
|
||||
walletName = 'walletCLN'
|
||||
} else if (wallet.type === 'LIGHTNING_ADDRESS') {
|
||||
walletName = 'walletLightningAddress'
|
||||
}
|
||||
|
|
|
@ -20,6 +20,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
|
||||
upsertWalletCLN(id: ID, socket: String!, rune: String!, cert: String, settings: AutowithdrawSettings!): Boolean
|
||||
upsertWalletLNAddr(id: ID, address: String!, settings: AutowithdrawSettings!): Boolean
|
||||
removeWallet(id: ID!): Boolean
|
||||
}
|
||||
|
@ -42,7 +43,13 @@ export default gql`
|
|||
cert: String
|
||||
}
|
||||
|
||||
union WalletDetails = WalletLNAddr | WalletLND
|
||||
type WalletCLN {
|
||||
socket: String!
|
||||
rune: String!
|
||||
cert: String
|
||||
}
|
||||
|
||||
union WalletDetails = WalletLNAddr | WalletLND | WalletCLN
|
||||
|
||||
input AutowithdrawSettings {
|
||||
autoWithdrawThreshold: Int!
|
||||
|
|
|
@ -8,7 +8,7 @@ export default function LogMessage ({ wallet, level, message, ts }) {
|
|||
<tr className={styles.line}>
|
||||
<td className={styles.timestamp}>{timeSince(new Date(ts))}</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>
|
||||
</tr>
|
||||
)
|
||||
|
|
|
@ -160,6 +160,7 @@ const initIndexedDB = async (storeName) => {
|
|||
const renameWallet = (wallet) => {
|
||||
if (wallet === 'walletLightningAddress') return 'lnAddr'
|
||||
if (wallet === 'walletLND') return 'lnd'
|
||||
if (wallet === 'walletCLN') return 'cln'
|
||||
return wallet
|
||||
}
|
||||
|
||||
|
|
|
@ -253,14 +253,17 @@ services:
|
|||
bash -c '
|
||||
blockcount=$$(bitcoin-cli -chain=regtest -rpcport=${RPC_PORT} -rpcuser=${RPC_USER} -rpcpassword=${RPC_PASS} getblockcount 2>/dev/null)
|
||||
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 ${STACKER_LND_ADDR}
|
||||
bitcoin-cli -chain=regtest -rpcport=${RPC_PORT} -rpcuser=${RPC_USER} -rpcpassword=${RPC_PASS} generatetoaddress 100 ${STACKER_CLN_ADDR}
|
||||
else
|
||||
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}
|
||||
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}
|
||||
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
|
||||
'
|
||||
sn_lnd:
|
||||
|
@ -381,8 +384,8 @@ services:
|
|||
- stacker_lnd:/home/lnd/.lnd
|
||||
labels:
|
||||
ofelia.enabled: "true"
|
||||
ofelia.job-exec.stacker_channel_cron.schedule: "@every 1m"
|
||||
ofelia.job-exec.stacker_channel_cron.command: >
|
||||
ofelia.job-exec.stacker_lnd_channel_cron.schedule: "@every 1m"
|
||||
ofelia.job-exec.stacker_lnd_channel_cron.command: >
|
||||
su lnd -c bash -c "
|
||||
if [ $$(lncli getinfo | jq '.num_active_channels + .num_pending_channels') -ge 3 ]; then
|
||||
exit 0
|
||||
|
@ -391,6 +394,55 @@ services:
|
|||
--min_confs 0 --local_amt=1000000000 --push_amt=500000000
|
||||
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:
|
||||
image: mcuadros/ofelia:latest
|
||||
container_name: channdler
|
||||
|
@ -427,4 +479,5 @@ volumes:
|
|||
bitcoin:
|
||||
sn_lnd:
|
||||
stacker_lnd:
|
||||
stacker_cln:
|
||||
s3:
|
||||
|
|
|
@ -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/"]
|
|
@ -0,0 +1,5 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgXhrU/UORcVCRnDbo
|
||||
KF35C+BM3FIlCvBX0Y5J2J9piE+hRANCAAQY5mh7rpXIMEhjp5SwB2EJT0kMBlwz
|
||||
SVsn81a1fYUxWgUXUJNWLnLo00PQKq5xCKxXc9fzs/tl1w+oANsTp2/u
|
||||
-----END PRIVATE KEY-----
|
|
@ -0,0 +1,10 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIBcjCCARigAwIBAgIJANrSvPZ/Y3KEMAoGCCqGSM49BAMCMBYxFDASBgNVBAMM
|
||||
C2NsbiBSb290IENBMCAXDTc1MDEwMTAwMDAwMFoYDzQwOTYwMTAxMDAwMDAwWjAW
|
||||
MRQwEgYDVQQDDAtjbG4gUm9vdCBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IA
|
||||
BBjmaHuulcgwSGOnlLAHYQlPSQwGXDNJWyfzVrV9hTFaBRdQk1YucujTQ9AqrnEI
|
||||
rFdz1/Oz+2XXD6gA2xOnb+6jTTBLMBkGA1UdEQQSMBCCA2NsboIJbG9jYWxob3N0
|
||||
MB0GA1UdDgQWBBSEcmN/9rzS2hR6G7EIgsX+51N0CjAPBgNVHRMBAf8EBTADAQH/
|
||||
MAoGCCqGSM49BAMCA0gAMEUCIHCePvNSvyiBYawqKgQquw9J2WVyJxn2MIYIqz9S
|
||||
QL18AiEAh8BVDz8pX7Nsll8sb0bO0RZ49cvqQocCgVabqJuSuik=
|
||||
-----END CERTIFICATE-----
|
|
@ -0,0 +1 @@
|
|||
70A2D30FE991B24B5A6BF85421BE5EF083665E7E
|
|
@ -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-----
|
|
@ -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-----
|
|
@ -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-----
|
|
@ -0,0 +1 @@
|
|||
$ְDMBֶװהמ$«bד†y5ת<35>$E0”<30>°P·§)ּ
|
|
@ -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-----
|
|
@ -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-----
|
|
@ -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 =
|
||||
gql`
|
||||
mutation removeWallet($id: ID!) {
|
||||
|
@ -113,6 +120,11 @@ export const WALLET = gql`
|
|||
macaroon
|
||||
cert
|
||||
}
|
||||
... on WalletCLN {
|
||||
socket
|
||||
rune
|
||||
cert
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -135,6 +147,11 @@ export const WALLET_BY_TYPE = gql`
|
|||
macaroon
|
||||
cert
|
||||
}
|
||||
... on WalletCLN {
|
||||
socket
|
||||
rune
|
||||
cert
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
import { SUPPORTED_CURRENCIES } from './currency'
|
||||
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 subsFragments from '@/fragments/subs'
|
||||
import { isInvoicableMacaroon, isInvoiceMacaroon } from './macaroon'
|
||||
import { parseNwcUrl } from './url'
|
||||
import { datePivot } from './time'
|
||||
import { decodeRune } from '@/lib/cln'
|
||||
|
||||
const { SUB } = subsFragments
|
||||
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 } = {}) {
|
||||
return {
|
||||
priority: boolean(),
|
||||
|
|
|
@ -51,6 +51,7 @@
|
|||
"next-auth": "^4.23.2",
|
||||
"next-plausible": "^3.11.1",
|
||||
"next-seo": "^6.1.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"node-s3-url-encode": "^0.0.4",
|
||||
"nodemailer": "^6.9.6",
|
||||
"nostr": "^0.2.8",
|
||||
|
@ -371,6 +372,25 @@
|
|||
"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": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
|
||||
|
@ -7385,6 +7405,25 @@
|
|||
"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": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
|
@ -14699,22 +14738,11 @@
|
|||
"integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA=="
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.6.12",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz",
|
||||
"integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
|
||||
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==",
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"encoding": "^0.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"encoding": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/node-gyp-build": {
|
||||
|
|
|
@ -56,6 +56,7 @@
|
|||
"next-auth": "^4.23.2",
|
||||
"next-plausible": "^3.11.1",
|
||||
"next-seo": "^6.1.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"node-s3-url-encode": "^0.0.4",
|
||||
"nodemailer": "^6.9.6",
|
||||
"nostr": "^0.2.8",
|
||||
|
|
|
@ -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 { NWCCard } from './nwc'
|
||||
import { LNDCard } from './lnd'
|
||||
import { CLNCard } from './cln'
|
||||
import { WALLETS } from '@/fragments/wallet'
|
||||
import { useQuery } from '@apollo/client'
|
||||
import PageLoading from '@/components/page-loading'
|
||||
|
@ -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 cln = wallets.find(w => w.type === 'CLN')
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
|
@ -28,6 +30,7 @@ export default function Wallet ({ ssrData }) {
|
|||
<div className={styles.walletGrid}>
|
||||
<LightningAddressWalletCard wallet={lnaddr} />
|
||||
<LNDCard wallet={lnd} />
|
||||
<CLNCard wallet={cln} />
|
||||
<LNbitsCard />
|
||||
<NWCCard />
|
||||
<WalletCard title='coming soon' badges={['probably']} />
|
||||
|
|
|
@ -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 {
|
||||
LIGHTNING_ADDRESS
|
||||
LND
|
||||
CLN
|
||||
}
|
||||
|
||||
model Wallet {
|
||||
|
@ -154,6 +155,7 @@ model Wallet {
|
|||
wallet Json? @db.JsonB
|
||||
walletLightningAddress WalletLightningAddress?
|
||||
walletLND WalletLND?
|
||||
walletCLN WalletCLN?
|
||||
|
||||
@@index([userId])
|
||||
}
|
||||
|
@ -190,6 +192,17 @@ model WalletLND {
|
|||
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 {
|
||||
muterId Int
|
||||
mutedId Int
|
||||
|
|
68
sndev
68
sndev
|
@ -48,6 +48,17 @@ docker__stacker_lnd() {
|
|||
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() {
|
||||
shift
|
||||
|
||||
|
@ -207,6 +218,35 @@ OPTIONS"
|
|||
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() {
|
||||
shift
|
||||
docker__stacker_lnd addinvoice --amt "$@" | jq -r '.payment_request'
|
||||
|
@ -287,24 +327,33 @@ sndev__help_compose() {
|
|||
docker__compose --help
|
||||
}
|
||||
|
||||
sndev__sn_lncli() {
|
||||
sndev__sn_lndcli() {
|
||||
shift
|
||||
docker__sn_lnd -t "$@"
|
||||
}
|
||||
|
||||
sndev__help_sn_lncli() {
|
||||
sndev__help_sn_lndcli() {
|
||||
docker__sn_lnd --help
|
||||
}
|
||||
|
||||
sndev__stacker_lncli() {
|
||||
sndev__stacker_lndcli() {
|
||||
shift
|
||||
docker__stacker_lnd -t "$@"
|
||||
}
|
||||
|
||||
sndev__help_stacker_lncli() {
|
||||
sndev__help_stacker_lndcli() {
|
||||
docker__stacker_lnd --help
|
||||
}
|
||||
|
||||
sndev__stacker_clncli() {
|
||||
shift
|
||||
docker__stacker_cln -t "$@"
|
||||
}
|
||||
|
||||
sndev__help_stacker_clncli() {
|
||||
docker__stacker_cln help
|
||||
}
|
||||
|
||||
__sndev__pr_track() {
|
||||
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
|
||||
|
@ -441,6 +490,10 @@ COMMANDS
|
|||
fund pay a bolt11 for funding
|
||||
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:
|
||||
psql open psql on db
|
||||
prisma run prisma commands
|
||||
|
@ -450,9 +503,10 @@ COMMANDS
|
|||
lint run linters
|
||||
|
||||
other:
|
||||
compose docker compose passthrough
|
||||
sn_lncli lncli passthrough on sn_lnd
|
||||
stacker_lncli lncli passthrough on stacker_lnd
|
||||
compose docker compose passthrough
|
||||
sn_lndcli lncli passthrough on sn_lnd
|
||||
stacker_lndcli lncli passthrough on stacker_lnd
|
||||
stacker_clncli lightning-cli passthrough on stacker_cln
|
||||
"
|
||||
echo "$help"
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import { authenticatedLndGrpc, createInvoice } from 'ln-service'
|
|||
import { msatsToSats, numWithUnits, satsToMsats } from '@/lib/format'
|
||||
import { datePivot } from '@/lib/time'
|
||||
import { createWithdrawal, sendToLnAddr, addWalletLog } from '@/api/resolvers/wallet'
|
||||
import { createInvoice as createInvoiceCLN } from '@/lib/cln'
|
||||
|
||||
export async function autoWithdraw ({ data: { id }, models, lnd }) {
|
||||
const user = await models.user.findUnique({ where: { id } })
|
||||
|
@ -55,6 +56,15 @@ export async function autoWithdraw ({ data: { id }, models, lnd }) {
|
|||
level: 'SUCCESS',
|
||||
message
|
||||
}, { 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') {
|
||||
await autowithdrawLNAddr(
|
||||
{ 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 } }]
|
||||
const details = error[2]?.err?.details || error.message || error.toString?.()
|
||||
await addWalletLog({
|
||||
wallet: wallet.type === 'LND' ? 'walletLND' : 'walletLightningAddress',
|
||||
wallet: wallet.type === 'LND'
|
||||
? 'walletLND'
|
||||
: wallet.type === 'CLN' ? 'walletCLN' : 'walletLightningAddress',
|
||||
level: 'ERROR',
|
||||
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 })
|
||||
}
|
||||
|
||||
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…
Reference in New Issue