Skip to content

Commit a897745

Browse files
committed
lnc receiver
1 parent 1a2176e commit a897745

File tree

10 files changed

+223
-22
lines changed

10 files changed

+223
-22
lines changed

.env.development

+4-1
Original file line numberDiff line numberDiff line change
@@ -183,4 +183,7 @@ CPU_SHARES_IMPORTANT=1024
183183
CPU_SHARES_MODERATE=512
184184
CPU_SHARES_LOW=256
185185

186-
NEXT_TELEMETRY_DISABLED=1
186+
NEXT_TELEMETRY_DISABLED=1
187+
188+
# LNCD
189+
LNCD_URL=http://lncd:7167

docker-compose.yml

+18
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,24 @@ services:
513513
CLI: "litcli"
514514
CLI_ARGS: "-n regtest --rpcserver localhost:8444"
515515
cpu_shares: "${CPU_SHARES_MODERATE}"
516+
lncd:
517+
container_name: lncd
518+
build:
519+
context: ./docker/lncd
520+
profiles:
521+
- wallets
522+
restart: unless-stopped
523+
environment:
524+
- LNCD_DEBUG=true
525+
- LNCD_DEV_UNSAFE_LOG=true
526+
healthcheck:
527+
<<: *healthcheck
528+
test: ["CMD", "curl", "-f", "http://localhost:7167/health"]
529+
depends_on:
530+
litd:
531+
condition: service_healthy
532+
restart: true
533+
cpu_shares: "${CPU_SHARES_MODERATE}"
516534
cln:
517535
build:
518536
context: ./docker/cln

docker/lncd/Dockerfile

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
FROM debian:bookworm-slim
2+
RUN useradd -u 1000 -m lncd
3+
4+
ARG VERSION=0.2.2
5+
ARG REPO=riccardobl/lncd
6+
ARG DOWNLOAD_URL=https://github.com/$REPO/releases/download/$VERSION/lncd
7+
8+
RUN mkdir -p /home/lncd && \
9+
chown 1000:1000 -Rvf /home/lncd/ &&\
10+
apt-get update && apt-get install -y curl &&\
11+
apt-get clean && rm -rf /var/lib/apt/lists/*
12+
13+
USER lncd
14+
RUN curl --proto '=https' --tlsv1.2 -LsSf $DOWNLOAD_URL -o /home/lncd/lncd && chmod +x /home/lncd/lncd
15+
WORKDIR /home/lncd
16+
EXPOSE 7167
17+
18+
CMD ["./lncd"]

fragments/wallet.js

+6
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,12 @@ export const WALLET_FIELDS = gql`
169169
apiKeyRecv
170170
currencyRecv
171171
}
172+
... on WalletLnc {
173+
pairingPhraseRecv
174+
localKeyRecv
175+
remoteKeyRecv
176+
serverHostRecv
177+
}
172178
}
173179
}
174180
`
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
-- CreateTable
2+
CREATE TABLE "WalletLNC" (
3+
"id" SERIAL NOT NULL,
4+
"walletId" INTEGER NOT NULL,
5+
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
6+
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
7+
"pairingPhraseRecv" TEXT NOT NULL,
8+
"localKeyRecv" TEXT,
9+
"remoteKeyRecv" TEXT,
10+
"serverHostRecv" TEXT,
11+
CONSTRAINT "WalletLNC_pkey" PRIMARY KEY ("id")
12+
);
13+
14+
-- CreateIndex
15+
CREATE UNIQUE INDEX "WalletLNC_walletId_key" ON "WalletLNC"("walletId");
16+
17+
-- AddForeignKey
18+
ALTER TABLE "WalletLNC" ADD CONSTRAINT "WalletLNC_walletId_fkey" FOREIGN KEY ("walletId") REFERENCES "Wallet"("id") ON DELETE CASCADE ON UPDATE CASCADE;
19+
20+
CREATE TRIGGER wallet_lnc_as_jsonb
21+
AFTER INSERT OR UPDATE ON "WalletLNC"
22+
FOR EACH ROW EXECUTE PROCEDURE wallet_wallet_type_as_jsonb();

prisma/schema.prisma

+13
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ model Wallet {
216216
walletNWC WalletNWC?
217217
walletPhoenixd WalletPhoenixd?
218218
walletBlink WalletBlink?
219+
walletLNC WalletLNC?
219220
220221
vaultEntries VaultEntry[] @relation("VaultEntries")
221222
withdrawals Withdrawl[]
@@ -315,6 +316,18 @@ model WalletBlink {
315316
currencyRecv String?
316317
}
317318

319+
model WalletLNC {
320+
id Int @id @default(autoincrement())
321+
walletId Int @unique
322+
wallet Wallet @relation(fields: [walletId], references: [id], onDelete: Cascade)
323+
createdAt DateTime @default(now()) @map("created_at")
324+
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
325+
pairingPhraseRecv String
326+
localKeyRecv String?
327+
remoteKeyRecv String?
328+
serverHostRecv String?
329+
}
330+
318331
model WalletPhoenixd {
319332
id Int @id @default(autoincrement())
320333
walletId Int @unique

wallets/lnc/ATTACH.md

+15-2
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,33 @@ For testing litd as an attached receiving wallet, you'll need a pairing phrase:
33
This can be done one of two ways:
44

55
# cli
6-
7-
We only need permissions for the uri `/lnrpc.Lightning/SendPaymentSync`
6+
Create an account
87

98
```bash
109
$ sndev cli litd accounts create --balance <budget>
1110
```
1211

1312
Grab the `account.id` from the output and use it here:
13+
14+
### send attachment
15+
The sender attachment only needs permissions for the uri `/lnrpc.Lightning/SendPaymentSync`
16+
1417
```bash
1518
$ sndev cli litd sessions add --type custom --label <your label> --account_id <account_id> --uri /lnrpc.Lightning/SendPaymentSync
1619
```
1720

1821
Grab the `pairing_secret_mnemonic` from the output and that's your pairing phrase.
1922

23+
### receive attachment
24+
The receive attachment only needs permissions for the uri `/lnrpc.Lightning/AddInvoice`
25+
26+
27+
```bash
28+
$ sndev cli litd sessions add --type custom --label <your label> --account_id <account_id> --uri /lnrpc.Lightning/AddInvoice
29+
```
30+
31+
Grab the `pairing_secret_mnemonic` from the output and that's your pairing phrase.
32+
2033
# gui
2134

2235
To open the gui, run:

wallets/lnc/index.js

+58-18
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,37 @@ export const name = 'lnc'
55
export const walletType = 'LNC'
66
export const walletField = 'walletLNC'
77

8+
const pairingPhraseSchema = string()
9+
.test((value, context) => {
10+
const words = value ? value.trim().split(/[\s]+/) : []
11+
for (const w of words) {
12+
try {
13+
string().oneOf(bip39Words).validateSync(w)
14+
} catch {
15+
return context.createError({ message: `'${w}' is not a valid pairing phrase word` })
16+
}
17+
}
18+
if (words.length < 2) {
19+
return context.createError({ message: 'needs at least two words' })
20+
}
21+
if (words.length > 10) {
22+
return context.createError({ message: 'max 10 words' })
23+
}
24+
return true
25+
})
26+
827
export const fields = [
928
{
1029
name: 'pairingPhrase',
1130
label: 'pairing phrase',
1231
type: 'password',
32+
optional: 'for sending',
1333
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 <budget>```\n\n```$ litcli sessions add --type custom --label <your label> --account_id <account_id> --uri /lnrpc.Lightning/SendPaymentSync```\n\nGrab the `pairing_secret_mnemonic` from the output and paste it here.',
1434
editable: false,
1535
clientOnly: true,
16-
validate: string()
17-
.test(async (value, context) => {
18-
const words = value ? value.trim().split(/[\s]+/) : []
19-
for (const w of words) {
20-
try {
21-
await string().oneOf(bip39Words).validate(w)
22-
} catch {
23-
return context.createError({ message: `'${w}' is not a valid pairing phrase word` })
24-
}
25-
}
26-
if (words.length < 2) {
27-
return context.createError({ message: 'needs at least two words' })
28-
}
29-
if (words.length > 10) {
30-
return context.createError({ message: 'max 10 words' })
31-
}
32-
return true
33-
})
36+
validate: pairingPhraseSchema,
37+
requiredWithout: 'pairingPhraseRecv'
38+
3439
},
3540
{
3641
name: 'localKey',
@@ -55,6 +60,41 @@ export const fields = [
5560
clientOnly: true,
5661
generated: true,
5762
validate: string()
63+
},
64+
{
65+
name: 'pairingPhraseRecv',
66+
label: 'pairing phrase',
67+
type: 'password',
68+
optional: 'for receiving',
69+
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 <your label> --account_id <account_id> --uri /lnrpc.Lightning/AddInvoice```\n\nGrab the `pairing_secret_mnemonic` from the output and paste it here.',
70+
editable: false,
71+
serverOnly: true,
72+
validate: pairingPhraseSchema,
73+
requiredWithout: 'pairingPhrase'
74+
},
75+
{
76+
name: 'localKeyRecv',
77+
type: 'text',
78+
hidden: true,
79+
serverOnly: true,
80+
generated: true,
81+
validate: string()
82+
},
83+
{
84+
name: 'remoteKeyRecv',
85+
type: 'text',
86+
hidden: true,
87+
serverOnly: true,
88+
generated: true,
89+
validate: string()
90+
},
91+
{
92+
name: 'serverHostRecv',
93+
type: 'text',
94+
hidden: true,
95+
serverOnly: true,
96+
generated: true,
97+
validate: string()
5898
}
5999
]
60100

wallets/lnc/server.js

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { assertContentTypeJson, assertResponseOk } from '@/lib/url'
2+
export * from 'wallets/lnc'
3+
4+
export async function testCreateInvoice (credentials, { signal }) {
5+
await checkPerms(credentials, { signal })
6+
return await createInvoice({ msats: 1000, expiry: 1 }, credentials, { signal })
7+
}
8+
9+
export async function createInvoice ({ msats, description, expiry }, credentials, { signal }) {
10+
const result = await rpcCall(credentials, 'lnrpc.Lightning.AddInvoice', { memo: description, valueMsat: msats, expiry }, { signal })
11+
return result.payment_request
12+
}
13+
14+
async function checkPerms (credentials, { signal }) {
15+
const enforcePerms = [
16+
{ 'lnrpc.Lightning.SendPaymentSync': false },
17+
{ 'lnrpc.Lightning.AddInvoice': true },
18+
{ 'lnrpc.Wallet.SendCoins': false }
19+
]
20+
21+
const results = await rpcCall(credentials, 'checkPerms', enforcePerms.map(perm => Object.keys(perm)[0]), { signal })
22+
for (let i = 0; i < enforcePerms.length; i++) {
23+
const [key, expected] = Object.entries(enforcePerms[i])[0]
24+
const result = results[i]
25+
if (result !== expected) {
26+
if (expected) {
27+
throw new Error(`missing permission: ${key}`)
28+
} else {
29+
throw new Error(`too broad permission: ${key}`)
30+
}
31+
}
32+
}
33+
}
34+
35+
async function rpcCall (credentials, method, payload, { signal }) {
36+
const body = {
37+
Connection: {
38+
Mailbox: credentials.serverHostRecv || 'mailbox.terminal.lightning.today:443',
39+
PairingPhrase: credentials.pairingPhraseRecv,
40+
LocalKey: credentials.localKeyRecv,
41+
RemoteKey: credentials.remoteKeyRecv
42+
},
43+
Method: method,
44+
Payload: JSON.stringify(payload)
45+
}
46+
47+
let res = await fetch(process.env.LNCD_URL + '/rpc', {
48+
method: 'POST',
49+
signal,
50+
headers: {
51+
'Content-Type': 'application/json'
52+
},
53+
body: JSON.stringify(body)
54+
})
55+
56+
assertResponseOk(res)
57+
assertContentTypeJson(res)
58+
59+
res = await res.json()
60+
61+
// cache auth credentials
62+
credentials.localKeyRecv = res.Connection.LocalKey
63+
credentials.remoteKeyRecv = res.Connection.RemoteKey
64+
credentials.serverHostRecv = res.Connection.Mailbox
65+
66+
const result = JSON.parse(res.Result)
67+
return result
68+
}

wallets/server.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import * as lnbits from '@/wallets/lnbits/server'
66
import * as nwc from '@/wallets/nwc/server'
77
import * as phoenixd from '@/wallets/phoenixd/server'
88
import * as blink from '@/wallets/blink/server'
9+
import * as lnc from '@/wallets/lnc/server'
910

1011
// we import only the metadata of client side wallets
11-
import * as lnc from '@/wallets/lnc'
1212
import * as webln from '@/wallets/webln'
1313

1414
import { walletLogger } from '@/api/resolvers/wallet'

0 commit comments

Comments
 (0)