mirror of
https://github.com/tompro/sattle.git
synced 2026-08-27 07:15:59 +00:00
fix: validate NWC requests before dispatch
This commit is contained in:
@@ -0,0 +1,187 @@
|
|||||||
|
// The NWC wallet service end to end: connection strings and key
|
||||||
|
// derivation, the request/response cycle over an in-memory relay (the
|
||||||
|
// transport is injected - no network), every method against the
|
||||||
|
// conformance mock mint, the legacy NIP-04 path, budget enforcement, and
|
||||||
|
// the error paths. Fund-safety focus: budgets can't be exceeded, stale
|
||||||
|
// requests never execute, and a settled preimage only ever reveals an
|
||||||
|
// already-rotated (burned) note secret.
|
||||||
|
|
||||||
|
import {afterEach, beforeEach, describe, expect, it} from 'vitest'
|
||||||
|
import {bytesToHex, hexToBytes} from '@noble/hashes/utils.js'
|
||||||
|
import {finalizeEvent, getPublicKey} from 'nostr-tools/pure'
|
||||||
|
import {encrypt as nip04Encrypt, decrypt as nip04Decrypt} from 'nostr-tools/nip04'
|
||||||
|
import {v2 as nip44v2} from 'nostr-tools/nip44'
|
||||||
|
import {buildNoteUrl, fetchNoteInfo, noteK1} from 'lnurlcash-kit'
|
||||||
|
import {createMockMint} from 'lnurlcash-conformance/mock-mint'
|
||||||
|
|
||||||
|
import {
|
||||||
|
NWC_INFO_KIND,
|
||||||
|
NWC_REQUEST_KIND,
|
||||||
|
NWC_RESPONSE_KIND,
|
||||||
|
buildConnectionString,
|
||||||
|
connectionInfoOf,
|
||||||
|
createConnection,
|
||||||
|
deriveNwcWalletKey,
|
||||||
|
migrateLegacyNwcStorage,
|
||||||
|
parseConnectionString,
|
||||||
|
readNwcEnabled,
|
||||||
|
readNwcConnections,
|
||||||
|
startService,
|
||||||
|
writeNwcEnabled,
|
||||||
|
writeNwcConnections,
|
||||||
|
} from './nwc'
|
||||||
|
import type {NostrEvent, NwcConnectionRecord, NwcServiceDeps, NwcTransport} from './nwc'
|
||||||
|
import type {NostrFilter} from './nwc/transport'
|
||||||
|
import type {NwcChangeset} from './nwc'
|
||||||
|
import type {Bearer} from './types'
|
||||||
|
import {ensureSavedKeyOwner, linkingPubKeyHex, saveLinkingKey} from './keys'
|
||||||
|
import {requiredValue, stubLocalStorage} from './test-utils'
|
||||||
|
|
||||||
|
import {
|
||||||
|
CLIENT_PUBKEY,
|
||||||
|
CLIENT_SECRET,
|
||||||
|
FAST_POLL,
|
||||||
|
LINKING_KEY,
|
||||||
|
OTHER_LINKING_KEY,
|
||||||
|
OTHER_OWNER_ID,
|
||||||
|
OWNER_ID,
|
||||||
|
RELAYS,
|
||||||
|
STRANGER_SECRET,
|
||||||
|
clientRequest,
|
||||||
|
createFakeRelay,
|
||||||
|
deferred,
|
||||||
|
foreignConnectionFixture,
|
||||||
|
methodRequest,
|
||||||
|
nowSeconds,
|
||||||
|
storeForeignConnection,
|
||||||
|
waitFor,
|
||||||
|
} from './nwc.testProtocol'
|
||||||
|
import {call, makeBearer, mint, readResponse, startTestService} from './nwc.testService'
|
||||||
|
describe('service: request validation', () => {
|
||||||
|
it('answers an unknown method with NOT_IMPLEMENTED', async () => {
|
||||||
|
const {relay, walletServicePubkey, stop} = await startTestService({})
|
||||||
|
const response = await call(relay, walletServicePubkey, 'get_payments', {})
|
||||||
|
expect(response.result_type).toBe('get_payments')
|
||||||
|
expect(response.error?.code).toBe('NOT_IMPLEMENTED')
|
||||||
|
expect(response.result).toBeNull()
|
||||||
|
await stop()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('answers a malformed (non-JSON) request with an error, not a crash', async () => {
|
||||||
|
const {relay, walletServicePubkey, stop} = await startTestService({})
|
||||||
|
const request = clientRequest(walletServicePubkey, 'this is not json')
|
||||||
|
relay.emit(request)
|
||||||
|
await waitFor(() => readResponse(relay.published, request.id, 'nip44_v2') !== null)
|
||||||
|
const response = requiredValue(readResponse(relay.published, request.id, 'nip44_v2'))
|
||||||
|
expect(response.error?.code).toBe('OTHER')
|
||||||
|
await stop()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('answers a JSON request without a method with an error', async () => {
|
||||||
|
const {relay, walletServicePubkey, stop} = await startTestService({})
|
||||||
|
const request = clientRequest(walletServicePubkey, JSON.stringify({params: {}}))
|
||||||
|
relay.emit(request)
|
||||||
|
await waitFor(() => readResponse(relay.published, request.id, 'nip44_v2') !== null)
|
||||||
|
expect(requiredValue(readResponse(relay.published, request.id, 'nip44_v2')).error?.code).toBe(
|
||||||
|
'OTHER',
|
||||||
|
)
|
||||||
|
await stop()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ignores a request signed by a stranger key - silently', async () => {
|
||||||
|
const {relay, walletServicePubkey, stop} = await startTestService({})
|
||||||
|
const content = nip44v2.encrypt(
|
||||||
|
JSON.stringify({method: 'get_balance', params: {}}),
|
||||||
|
nip44v2.utils.getConversationKey(STRANGER_SECRET, walletServicePubkey),
|
||||||
|
)
|
||||||
|
const forged = finalizeEvent(
|
||||||
|
{
|
||||||
|
kind: NWC_REQUEST_KIND,
|
||||||
|
created_at: nowSeconds(),
|
||||||
|
tags: [
|
||||||
|
['p', walletServicePubkey],
|
||||||
|
['encryption', 'nip44_v2'],
|
||||||
|
],
|
||||||
|
content,
|
||||||
|
},
|
||||||
|
STRANGER_SECRET,
|
||||||
|
)
|
||||||
|
relay.emit(forged)
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||||
|
expect(relay.published.filter((e) => e.kind === NWC_RESPONSE_KIND)).toHaveLength(0)
|
||||||
|
await stop()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('answers an unsupported encryption scheme with UNSUPPORTED_ENCRYPTION', async () => {
|
||||||
|
const {relay, walletServicePubkey, stop} = await startTestService({})
|
||||||
|
const content = nip04Encrypt(
|
||||||
|
CLIENT_SECRET,
|
||||||
|
walletServicePubkey,
|
||||||
|
JSON.stringify({method: 'get_balance', params: {}}),
|
||||||
|
)
|
||||||
|
const request = finalizeEvent(
|
||||||
|
{
|
||||||
|
kind: NWC_REQUEST_KIND,
|
||||||
|
created_at: nowSeconds(),
|
||||||
|
tags: [
|
||||||
|
['p', walletServicePubkey],
|
||||||
|
['encryption', 'nip17'],
|
||||||
|
],
|
||||||
|
content,
|
||||||
|
},
|
||||||
|
CLIENT_SECRET,
|
||||||
|
)
|
||||||
|
relay.emit(request)
|
||||||
|
// the error answer goes out in the legacy scheme every client reads
|
||||||
|
await waitFor(() => readResponse(relay.published, request.id, 'nip04') !== null)
|
||||||
|
expect(requiredValue(readResponse(relay.published, request.id, 'nip04')).error?.code).toBe(
|
||||||
|
'UNSUPPORTED_ENCRYPTION',
|
||||||
|
)
|
||||||
|
await stop()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('speaks legacy NIP-04: no encryption tag, and an explicit nip04 tag', async () => {
|
||||||
|
const {relay, walletServicePubkey, stop} = await startTestService({})
|
||||||
|
for (const scheme of ['none', 'nip04'] as const) {
|
||||||
|
const request = methodRequest(walletServicePubkey, 'get_balance', {}, scheme)
|
||||||
|
relay.emit(request)
|
||||||
|
await waitFor(() => readResponse(relay.published, request.id, 'nip04') !== null)
|
||||||
|
const response = requiredValue(readResponse(relay.published, request.id, 'nip04'))
|
||||||
|
expect(response.error).toBeNull()
|
||||||
|
expect(response.result).toEqual({balance: 0})
|
||||||
|
// the response mirrors the request's scheme
|
||||||
|
const event = requiredValue(
|
||||||
|
relay.published.find(
|
||||||
|
(e) =>
|
||||||
|
e.kind === NWC_RESPONSE_KIND && e.tags.some((t) => t[0] === 'e' && t[1] === request.id),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
expect(event.tags).toContainEqual(['encryption', 'nip04'])
|
||||||
|
}
|
||||||
|
await stop()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('drops requests older than the replay window unanswered', async () => {
|
||||||
|
const {relay, walletServicePubkey, stop} = await startTestService({})
|
||||||
|
const stale = methodRequest(
|
||||||
|
walletServicePubkey,
|
||||||
|
'get_balance',
|
||||||
|
{},
|
||||||
|
'nip44_v2',
|
||||||
|
nowSeconds() - 1200,
|
||||||
|
)
|
||||||
|
relay.emit(stale)
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||||
|
expect(relay.published.filter((e) => e.kind === NWC_RESPONSE_KIND)).toHaveLength(0)
|
||||||
|
await stop()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('picks up no new requests after stop', async () => {
|
||||||
|
const {relay, walletServicePubkey, stop} = await startTestService({})
|
||||||
|
await stop()
|
||||||
|
const request = methodRequest(walletServicePubkey, 'get_balance', {})
|
||||||
|
relay.emitAfterClose(request)
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||||
|
expect(relay.published.filter((e) => e.kind === NWC_RESPONSE_KIND)).toHaveLength(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -9,6 +9,7 @@ import {noteK1, sameInvoice} from 'lnurlcash-kit'
|
|||||||
import type {Bearer} from '../types'
|
import type {Bearer} from '../types'
|
||||||
import type {PreparedMint} from '../ops'
|
import type {PreparedMint} from '../ops'
|
||||||
import {prepareMint} from '../ops'
|
import {prepareMint} from '../ops'
|
||||||
|
import {PollAbortedError} from '../ops/shared'
|
||||||
|
|
||||||
import type {PendingInvoice, RequestContext} from './context'
|
import type {PendingInvoice, RequestContext} from './context'
|
||||||
import {invoiceResult, resolvePaymentHash, settleAndClaim} from './invoices'
|
import {invoiceResult, resolvePaymentHash, settleAndClaim} from './invoices'
|
||||||
@@ -19,10 +20,7 @@ import {NWC_METHODS, errResult, okResult} from './protocol'
|
|||||||
// the same eligibility carve applies - the balance answers "what could
|
// the same eligibility carve applies - the balance answers "what could
|
||||||
// this wallet actually pay with right now"
|
// this wallet actually pay with right now"
|
||||||
const spendable = (bearer: Bearer): boolean =>
|
const spendable = (bearer: Bearer): boolean =>
|
||||||
!bearer.spent &&
|
!bearer.spent && bearer.callback !== '' && !bearer.deviceId && !!noteK1(bearer.url)
|
||||||
bearer.callback !== '' &&
|
|
||||||
!bearer.deviceId &&
|
|
||||||
!!noteK1(bearer.url)
|
|
||||||
|
|
||||||
const handleGetInfo = (ctx: RequestContext): NwcResponse =>
|
const handleGetInfo = (ctx: RequestContext): NwcResponse =>
|
||||||
okResult('get_info', {
|
okResult('get_info', {
|
||||||
@@ -34,7 +32,7 @@ const handleGetInfo = (ctx: RequestContext): NwcResponse =>
|
|||||||
// block height/hash exists, so those fields are simply absent.
|
// block height/hash exists, so those fields are simply absent.
|
||||||
network: 'mainnet',
|
network: 'mainnet',
|
||||||
methods: [...NWC_METHODS],
|
methods: [...NWC_METHODS],
|
||||||
notifications: []
|
notifications: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleGetBalance = (ctx: RequestContext): NwcResponse =>
|
const handleGetBalance = (ctx: RequestContext): NwcResponse =>
|
||||||
@@ -42,20 +40,16 @@ const handleGetBalance = (ctx: RequestContext): NwcResponse =>
|
|||||||
balance: ctx.deps
|
balance: ctx.deps
|
||||||
.getBearers()
|
.getBearers()
|
||||||
.filter(spendable)
|
.filter(spendable)
|
||||||
.reduce((sum, b) => sum + b.amount, 0)
|
.reduce((sum, b) => sum + b.amount, 0),
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleMakeInvoice = async (
|
const handleMakeInvoice = async (
|
||||||
ctx: RequestContext,
|
ctx: RequestContext,
|
||||||
params: Record<string, unknown>
|
params: Record<string, unknown>,
|
||||||
): Promise<NwcResponse> => {
|
): Promise<NwcResponse> => {
|
||||||
const amountMsat = Number(params.amount)
|
const amountMsat = Number(params.amount)
|
||||||
if (!Number.isInteger(amountMsat) || amountMsat <= 0) {
|
if (!Number.isInteger(amountMsat) || amountMsat <= 0) {
|
||||||
return errResult(
|
return errResult('make_invoice', 'OTHER', 'Amount must be a positive whole number of msat.')
|
||||||
'make_invoice',
|
|
||||||
'OTHER',
|
|
||||||
'Amount must be a positive whole number of msat.'
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
const mint = ctx.deps.getDefaultMint()
|
const mint = ctx.deps.getDefaultMint()
|
||||||
if (!mint) {
|
if (!mint) {
|
||||||
@@ -65,11 +59,7 @@ const handleMakeInvoice = async (
|
|||||||
try {
|
try {
|
||||||
prepared = await prepareMint(mint, amountMsat, ctx.deps.kit ?? {})
|
prepared = await prepareMint(mint, amountMsat, ctx.deps.kit ?? {})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return errResult(
|
return errResult('make_invoice', 'INTERNAL', err instanceof Error ? err.message : String(err))
|
||||||
'make_invoice',
|
|
||||||
'INTERNAL',
|
|
||||||
err instanceof Error ? err.message : String(err)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
const entry: PendingInvoice = {
|
const entry: PendingInvoice = {
|
||||||
invoice: prepared.invoice,
|
invoice: prepared.invoice,
|
||||||
@@ -77,7 +67,7 @@ const handleMakeInvoice = async (
|
|||||||
amountMsat: prepared.grossMsat,
|
amountMsat: prepared.grossMsat,
|
||||||
createdAt: ctx.nowSeconds(),
|
createdAt: ctx.nowSeconds(),
|
||||||
prepared,
|
prepared,
|
||||||
state: 'pending'
|
state: 'pending',
|
||||||
}
|
}
|
||||||
if (typeof params.description === 'string' && params.description) {
|
if (typeof params.description === 'string' && params.description) {
|
||||||
entry.description = params.description
|
entry.description = params.description
|
||||||
@@ -88,30 +78,29 @@ const handleMakeInvoice = async (
|
|||||||
}
|
}
|
||||||
ctx.invoices.set(entry.paymentHash, entry)
|
ctx.invoices.set(entry.paymentHash, entry)
|
||||||
// phase two runs in the background; the invoice goes out now and
|
// phase two runs in the background; the invoice goes out now and
|
||||||
// lookup_invoice reports the settlement the claim observes
|
// lookup_invoice reports the settlement the service-owned claim observes
|
||||||
void settleAndClaim(ctx, entry).catch(err => {
|
const started = ctx.startBackground(() =>
|
||||||
|
settleAndClaim(ctx, entry).catch((err) => {
|
||||||
entry.state = 'failed'
|
entry.state = 'failed'
|
||||||
|
// an interrupted claim poll is normal service teardown (stop
|
||||||
|
// aborted it), not a background failure worth surfacing
|
||||||
|
if (err instanceof PollAbortedError) return
|
||||||
ctx.deps.onError?.(err, ctx.connection())
|
ctx.deps.onError?.(err, ctx.connection())
|
||||||
})
|
}),
|
||||||
|
)
|
||||||
|
if (!started) {
|
||||||
|
entry.state = 'failed'
|
||||||
|
return errResult('make_invoice', 'INTERNAL', 'The wallet service is stopping.')
|
||||||
|
}
|
||||||
return okResult('make_invoice', invoiceResult(entry))
|
return okResult('make_invoice', invoiceResult(entry))
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleLookupInvoice = (
|
const handleLookupInvoice = (ctx: RequestContext, params: Record<string, unknown>): NwcResponse => {
|
||||||
ctx: RequestContext,
|
const invoiceParam = typeof params.invoice === 'string' ? params.invoice : undefined
|
||||||
params: Record<string, unknown>
|
|
||||||
): NwcResponse => {
|
|
||||||
const invoiceParam =
|
|
||||||
typeof params.invoice === 'string' ? params.invoice : undefined
|
|
||||||
const hashParam =
|
const hashParam =
|
||||||
typeof params.payment_hash === 'string'
|
typeof params.payment_hash === 'string' ? params.payment_hash.toLowerCase() : undefined
|
||||||
? params.payment_hash.toLowerCase()
|
|
||||||
: undefined
|
|
||||||
if (!invoiceParam && !hashParam) {
|
if (!invoiceParam && !hashParam) {
|
||||||
return errResult(
|
return errResult('lookup_invoice', 'OTHER', 'Provide an invoice or a payment hash.')
|
||||||
'lookup_invoice',
|
|
||||||
'OTHER',
|
|
||||||
'Provide an invoice or a payment hash.'
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
let entry = hashParam ? ctx.invoices.get(hashParam) : undefined
|
let entry = hashParam ? ctx.invoices.get(hashParam) : undefined
|
||||||
if (!entry && invoiceParam) {
|
if (!entry && invoiceParam) {
|
||||||
@@ -128,10 +117,7 @@ const handleLookupInvoice = (
|
|||||||
return okResult('lookup_invoice', invoiceResult(entry))
|
return okResult('lookup_invoice', invoiceResult(entry))
|
||||||
}
|
}
|
||||||
|
|
||||||
export const dispatch = async (
|
export const dispatch = async (ctx: RequestContext, request: NwcRequest): Promise<NwcResponse> => {
|
||||||
ctx: RequestContext,
|
|
||||||
request: NwcRequest
|
|
||||||
): Promise<NwcResponse> => {
|
|
||||||
switch (request.method) {
|
switch (request.method) {
|
||||||
case 'get_info':
|
case 'get_info':
|
||||||
return handleGetInfo(ctx)
|
return handleGetInfo(ctx)
|
||||||
@@ -144,10 +130,6 @@ export const dispatch = async (
|
|||||||
case 'lookup_invoice':
|
case 'lookup_invoice':
|
||||||
return handleLookupInvoice(ctx, request.params)
|
return handleLookupInvoice(ctx, request.params)
|
||||||
default:
|
default:
|
||||||
return errResult(
|
return errResult(request.method, 'NOT_IMPLEMENTED', `Unknown method: ${request.method}.`)
|
||||||
request.method,
|
|
||||||
'NOT_IMPLEMENTED',
|
|
||||||
`Unknown method: ${request.method}.`
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export const NWC_METHODS = [
|
|||||||
'get_balance',
|
'get_balance',
|
||||||
'make_invoice',
|
'make_invoice',
|
||||||
'pay_invoice',
|
'pay_invoice',
|
||||||
'lookup_invoice'
|
'lookup_invoice',
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
export type NwcMethod = (typeof NWC_METHODS)[number]
|
export type NwcMethod = (typeof NWC_METHODS)[number]
|
||||||
@@ -61,17 +61,13 @@ export type NwcResponse = {
|
|||||||
export const okResult = (method: string, result: unknown): NwcResponse => ({
|
export const okResult = (method: string, result: unknown): NwcResponse => ({
|
||||||
result_type: method,
|
result_type: method,
|
||||||
error: null,
|
error: null,
|
||||||
result
|
result,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const errResult = (
|
export const errResult = (method: string, code: NwcErrorCode, message: string): NwcResponse => ({
|
||||||
method: string,
|
|
||||||
code: NwcErrorCode,
|
|
||||||
message: string
|
|
||||||
): NwcResponse => ({
|
|
||||||
result_type: method,
|
result_type: method,
|
||||||
error: {code, message},
|
error: {code, message},
|
||||||
result: null
|
result: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
// the two encryption schemes this service speaks; the scheme of a request
|
// the two encryption schemes this service speaks; the scheme of a request
|
||||||
@@ -79,16 +75,14 @@ export const errResult = (
|
|||||||
// requested by the client")
|
// requested by the client")
|
||||||
export type NwcEncryption = 'nip44_v2' | 'nip04'
|
export type NwcEncryption = 'nip44_v2' | 'nip04'
|
||||||
|
|
||||||
const conversationKey = (
|
const conversationKey = (walletSecretKey: Uint8Array, clientPubkey: string): Uint8Array =>
|
||||||
walletSecretKey: Uint8Array,
|
nip44v2.utils.getConversationKey(walletSecretKey, clientPubkey)
|
||||||
clientPubkey: string
|
|
||||||
): Uint8Array => nip44v2.utils.getConversationKey(walletSecretKey, clientPubkey)
|
|
||||||
|
|
||||||
export const encryptFor = (
|
export const encryptFor = (
|
||||||
scheme: NwcEncryption,
|
scheme: NwcEncryption,
|
||||||
walletSecretKey: Uint8Array,
|
walletSecretKey: Uint8Array,
|
||||||
clientPubkey: string,
|
clientPubkey: string,
|
||||||
plaintext: string
|
plaintext: string,
|
||||||
): string =>
|
): string =>
|
||||||
scheme === 'nip44_v2'
|
scheme === 'nip44_v2'
|
||||||
? nip44v2.encrypt(plaintext, conversationKey(walletSecretKey, clientPubkey))
|
? nip44v2.encrypt(plaintext, conversationKey(walletSecretKey, clientPubkey))
|
||||||
@@ -98,14 +92,14 @@ const decryptFrom = (
|
|||||||
scheme: NwcEncryption,
|
scheme: NwcEncryption,
|
||||||
walletSecretKey: Uint8Array,
|
walletSecretKey: Uint8Array,
|
||||||
clientPubkey: string,
|
clientPubkey: string,
|
||||||
content: string
|
content: string,
|
||||||
): string =>
|
): string =>
|
||||||
scheme === 'nip44_v2'
|
scheme === 'nip44_v2'
|
||||||
? nip44v2.decrypt(content, conversationKey(walletSecretKey, clientPubkey))
|
? nip44v2.decrypt(content, conversationKey(walletSecretKey, clientPubkey))
|
||||||
: nip04Decrypt(walletSecretKey, clientPubkey, content)
|
: nip04Decrypt(walletSecretKey, clientPubkey, content)
|
||||||
|
|
||||||
const tagValue = (event: NostrEvent, name: string): string | undefined =>
|
const tagValue = (event: NostrEvent, name: string): string | undefined =>
|
||||||
event.tags.find(t => t[0] === name)?.[1]
|
event.tags.find((t) => t[0] === name)?.[1]
|
||||||
|
|
||||||
// The outcome of validating + decrypting a candidate request event:
|
// The outcome of validating + decrypting a candidate request event:
|
||||||
// - a request to dispatch (encryption scheme carried so the response can
|
// - a request to dispatch (encryption scheme carried so the response can
|
||||||
@@ -126,7 +120,7 @@ export const decryptRequest = (
|
|||||||
walletServicePubkey: string,
|
walletServicePubkey: string,
|
||||||
clientPubkey: string,
|
clientPubkey: string,
|
||||||
event: NostrEvent,
|
event: NostrEvent,
|
||||||
nowSeconds: number = Math.floor(Date.now() / 1000)
|
nowSeconds: number = Math.floor(Date.now() / 1000),
|
||||||
): DecryptedNwcRequest | null => {
|
): DecryptedNwcRequest | null => {
|
||||||
if (event.kind !== NWC_REQUEST_KIND) return null
|
if (event.kind !== NWC_REQUEST_KIND) return null
|
||||||
// only the authorized client may talk to this connection, and the
|
// only the authorized client may talk to this connection, and the
|
||||||
@@ -155,8 +149,8 @@ export const decryptRequest = (
|
|||||||
response: errResult(
|
response: errResult(
|
||||||
'',
|
'',
|
||||||
'UNSUPPORTED_ENCRYPTION',
|
'UNSUPPORTED_ENCRYPTION',
|
||||||
`Unsupported encryption scheme: ${advertised}.`
|
`Unsupported encryption scheme: ${advertised}.`,
|
||||||
)
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let plaintext: string
|
let plaintext: string
|
||||||
@@ -173,7 +167,7 @@ export const decryptRequest = (
|
|||||||
return {
|
return {
|
||||||
respond: true,
|
respond: true,
|
||||||
encryption,
|
encryption,
|
||||||
response: errResult('', 'OTHER', 'The request is not valid JSON.')
|
response: errResult('', 'OTHER', 'The request is not valid JSON.'),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
@@ -185,14 +179,11 @@ export const decryptRequest = (
|
|||||||
return {
|
return {
|
||||||
respond: true,
|
respond: true,
|
||||||
encryption,
|
encryption,
|
||||||
response: errResult('', 'OTHER', 'The request has no method.')
|
response: errResult('', 'OTHER', 'The request has no method.'),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const request = data as NwcRequest
|
const request = data as NwcRequest
|
||||||
const params =
|
const params = typeof request.params === 'object' && request.params !== null ? request.params : {}
|
||||||
typeof request.params === 'object' && request.params !== null
|
|
||||||
? request.params
|
|
||||||
: {}
|
|
||||||
return {respond: false, request: {method: request.method, params}, encryption}
|
return {respond: false, request: {method: request.method, params}, encryption}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,7 +195,7 @@ export const buildResponseEvent = (
|
|||||||
encryption: NwcEncryption,
|
encryption: NwcEncryption,
|
||||||
requestEventId: string,
|
requestEventId: string,
|
||||||
response: NwcResponse,
|
response: NwcResponse,
|
||||||
createdAt: number = Math.floor(Date.now() / 1000)
|
createdAt: number = Math.floor(Date.now() / 1000),
|
||||||
): NostrEvent =>
|
): NostrEvent =>
|
||||||
finalizeEvent(
|
finalizeEvent(
|
||||||
{
|
{
|
||||||
@@ -213,29 +204,24 @@ export const buildResponseEvent = (
|
|||||||
tags: [
|
tags: [
|
||||||
['p', clientPubkey],
|
['p', clientPubkey],
|
||||||
['e', requestEventId],
|
['e', requestEventId],
|
||||||
['encryption', encryption]
|
['encryption', encryption],
|
||||||
],
|
],
|
||||||
content: encryptFor(
|
content: encryptFor(encryption, walletSecretKey, clientPubkey, JSON.stringify(response)),
|
||||||
encryption,
|
|
||||||
walletSecretKey,
|
|
||||||
clientPubkey,
|
|
||||||
JSON.stringify(response)
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
walletSecretKey
|
walletSecretKey,
|
||||||
)
|
)
|
||||||
|
|
||||||
// the replaceable info event advertising this service's capabilities
|
// the replaceable info event advertising this service's capabilities
|
||||||
export const buildInfoEvent = (
|
export const buildInfoEvent = (
|
||||||
walletSecretKey: Uint8Array,
|
walletSecretKey: Uint8Array,
|
||||||
createdAt: number = Math.floor(Date.now() / 1000)
|
createdAt: number = Math.floor(Date.now() / 1000),
|
||||||
): NostrEvent =>
|
): NostrEvent =>
|
||||||
finalizeEvent(
|
finalizeEvent(
|
||||||
{
|
{
|
||||||
kind: NWC_INFO_KIND,
|
kind: NWC_INFO_KIND,
|
||||||
created_at: createdAt,
|
created_at: createdAt,
|
||||||
tags: [['encryption', 'nip44_v2 nip04']],
|
tags: [['encryption', 'nip44_v2 nip04']],
|
||||||
content: NWC_METHODS.join(' ')
|
content: NWC_METHODS.join(' '),
|
||||||
},
|
},
|
||||||
walletSecretKey
|
walletSecretKey,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user