diff --git a/.env.development b/.env.development index 419252140..f7abac8b5 100644 --- a/.env.development +++ b/.env.development @@ -183,4 +183,10 @@ CPU_SHARES_IMPORTANT=1024 CPU_SHARES_MODERATE=512 CPU_SHARES_LOW=256 -NEXT_TELEMETRY_DISABLED=1 \ No newline at end of file +NEXT_TELEMETRY_DISABLED=1 + +# LNCD +LNCD_URL=https://lncd:7167 +# xxd -p -c0 docker/lncd/certs/cert.pem +LNCD_CERT=2d2d2d2d2d424547494e2043455254494649434154452d2d2d2d2d0a4d494944717a434341704f6741774942416749554a6872356137726671657867397737665a375930324e37686a5573774451594a4b6f5a496876634e4151454c0a425141775a54454c4d416b474131554542684d43534655784554415042674e564241674d43454a315a4746775a584e304d5245774477594456515148444168430a645752686347567a6444454e4d4173474131554543677745544535445244454e4d417347413155454377774554453544524445534d424147413155454177774a0a6247396a5957786f62334e304d423458445449304d54497a4d5449774d6a63314d566f58445449314d54497a4d5449774d6a63314d566f775a54454c4d416b470a4131554542684d43534655784554415042674e564241674d43454a315a4746775a584e304d524577447759445651514844416843645752686347567a6444454e0a4d4173474131554543677745544535445244454e4d417347413155454377774554453544524445534d424147413155454177774a6247396a5957786f62334e300a4d494942496a414e42676b71686b6947397730424151454641414f43415138414d49494243674b4341514541766c5763515935684a637a527a777877523741570a4c316d7839673143456c3631685255503443624555687a55592b70334334676b786a4f464c61666e3030445a6e56393537677632704d654e624449762f4935700a43753634715136362b3551664e5034485435475a737669317262346f56547775594932684a524f4272314c7a5875706d31446d43786d565944304761384a375a0a5239634f4a474471366f70316e6d643871764e6a32786a7741374e714e6c39642f69384d453236646e484a7a334e71704c61344b2f4550727a754478722b546a0a4e3658374a464157476a503833726e73714a7a73774f364b36664f766d31647550494961504e72316334675678556e773774496c4b6f44664a554651786854620a4c787a48466e4d6b454b4157485548346d4446745162316f4d435a4d704d6853413137483262506a7655784e66575342504f7635616c764e3871582f6e7641760a66774944415141426f314d775554416442674e56485134454667515553784d72386c36574a3049337469536b5755362f4948516b486b5177487759445652306a0a42426777466f415553784d72386c36574a3049337469536b5755362f4948516b486b517744775944565230544151482f42415577417745422f7a414e42676b710a686b6947397730424151734641414f434151454168445238766b75364a706c314a7a51744a62634e4a446c487056365368767053467a725231657858424b66330a324f414167535535577062354c75456b5a70765a4e684c4f6147576c6c6b676d5342712b796134475a3869584f6f4f4d4e31507650556e324e46486b525476650a3746642f476356684444754c51326547664a74532f673465626d6949505362645631627157536c4c6151487367314a30575043474a454b774e7032364e776a470a59436e315378594b496d3546316c504442394847735276634a5972476a785a446f68764e2f536d335a52416a792b637931567247557a3176316a6e73386b596b0a78644144754a4a6e30784966324734385361546f796e2f494335444b4f36567761716f315a4f684546336e3836695149396937306547773873626a41783843620a6152776165716b7362386874303962566e334c484d6e4c624542483433492b74645456755a57377436413d3d0a2d2d2d2d2d454e442043455254494649434154452d2d2d2d2d0a +LNCD_AUTH_TOKEN="satoshi" diff --git a/.gitignore b/.gitignore index 3f2c478fa..3c8534f0c 100644 --- a/.gitignore +++ b/.gitignore @@ -61,4 +61,7 @@ scripts/nwc-keys.json docker/lnbits/data # lndk -!docker/lndk/tls-*.pem \ No newline at end of file +!docker/lndk/tls-*.pem + +# lncd +!docker/lncd/certs/*.pem \ No newline at end of file diff --git a/api/resolvers/wallet.js b/api/resolvers/wallet.js index c71749ac0..7b10d2210 100644 --- a/api/resolvers/wallet.js +++ b/api/resolvers/wallet.js @@ -47,14 +47,19 @@ function injectResolvers (resolvers) { }) } - const validData = await validateWallet(walletDef, - { ...data, ...settings, vaultEntries: vaultEntries ?? existingVaultEntries }, - { serverSide: true }) - if (validData) { - data && Object.keys(validData).filter(key => key in data).forEach(key => { data[key] = validData[key] }) - settings && Object.keys(validData).filter(key => key in settings).forEach(key => { settings[key] = validData[key] }) + const validate = async ({ data, settings, skipGenerated = false }) => { + const validData = await validateWallet(walletDef, + { ...data, ...settings, vaultEntries: vaultEntries ?? existingVaultEntries }, + { serverSide: true, skipGenerated }) + if (validData) { + data && Object.keys(validData).filter(key => key in data).forEach(key => { data[key] = validData[key] }) + settings && Object.keys(validData).filter(key => key in settings).forEach(key => { settings[key] = validData[key] }) + } } + const needsTest = walletDef.testCreateInvoice && validateLightning && canReceive({ def: walletDef, config: data }) + await validate({ data, settings, skipGenerated: needsTest }) + // wallet in shape of db row const wallet = { field: walletDef.walletField, @@ -67,7 +72,7 @@ function injectResolvers (resolvers) { wallet, walletDef, testCreateInvoice: - walletDef.testCreateInvoice && validateLightning && canReceive({ def: walletDef, config: data }) + needsTest ? (data) => withTimeout( walletDef.testCreateInvoice(data, { logger, @@ -79,7 +84,7 @@ function injectResolvers (resolvers) { settings, data, vaultEntries - }, { logger, me, models }) + }, { logger, me, models, validate }) } } console.groupEnd() @@ -785,7 +790,7 @@ export const walletLogger = ({ wallet, models }) => { } async function upsertWallet ( - { wallet, walletDef, testCreateInvoice }, { settings, data, vaultEntries }, { logger, me, models }) { + { wallet, walletDef, testCreateInvoice }, { settings, data, vaultEntries }, { logger, me, models, validate }) { if (!me) { throw new GqlAuthenticationError() } @@ -794,6 +799,7 @@ async function upsertWallet ( if (testCreateInvoice) { try { await testCreateInvoice(data) + await validate({ data, settings, skipGenerated: false }) } catch (err) { const message = 'failed to create test invoice: ' + (err.message || err.toString?.()) logger.error(message) diff --git a/docker-compose.yml b/docker-compose.yml index 31825444e..2c22c843d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -513,6 +513,25 @@ services: CLI: "litcli" CLI_ARGS: "-n regtest --rpcserver localhost:8444" cpu_shares: "${CPU_SHARES_MODERATE}" + lncd: + container_name: lncd + build: + context: ./docker/lncd + profiles: + - wallets + restart: unless-stopped + environment: + - LNCD_DEBUG=true + - LNCD_DEV_UNSAFE_LOG=true + - LNCD_AUTH_TOKEN=${LNCD_AUTH_TOKEN} + healthcheck: + <<: *healthcheck + test: ["CMD", "curl", "-k", "-f", "-H", "Authorization: Bearer ${LNCD_AUTH_TOKEN}", "https://localhost:7167/health"] + depends_on: + litd: + condition: service_healthy + restart: true + cpu_shares: "${CPU_SHARES_MODERATE}" cln: build: context: ./docker/cln diff --git a/docker/lncd/Dockerfile b/docker/lncd/Dockerfile new file mode 100644 index 000000000..6102b431a --- /dev/null +++ b/docker/lncd/Dockerfile @@ -0,0 +1,23 @@ +FROM debian:bookworm-slim +RUN useradd -u 1000 -m lncd + +ARG VERSION=0.3.2 +ARG REPO=stackernews/lncd +ARG DOWNLOAD_URL=https://github.com/$REPO/releases/download/$VERSION/lncd + +RUN mkdir -p /home/lncd +ADD certs /home/lncd/certs + +ENV LNCD_TLS_CERT_PATH=/home/lncd/certs/cert.pem +ENV LNCD_TLS_KEY_PATH=/home/lncd/certs/key.pem + +RUN chown 1000:1000 -Rvf /home/lncd/ &&\ +apt-get update && apt-get install -y curl &&\ +apt-get clean && rm -rf /var/lib/apt/lists/* + +USER lncd +RUN curl --proto '=https' --tlsv1.2 -LsSf $DOWNLOAD_URL -o /home/lncd/lncd && chmod +x /home/lncd/lncd +WORKDIR /home/lncd +EXPOSE 7167 + +CMD ["./lncd"] diff --git a/docker/lncd/certs/cert.pem b/docker/lncd/certs/cert.pem new file mode 100644 index 000000000..fb70d1a52 --- /dev/null +++ b/docker/lncd/certs/cert.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDqzCCApOgAwIBAgIUJhr5a7rfqexg9w7fZ7Y02N7hjUswDQYJKoZIhvcNAQEL +BQAwZTELMAkGA1UEBhMCSFUxETAPBgNVBAgMCEJ1ZGFwZXN0MREwDwYDVQQHDAhC +dWRhcGVzdDENMAsGA1UECgwETE5DRDENMAsGA1UECwwETE5DRDESMBAGA1UEAwwJ +bG9jYWxob3N0MB4XDTI0MTIzMTIwMjc1MVoXDTI1MTIzMTIwMjc1MVowZTELMAkG +A1UEBhMCSFUxETAPBgNVBAgMCEJ1ZGFwZXN0MREwDwYDVQQHDAhCdWRhcGVzdDEN +MAsGA1UECgwETE5DRDENMAsGA1UECwwETE5DRDESMBAGA1UEAwwJbG9jYWxob3N0 +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvlWcQY5hJczRzwxwR7AW +L1mx9g1CEl61hRUP4CbEUhzUY+p3C4gkxjOFLafn00DZnV957gv2pMeNbDIv/I5p +Cu64qQ66+5QfNP4HT5GZsvi1rb4oVTwuYI2hJROBr1LzXupm1DmCxmVYD0Ga8J7Z +R9cOJGDq6op1nmd8qvNj2xjwA7NqNl9d/i8ME26dnHJz3NqpLa4K/EPrzuDxr+Tj +N6X7JFAWGjP83rnsqJzswO6K6fOvm1duPIIaPNr1c4gVxUnw7tIlKoDfJUFQxhTb +LxzHFnMkEKAWHUH4mDFtQb1oMCZMpMhSA17H2bPjvUxNfWSBPOv5alvN8qX/nvAv +fwIDAQABo1MwUTAdBgNVHQ4EFgQUSxMr8l6WJ0I3tiSkWU6/IHQkHkQwHwYDVR0j +BBgwFoAUSxMr8l6WJ0I3tiSkWU6/IHQkHkQwDwYDVR0TAQH/BAUwAwEB/zANBgkq +hkiG9w0BAQsFAAOCAQEAhDR8vku6Jpl1JzQtJbcNJDlHpV6ShvpSFzrR1exXBKf3 +2OAAgSU5Wpb5LuEkZpvZNhLOaGWllkgmSBq+ya4GZ8iXOoOMN1PvPUn2NFHkRTve +7Fd/GcVhDDuLQ2eGfJtS/g4ebmiIPSbdV1bqWSlLaQHsg1J0WPCGJEKwNp26NwjG +YCn1SxYKIm5F1lPDB9HGsRvcJYrGjxZDohvN/Sm3ZRAjy+cy1VrGUz1v1jns8kYk +xdADuJJn0xIf2G48SaToyn/IC5DKO6Vwaqo1ZOhEF3n86iQI9i70eGw8sbjAx8Cb +aRwaeqksb8ht09bVn3LHMnLbEBH43I+tdTVuZW7t6A== +-----END CERTIFICATE----- diff --git a/docker/lncd/certs/key.pem b/docker/lncd/certs/key.pem new file mode 100644 index 000000000..723334d64 --- /dev/null +++ b/docker/lncd/certs/key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC+VZxBjmElzNHP +DHBHsBYvWbH2DUISXrWFFQ/gJsRSHNRj6ncLiCTGM4Utp+fTQNmdX3nuC/akx41s +Mi/8jmkK7ripDrr7lB80/gdPkZmy+LWtvihVPC5gjaElE4GvUvNe6mbUOYLGZVgP +QZrwntlH1w4kYOrqinWeZ3yq82PbGPADs2o2X13+LwwTbp2ccnPc2qktrgr8Q+vO +4PGv5OM3pfskUBYaM/zeueyonOzA7orp86+bV248gho82vVziBXFSfDu0iUqgN8l +QVDGFNsvHMcWcyQQoBYdQfiYMW1BvWgwJkykyFIDXsfZs+O9TE19ZIE86/lqW83y +pf+e8C9/AgMBAAECggEAOPjlQdY9jBQIBWLixQKXUWsW0uDbEyaYTRKl4uGXyEBq +7tGC+sewwkcvqR/mS5zQxsOKes/H70DwOx+2r3FtTeFxEuGe5KlMwg773zxk9mZt +82jFJ+ZQt4QNZUy2d+VrhdDCIOpqE7rIJiDsIPRbc56S1B7SkowJcvXlIkKidDU4 +SkOucahVUwpMz7IaTtfQNKJzCc9Ut5Q4cVizZbtNSVjhzvicLenjDTgqRoDxGa80 +ZDd8Jtb/BMXJVTGs4W+I7Fu+KRCFc3FW/i3kHGyr7cqjLyzV5nIJnCJ9rXNukDYf +1TSdPfQyXc6wuhq3dK/tjfbuux82jipdBrKg4yGbiQKBgQD91QPwDAkWjNnteJSm +up21/j48vrdBsAdHJeK78KL4Mm6vMuqsp7kVm6sNGofRzv9KV6RWPw1zA5AVhJbF +ZZVvRRI28xpPWbh392ZJzRuHpHDrmurk/+OAEwi3cnolRfs5YHg87uZEqubuWWdK +4O8weu2TLT10WmW0mqtOaqc/pQKBgQC/9cMbqqWICOzTnsR78+dPoZTEfadEMJ/F +HvMA4FRy3IYxwPMNucH/7MTwaCFnR2GiN+2MWvS6aBWNTORBwwKHBp9+GYqSSGNQ +EvK8MUYo2XI6snWBdapayoaXNlO5KCiLtY+s46FYtW9Y3vUabZP8Khc9K0hyl3+r +KZq6hmbJUwKBgQC0SRvm32WFEr2moUJTubBSlyX1VzAqA7Dno80K17uotYlP/sYX +o3keE9bGE9Xr+y3vy5f5egc/bYRlBCtiQOiGg3SQetJxEbSn4JxSRtAK440gioPT +6rvXN621PiXrW204L4C3Jqd+ljQ3jmCDGohI0sbzBerkPWCHimOp6q6n0QKBgDnW +MH0Lg2hjWAfC1GyMZmtFwe8Z9OXEyL65vnnLHWamLwCapCDEkUEs84GDvlzB0xbv +RvF9DjOD3MqAGl3+BartQezagTfl+5ZKvzwYlI0GRzaMQn9JFpTYZIj2427sPJsr +jyiGRTzXHb9nHe5iia32eJ4DhoaQQvUtSeNdT2blAoGBANjREQBuA46nllFondmI +G5/XGOGYJAT7BiJ/hqq0XK9k+EHVhkJoLwC6DxomNAdySo11BSrSgKS83QXMLpam +8kznu/OOtG2+V4uCdR4X2/cQRNLDXUAr107QbAuQoWy05mfXy6HEchLv0Cwtz9s7 +jdMAG8IYu7kUvokgc+/Uc4sh +-----END PRIVATE KEY----- diff --git a/fragments/wallet.js b/fragments/wallet.js index f75d6547e..74283b536 100644 --- a/fragments/wallet.js +++ b/fragments/wallet.js @@ -169,6 +169,12 @@ export const WALLET_FIELDS = gql` apiKeyRecv currencyRecv } + ... on WalletLnc { + pairingPhraseRecv + localKeyRecv + remoteKeyRecv + serverHostRecv + } } } ` diff --git a/prisma/migrations/20241223232401_lnc_recv/migration.sql b/prisma/migrations/20241223232401_lnc_recv/migration.sql new file mode 100644 index 000000000..2d3ea0838 --- /dev/null +++ b/prisma/migrations/20241223232401_lnc_recv/migration.sql @@ -0,0 +1,22 @@ +-- CreateTable +CREATE TABLE "WalletLNC" ( + "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, + "pairingPhraseRecv" TEXT NOT NULL, + "localKeyRecv" TEXT, + "remoteKeyRecv" TEXT, + "serverHostRecv" TEXT, + CONSTRAINT "WalletLNC_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "WalletLNC_walletId_key" ON "WalletLNC"("walletId"); + +-- AddForeignKey +ALTER TABLE "WalletLNC" ADD CONSTRAINT "WalletLNC_walletId_fkey" FOREIGN KEY ("walletId") REFERENCES "Wallet"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +CREATE TRIGGER wallet_lnc_as_jsonb +AFTER INSERT OR UPDATE ON "WalletLNC" +FOR EACH ROW EXECUTE PROCEDURE wallet_wallet_type_as_jsonb(); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index dcdb3b938..5f9536e2b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -220,6 +220,7 @@ model Wallet { walletNWC WalletNWC? walletPhoenixd WalletPhoenixd? walletBlink WalletBlink? + walletLNC WalletLNC? vaultEntries VaultEntry[] @relation("VaultEntries") withdrawals Withdrawl[] @@ -319,6 +320,18 @@ model WalletBlink { currencyRecv String? } +model WalletLNC { + 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") + pairingPhraseRecv String + localKeyRecv String? + remoteKeyRecv String? + serverHostRecv String? +} + model WalletPhoenixd { id Int @id @default(autoincrement()) walletId Int @unique diff --git a/wallets/config.js b/wallets/config.js index 25f1f2ba2..ae5f02900 100644 --- a/wallets/config.js +++ b/wallets/config.js @@ -64,7 +64,7 @@ export function useWalletConfigurator (wallet) { throw err } } else if (canReceive({ def: wallet.def, config: serverConfig })) { - const transformedConfig = await validateWallet(wallet.def, serverConfig) + const transformedConfig = await validateWallet(wallet.def, serverConfig, { skipGenerated: true }) if (transformedConfig) { serverConfig = Object.assign(serverConfig, transformedConfig) } diff --git a/wallets/lnc/ATTACH.md b/wallets/lnc/ATTACH.md index 6118253ce..63daa4427 100644 --- a/wallets/lnc/ATTACH.md +++ b/wallets/lnc/ATTACH.md @@ -3,20 +3,33 @@ For testing litd as an attached receiving wallet, you'll need a pairing phrase: This can be done one of two ways: # cli - -We only need permissions for the uri `/lnrpc.Lightning/SendPaymentSync` +Create an account ```bash $ sndev cli litd accounts create --balance ``` Grab the `account.id` from the output and use it here: + +### send attachment +The sender attachment only needs permissions for the uri `/lnrpc.Lightning/SendPaymentSync` + ```bash $ sndev cli litd sessions add --type custom --label --account_id --uri /lnrpc.Lightning/SendPaymentSync ``` Grab the `pairing_secret_mnemonic` from the output and that's your pairing phrase. +### receive attachment +The receive attachment only needs permissions for the uri `/lnrpc.Lightning/AddInvoice` + + +```bash +$ sndev cli litd sessions add --type custom --label --account_id --uri /lnrpc.Lightning/AddInvoice +``` + +Grab the `pairing_secret_mnemonic` from the output and that's your pairing phrase. + # gui To open the gui, run: diff --git a/wallets/lnc/index.js b/wallets/lnc/index.js index c039678bd..b52a1215b 100644 --- a/wallets/lnc/index.js +++ b/wallets/lnc/index.js @@ -5,32 +5,37 @@ export const name = 'lnc' export const walletType = 'LNC' export const walletField = 'walletLNC' +const pairingPhraseSchema = string() + .test((value, context) => { + const words = value ? value.trim().split(/[\s]+/) : [] + for (const w of words) { + try { + string().oneOf(bip39Words).validateSync(w) + } catch { + return context.createError({ message: `'${w}' is not a valid pairing phrase word` }) + } + } + if (words.length < 2) { + return context.createError({ message: 'needs at least two words' }) + } + if (words.length > 10) { + return context.createError({ message: 'max 10 words' }) + } + return true + }) + export const fields = [ { name: 'pairingPhrase', label: 'pairing phrase', type: 'password', + optional: 'for sending', help: 'We only need permissions for the uri `/lnrpc.Lightning/SendPaymentSync`\n\nCreate a budgeted account with narrow permissions:\n\n```$ litcli accounts create --balance ```\n\n```$ litcli sessions add --type custom --label --account_id --uri /lnrpc.Lightning/SendPaymentSync```\n\nGrab the `pairing_secret_mnemonic` from the output and paste it here.', editable: false, clientOnly: true, - validate: string() - .test(async (value, context) => { - const words = value ? value.trim().split(/[\s]+/) : [] - for (const w of words) { - try { - await string().oneOf(bip39Words).validate(w) - } catch { - return context.createError({ message: `'${w}' is not a valid pairing phrase word` }) - } - } - if (words.length < 2) { - return context.createError({ message: 'needs at least two words' }) - } - if (words.length > 10) { - return context.createError({ message: 'max 10 words' }) - } - return true - }) + validate: pairingPhraseSchema, + requiredWithout: 'pairingPhraseRecv' + }, { name: 'localKey', @@ -55,6 +60,41 @@ export const fields = [ clientOnly: true, generated: true, validate: string() + }, + { + name: 'pairingPhraseRecv', + label: 'pairing phrase', + type: 'password', + optional: 'for receiving', + help: 'We only need permissions for the uri `/lnrpc.Lightning/AddInvoice`\n\nCreate an account with narrow permissions:\n\n```$ litcli accounts create```\n\n```$ litcli sessions add --type custom --label --account_id --uri /lnrpc.Lightning/AddInvoice```\n\nGrab the `pairing_secret_mnemonic` from the output and paste it here.', + editable: false, + serverOnly: true, + validate: pairingPhraseSchema, + requiredWithout: 'pairingPhrase' + }, + { + name: 'localKeyRecv', + type: 'text', + hidden: true, + serverOnly: true, + generated: true, + validate: string() + }, + { + name: 'remoteKeyRecv', + type: 'text', + hidden: true, + serverOnly: true, + generated: true, + validate: string() + }, + { + name: 'serverHostRecv', + type: 'text', + hidden: true, + serverOnly: true, + generated: true, + validate: string() } ] diff --git a/wallets/lnc/server.js b/wallets/lnc/server.js new file mode 100644 index 000000000..509f1d891 --- /dev/null +++ b/wallets/lnc/server.js @@ -0,0 +1,86 @@ +import { assertContentTypeJson, assertResponseOk } from '@/lib/url' +import { fetch } from 'cross-fetch' +import https from 'https' +export * from 'wallets/lnc' + +export async function testCreateInvoice (credentials, { signal }) { + await checkPerms(credentials, { signal }) + return await createInvoice({ msats: 1000, expiry: 1 }, credentials, { signal }) +} + +export async function createInvoice ({ msats, description, expiry }, credentials, { signal }) { + const result = await rpcCall(credentials, 'lnrpc.Lightning.AddInvoice', { memo: description, valueMsat: msats, expiry }, { signal }) + return result.payment_request +} + +async function checkPerms (credentials, { signal }) { + const enforcePerms = [ + { 'lnrpc.Lightning.SendPaymentSync': false }, + { 'lnrpc.Lightning.AddInvoice': true }, + { 'lnrpc.Wallet.SendCoins': false } + ] + + const results = await rpcCall(credentials, 'checkPerms', enforcePerms.map(perm => Object.keys(perm)[0]), { signal }) + for (let i = 0; i < enforcePerms.length; i++) { + const [key, expected] = Object.entries(enforcePerms[i])[0] + const result = results[i] + if (result !== expected) { + if (expected) { + throw new Error(`missing permission: ${key}`) + } else { + throw new Error(`too broad permission: ${key}`) + } + } + } +} + +async function rpcCall (credentials, method, payload, { signal }) { + const fetchArgs = { + method: 'POST', + signal, + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + Connection: { + Mailbox: credentials.serverHostRecv || 'mailbox.terminal.lightning.today:443', + PairingPhrase: credentials.pairingPhraseRecv, + LocalKey: credentials.localKeyRecv, + RemoteKey: credentials.remoteKeyRecv + }, + Method: method, + Payload: JSON.stringify(payload) + }) + } + + // auth + if (process.env.LNCD_AUTH_TOKEN) { + fetchArgs.headers.Authorization = `Bearer ${process.env.LNCD_AUTH_TOKEN}` + } + + // self-signed cert support + if (process.env.LNCD_URL.startsWith('https://') && process.env.LNCD_CERT) { + const cert = Buffer.from(process.env.LNCD_CERT, 'hex').toString('utf-8') + const agent = new https.Agent({ + ca: cert, + cert, + rejectUnauthorized: false + }) + fetchArgs.agent = agent + } + + let res = await fetch(process.env.LNCD_URL + '/rpc', fetchArgs) + + assertResponseOk(res) + assertContentTypeJson(res) + + res = await res.json() + + // cache auth credentials + credentials.localKeyRecv = res.Connection.LocalKey + credentials.remoteKeyRecv = res.Connection.RemoteKey + credentials.serverHostRecv = res.Connection.Mailbox + + const result = JSON.parse(res.Result) + return result +} diff --git a/wallets/server.js b/wallets/server.js index f14e9fb36..478c6fcfb 100644 --- a/wallets/server.js +++ b/wallets/server.js @@ -6,9 +6,9 @@ import * as lnbits from '@/wallets/lnbits/server' import * as nwc from '@/wallets/nwc/server' import * as phoenixd from '@/wallets/phoenixd/server' import * as blink from '@/wallets/blink/server' +import * as lnc from '@/wallets/lnc/server' // we import only the metadata of client side wallets -import * as lnc from '@/wallets/lnc' import * as webln from '@/wallets/webln' import { walletLogger } from '@/api/resolvers/wallet' diff --git a/wallets/validate.js b/wallets/validate.js index bea0cddd3..718d48bb9 100644 --- a/wallets/validate.js +++ b/wallets/validate.js @@ -72,7 +72,7 @@ function composeWalletSchema (walletDef, serverSide, skipGenerated) { if (clientOnly && serverSide) { // For server-side validation, accumulate clientOnly fields as vaultEntries - vaultEntrySchemas[optional ? 'optional' : 'required'].push(vaultEntrySchema(name)) + vaultEntrySchemas[(optional || generated) ? 'optional' : 'required'].push(vaultEntrySchema(name)) } else { acc[name] = createFieldSchema(name, validate)