feat(docker): add fund command and Nostr RPC funding script
- Add './dev.sh fund [amount]' for funding ATM via Nostr RPC - Create invoices via NIP-44 encrypted RPC instead of HTTP API - Support existing users without fail_if_exists errors - Add --fund flag to './dev.sh up' for one-command setup Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c4fadd7438
commit
e6e77f808e
2 changed files with 191 additions and 69 deletions
|
|
@ -1,17 +1,44 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Fund the ATM's Lightning.Pub account via Nostr RPC
|
||||
*
|
||||
* This creates an invoice for the ATM user (authenticated via Nostr),
|
||||
* so the funds go directly to the user that will be queried for balance.
|
||||
*
|
||||
* Usage:
|
||||
* VITE_ATM_PRIVATE_KEY=xxx VITE_LIGHTNING_PUB_PUBKEY=yyy node fund-dev.mjs [amount]
|
||||
*/
|
||||
import { NostrClient, loadIdentityFromHex, encryptContent, decryptJSON } from './dist/index.js'
|
||||
import { finalizeEvent } from 'nostr-tools'
|
||||
import { randomUUID } from 'crypto'
|
||||
|
||||
const DEV_PRIVATE_KEY = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'
|
||||
const LIGHTNING_PUB_PUBKEY =
|
||||
process.env.LIGHTNING_PUB_PUBKEY ||
|
||||
'4a72e400a254bf74a70cc711ab97e461b8d4fd9738b1ac3b5194cdeb3192ab91'
|
||||
const RELAY_URL = process.env.NOSTR_RELAY_URL || 'ws://localhost:7777'
|
||||
const FUND_AMOUNT = parseInt(process.env.FUND_AMOUNT || '100000', 10)
|
||||
const ATM_PRIVATE_KEY = process.env.VITE_ATM_PRIVATE_KEY
|
||||
const LIGHTNING_PUB_PUBKEY = process.env.VITE_LIGHTNING_PUB_PUBKEY
|
||||
const RELAY_URL = process.env.VITE_RELAY_URL || 'ws://localhost:7777'
|
||||
const APP_ID = process.env.VITE_APP_ID || ''
|
||||
const FUND_AMOUNT = parseInt(process.argv[2] || process.env.FUND_AMOUNT || '100000', 10)
|
||||
|
||||
if (!ATM_PRIVATE_KEY) {
|
||||
console.error('Error: VITE_ATM_PRIVATE_KEY environment variable required')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (!LIGHTNING_PUB_PUBKEY) {
|
||||
console.error('Error: VITE_LIGHTNING_PUB_PUBKEY environment variable required')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (!APP_ID) {
|
||||
console.error('Error: VITE_APP_ID environment variable required')
|
||||
console.error('This ensures funds go to the same user as LNURL-withdraw uses')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const identity = loadIdentityFromHex(DEV_PRIVATE_KEY)
|
||||
console.log('Using identity:', identity.publicKey)
|
||||
const identity = loadIdentityFromHex(ATM_PRIVATE_KEY)
|
||||
console.error('[fund-dev] ATM pubkey:', identity.publicKey)
|
||||
console.error('[fund-dev] Lightning.Pub pubkey:', LIGHTNING_PUB_PUBKEY)
|
||||
console.error('[fund-dev] Creating invoice for', FUND_AMOUNT, 'sats')
|
||||
|
||||
const client = new NostrClient({
|
||||
relays: [{ url: RELAY_URL }],
|
||||
|
|
@ -19,27 +46,27 @@ async function main() {
|
|||
})
|
||||
|
||||
await client.connect()
|
||||
console.log('Connected to relay')
|
||||
console.error('[fund-dev] Connected to relay:', RELAY_URL)
|
||||
|
||||
const requestId = randomUUID()
|
||||
|
||||
console.error('[fund-dev] App ID:', APP_ID)
|
||||
|
||||
// Correct RPC structure per Lightning.Pub documentation
|
||||
// appId ensures the Nostr user is created under the same app as HTTP API users
|
||||
const rpcRequest = {
|
||||
rpcName: 'NewInvoice',
|
||||
params: {},
|
||||
query: {},
|
||||
body: {
|
||||
amountSats: FUND_AMOUNT,
|
||||
memo: `Fund dev account (${FUND_AMOUNT} sats)`,
|
||||
memo: `ATM funding (${FUND_AMOUNT} sats)`,
|
||||
},
|
||||
authIdentifier: identity.publicKey,
|
||||
requestId,
|
||||
appId: APP_ID,
|
||||
}
|
||||
|
||||
console.log('Creating invoice for', FUND_AMOUNT, 'sats')
|
||||
|
||||
console.log('Request structure:', JSON.stringify(rpcRequest, null, 2))
|
||||
|
||||
const encryptedContent = encryptContent(identity, LIGHTNING_PUB_PUBKEY, rpcRequest)
|
||||
|
||||
const event = finalizeEvent(
|
||||
|
|
@ -52,63 +79,55 @@ async function main() {
|
|||
identity.privateKey
|
||||
)
|
||||
|
||||
console.log('Publishing NewInvoice request (event id:', event.id, ')...')
|
||||
|
||||
// Subscribe to responses before publishing
|
||||
let responseReceived = false
|
||||
let invoice = null
|
||||
const subId = client.subscribe(
|
||||
[
|
||||
{
|
||||
kinds: [21000],
|
||||
authors: [LIGHTNING_PUB_PUBKEY],
|
||||
'#p': [identity.publicKey],
|
||||
since: Math.floor(Date.now() / 1000) - 5,
|
||||
},
|
||||
],
|
||||
{
|
||||
onEvent: (evt) => {
|
||||
console.log('Got event from Lightning.Pub:', evt.id.substring(0, 8) + '...')
|
||||
// Check if it's for us
|
||||
const pTags = evt.tags.filter((t) => t[0] === 'p')
|
||||
const eTags = evt.tags.filter((t) => t[0] === 'e')
|
||||
const isForUs = pTags.some((t) => t[1] === identity.publicKey)
|
||||
const isReplyToOurEvent = eTags.some((t) => t[1] === event.id)
|
||||
console.log(
|
||||
' Tags p:',
|
||||
pTags.map((t) => t[1].substring(0, 8)),
|
||||
'e:',
|
||||
eTags.map((t) => t[1].substring(0, 8))
|
||||
)
|
||||
console.log(' For us:', isForUs, 'Reply to our event:', isReplyToOurEvent)
|
||||
|
||||
if (isForUs) {
|
||||
try {
|
||||
const response = decryptJSON(identity, LIGHTNING_PUB_PUBKEY, evt.content)
|
||||
console.log('Decrypted response:', JSON.stringify(response, null, 2))
|
||||
responseReceived = true
|
||||
} catch (err) {
|
||||
console.log('Failed to decrypt response:', err.message)
|
||||
try {
|
||||
const response = decryptJSON(identity, LIGHTNING_PUB_PUBKEY, evt.content)
|
||||
if (response.requestId === requestId && response.invoice) {
|
||||
invoice = response.invoice
|
||||
console.error('[fund-dev] Got invoice!')
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignore decrypt errors for other messages
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
await client.publish(event)
|
||||
console.log('Request published, waiting for response...')
|
||||
console.error('[fund-dev] Request published, waiting for invoice...')
|
||||
|
||||
// Wait up to 10 seconds for response
|
||||
for (let i = 0; i < 20; i++) {
|
||||
// Wait up to 15 seconds for response
|
||||
for (let i = 0; i < 30; i++) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
if (responseReceived) break
|
||||
}
|
||||
|
||||
if (!responseReceived) {
|
||||
console.log('No response received within timeout')
|
||||
if (invoice) break
|
||||
}
|
||||
|
||||
client.unsubscribe(subId)
|
||||
client.disconnect()
|
||||
|
||||
if (!invoice) {
|
||||
console.error('[fund-dev] Error: No invoice received within timeout')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Output just the invoice to stdout (for piping to payment command)
|
||||
console.log(invoice)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
main().catch(console.error)
|
||||
main().catch((err) => {
|
||||
console.error('[fund-dev] Error:', err.message)
|
||||
process.exit(1)
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue