mirror of
https://github.com/tompro/sattle.git
synced 2026-08-27 07:15:59 +00:00
build: pin LNURLcash protocol packages to npm 0.1.1
This commit is contained in:
+27
-12
@@ -40,8 +40,10 @@ interface MockNoteInfoOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface MockTargetMintOptions {
|
interface MockTargetMintOptions {
|
||||||
// the mint's signing pubkey (66 hex chars), advertised in the payRequest
|
// the mint's signing pubkey (66 hex chars), advertised on the mint-address
|
||||||
// and note responses
|
// (/.well-known/lnurlw/) and note-info responses - NEVER the payRequest:
|
||||||
|
// LUD-25 announces it on the withdraw side only, and a too-generous mock
|
||||||
|
// once masked a real bug
|
||||||
mintPubkey: string;
|
mintPubkey: string;
|
||||||
// the invoice this mint hands out (and reports back as settled) - keep it
|
// the invoice this mint hands out (and reports back as settled) - keep it
|
||||||
// amount-less (decodeBolt11AmountMsat returns null) so the kit skips the
|
// amount-less (decodeBolt11AmountMsat returns null) so the kit skips the
|
||||||
@@ -97,24 +99,38 @@ export class MintMocker {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// A mint that never answers its mint-address discovery endpoint with
|
// The mint-address discovery endpoint (LUD-25): announces the signing key
|
||||||
// anything usable - prepareMint treats that as "no mint-address support"
|
// and the node stats a real lnurl-mint advertises. `nodeCapacity` is msat
|
||||||
// and falls back to the plain LNURL-pay guess.
|
// under its WIRE name (no suffix) - the kit has to map it onto
|
||||||
private async mockNoMintAddress(origin: string): Promise<void> {
|
// nodeCapacityMsat, which is exactly the 0.1.0 spread bug this exercises.
|
||||||
|
// The payLink points back at the lnurlp route below, as prepareMint treats
|
||||||
|
// it as the authoritative place to read the payRequest from.
|
||||||
|
private async mockMintAddress(options: MockTargetMintOptions, origin: string): Promise<void> {
|
||||||
await this.page.route(
|
await this.page.route(
|
||||||
new RegExp(`^${escapeRegExp(`${origin}/.well-known/lnurlw/`)}`),
|
new RegExp(`^${escapeRegExp(`${origin}/.well-known/lnurlw/`)}`),
|
||||||
async (route: Route) => {
|
async (route: Route) => {
|
||||||
await fulfillJson(route, { status: 'ERROR', reason: 'not supported' });
|
await fulfillJson(route, {
|
||||||
|
tag: 'withdrawRequest',
|
||||||
|
callback: `${origin}${CALLBACK_PATH}`,
|
||||||
|
minWithdrawable: 1000,
|
||||||
|
maxWithdrawable: 100_000_000_000,
|
||||||
|
mintPubkey: options.mintPubkey,
|
||||||
|
payLink: `${origin}/.well-known/lnurlp/mint`,
|
||||||
|
nodeCapacity: 500_000_000,
|
||||||
|
nodeNumChannels: 4,
|
||||||
|
nodeNumPeers: 6,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Everything the target side of a transfer (or a Lightning receive)
|
// Everything the target side of a transfer (or a Lightning receive)
|
||||||
// needs: the payRequest at the standard mint@ address, the invoice
|
// needs: the mint-address discovery endpoint carrying the mint metadata,
|
||||||
// callback, an immediately-settled verify endpoint revealing the
|
// the payRequest at the standard mint@ address, the invoice callback, an
|
||||||
// preimage, and the note info + rotate the claim then performs.
|
// immediately-settled verify endpoint revealing the preimage, and the
|
||||||
|
// note info + rotate the claim then performs.
|
||||||
async mockTargetMint(options: MockTargetMintOptions, origin = MINT2_ORIGIN): Promise<void> {
|
async mockTargetMint(options: MockTargetMintOptions, origin = MINT2_ORIGIN): Promise<void> {
|
||||||
await this.mockNoMintAddress(origin);
|
await this.mockMintAddress(options, origin);
|
||||||
await this.mockNoteInfo(
|
await this.mockNoteInfo(
|
||||||
{ amountMsat: options.noteAmountMsat, mintPubkey: options.mintPubkey },
|
{ amountMsat: options.noteAmountMsat, mintPubkey: options.mintPubkey },
|
||||||
origin,
|
origin,
|
||||||
@@ -129,7 +145,6 @@ export class MintMocker {
|
|||||||
minSendable: 1000,
|
minSendable: 1000,
|
||||||
maxSendable: 100_000_000_000,
|
maxSendable: 100_000_000_000,
|
||||||
withdrawLink: `${origin}${NOTE_PATH}`,
|
withdrawLink: `${origin}${NOTE_PATH}`,
|
||||||
mintPubkey: options.mintPubkey,
|
|
||||||
metadata: options.mintFeeMetadata ?? '[]',
|
metadata: options.mintFeeMetadata ?? '[]',
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+8
-7
@@ -23,7 +23,7 @@
|
|||||||
"@quasar/extras": "^2.0.4",
|
"@quasar/extras": "^2.0.4",
|
||||||
"@scure/bip32": "^2.3.0",
|
"@scure/bip32": "^2.3.0",
|
||||||
"@scure/bip39": "^2.3.0",
|
"@scure/bip39": "^2.3.0",
|
||||||
"lnurlcash-kit": "github:TheCryptoDonkey/lnurlcash-kit#392aeaf6682f23a0e66d28175a9df62802cd1d76",
|
"lnurlcash-kit": "0.1.1",
|
||||||
"nostr-tools": "2.24.3",
|
"nostr-tools": "2.24.3",
|
||||||
"pinia": "^4.0.2",
|
"pinia": "^4.0.2",
|
||||||
"qrcode.vue": "^3.10.0",
|
"qrcode.vue": "^3.10.0",
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
"eslint": "^10.8.0",
|
"eslint": "^10.8.0",
|
||||||
"eslint-plugin-vue": "^10.8.0",
|
"eslint-plugin-vue": "^10.8.0",
|
||||||
"globals": "^17.4.0",
|
"globals": "^17.4.0",
|
||||||
"lnurlcash-conformance": "github:TheCryptoDonkey/lnurlcash-conformance",
|
"lnurlcash-conformance": "0.1.1",
|
||||||
"postcss": "^8.5.8",
|
"postcss": "^8.5.8",
|
||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.1",
|
||||||
"sass": "^1.102.0",
|
"sass": "^1.102.0",
|
||||||
@@ -5961,8 +5961,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lnurlcash-conformance": {
|
"node_modules/lnurlcash-conformance": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"resolved": "git+ssh://git@github.com/TheCryptoDonkey/lnurlcash-conformance.git#3d63f8ac3f3b8b87f5b490ff4a55c310afe4e5e2",
|
"resolved": "https://registry.npmjs.org/lnurlcash-conformance/-/lnurlcash-conformance-0.1.1.tgz",
|
||||||
|
"integrity": "sha512-12EXkom7x+tpkk4Pi2zanlMbDpA64hh+b5PKTPoKfahiWPG3GQbNusun1NHKpYjNwyevRISTP9487W+ZqEuEZw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -5986,9 +5987,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lnurlcash-kit": {
|
"node_modules/lnurlcash-kit": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"resolved": "git+ssh://git@github.com/TheCryptoDonkey/lnurlcash-kit.git#392aeaf6682f23a0e66d28175a9df62802cd1d76",
|
"resolved": "https://registry.npmjs.org/lnurlcash-kit/-/lnurlcash-kit-0.1.1.tgz",
|
||||||
"integrity": "sha512-xgO7ykIBD5SdGV7M6xyhDJcfgbkvlHxmBmXu2gBs/Cew/RAfH7GRZSCdeVexo6vRGDXhX6IjqqqzfFXrYqkM9A==",
|
"integrity": "sha512-O1rak4hcoKIBZdtbCMfQhCS4hp9HducJeJf8nEYxbPXtFm83EyPBqOAwWHYkzJMFKnyTqjJmvjjzfgL3ePQQOw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@noble/curves": "^2.3.0",
|
"@noble/curves": "^2.3.0",
|
||||||
|
|||||||
+2
-2
@@ -34,7 +34,7 @@
|
|||||||
"@quasar/extras": "^2.0.4",
|
"@quasar/extras": "^2.0.4",
|
||||||
"@scure/bip32": "^2.3.0",
|
"@scure/bip32": "^2.3.0",
|
||||||
"@scure/bip39": "^2.3.0",
|
"@scure/bip39": "^2.3.0",
|
||||||
"lnurlcash-kit": "github:TheCryptoDonkey/lnurlcash-kit#392aeaf6682f23a0e66d28175a9df62802cd1d76",
|
"lnurlcash-kit": "0.1.1",
|
||||||
"nostr-tools": "2.24.3",
|
"nostr-tools": "2.24.3",
|
||||||
"pinia": "^4.0.2",
|
"pinia": "^4.0.2",
|
||||||
"qrcode.vue": "^3.10.0",
|
"qrcode.vue": "^3.10.0",
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
"eslint": "^10.8.0",
|
"eslint": "^10.8.0",
|
||||||
"eslint-plugin-vue": "^10.8.0",
|
"eslint-plugin-vue": "^10.8.0",
|
||||||
"globals": "^17.4.0",
|
"globals": "^17.4.0",
|
||||||
"lnurlcash-conformance": "github:TheCryptoDonkey/lnurlcash-conformance",
|
"lnurlcash-conformance": "0.1.1",
|
||||||
"postcss": "^8.5.8",
|
"postcss": "^8.5.8",
|
||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.1",
|
||||||
"sass": "^1.102.0",
|
"sass": "^1.102.0",
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
// Wire-contract coverage for the pinned lnurlcash-kit / lnurlcash-conformance
|
||||||
|
// 0.1.1 artifacts. Two contracts the app's mint discovery relies on:
|
||||||
|
// the kit must MAP the mint-address wire field `nodeCapacity` onto the
|
||||||
|
// app-facing `nodeCapacityMsat` (0.1.0 spread it under its wire name, so the
|
||||||
|
// typed field read undefined forever), and a payRequest withdraw link is
|
||||||
|
// legal in both its HTTPS and LUD-17 `lnurlw://` forms - the published
|
||||||
|
// conformance mock mint emits `lnurlw://` by default, so do NOT assume an
|
||||||
|
// HTTPS default anywhere in the receive path.
|
||||||
|
|
||||||
|
import {afterEach, describe, expect, it} from 'vitest'
|
||||||
|
import {createMockMint} from 'lnurlcash-conformance/mock-mint'
|
||||||
|
import {buildNoteUrl, fetchMintAddress} from 'lnurlcash-kit'
|
||||||
|
|
||||||
|
import {claimMintedNote, prepareMint} from './ops'
|
||||||
|
import {mintAddressCacheInfo} from './trustedMints'
|
||||||
|
|
||||||
|
type Mint = Awaited<ReturnType<typeof createMockMint>>
|
||||||
|
|
||||||
|
const mints: Mint[] = []
|
||||||
|
const mint = async (options: Parameters<typeof createMockMint>[0] = {}): Promise<Mint> => {
|
||||||
|
const m = await createMockMint(options)
|
||||||
|
mints.push(m)
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await Promise.all(mints.splice(0).map((m) => m.close()))
|
||||||
|
})
|
||||||
|
|
||||||
|
// paying a mint invoice is what brings its note into existence - the mock
|
||||||
|
// exposes that through its test hook (settle + credit in one step)
|
||||||
|
const settleLastInvoice = async (m: Mint): Promise<string> => {
|
||||||
|
const paymentHash = [...m.state.invoices.keys()].at(-1)
|
||||||
|
if (!paymentHash) throw new Error('no invoice requested yet')
|
||||||
|
const res = await fetch(`${m.url}/_test/settle?payment_hash=${paymentHash}`)
|
||||||
|
if (!res.ok) throw new Error(`settle hook failed: ${res.status}`)
|
||||||
|
const invoice = m.state.invoices.get(paymentHash)
|
||||||
|
if (!invoice) throw new Error('settled invoice vanished from the mock')
|
||||||
|
return invoice.preimage
|
||||||
|
}
|
||||||
|
|
||||||
|
const MINT_PUBKEY = `02${'ab'.repeat(32)}`
|
||||||
|
|
||||||
|
// a mint-address (LUD-25) wire response exactly as lnurl-mint serves it:
|
||||||
|
// node stats under their WIRE names - `nodeCapacity` is msat like every
|
||||||
|
// other amount, named without the suffix on the wire
|
||||||
|
const mintAddressFixture = {
|
||||||
|
tag: 'withdrawRequest',
|
||||||
|
callback: 'https://mint.example/w/cb',
|
||||||
|
minWithdrawable: 1_000,
|
||||||
|
maxWithdrawable: 100_000_000,
|
||||||
|
defaultDescription: 'fixture mint',
|
||||||
|
payLink: 'https://mint.example/.well-known/lnurlp/mint',
|
||||||
|
mintPubkey: MINT_PUBKEY,
|
||||||
|
nodeAlias: 'fixture-mint',
|
||||||
|
nodeCapacity: 500_000_000,
|
||||||
|
nodeNumChannels: 4,
|
||||||
|
nodeNumPeers: 6,
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsonResponse = (body: unknown): Response =>
|
||||||
|
new Response(JSON.stringify(body), {
|
||||||
|
status: 200,
|
||||||
|
headers: {'content-type': 'application/json'},
|
||||||
|
})
|
||||||
|
|
||||||
|
// a fetch that serves fixture bodies by URL prefix and 404s everything else,
|
||||||
|
// so a test drives the real kit HTTP boundary without any network
|
||||||
|
const fixtureFetch = (routes: ReadonlyArray<readonly [string, unknown]>): typeof fetch => {
|
||||||
|
const impl: typeof fetch = (input, _init) => {
|
||||||
|
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url
|
||||||
|
for (const [prefix, body] of routes) {
|
||||||
|
if (url.startsWith(prefix)) return Promise.resolve(jsonResponse(body))
|
||||||
|
}
|
||||||
|
return Promise.resolve(new Response('not found', {status: 404}))
|
||||||
|
}
|
||||||
|
return impl
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('mint-address wire contract', () => {
|
||||||
|
it('maps the wire nodeCapacity onto the app-facing nodeCapacityMsat', async () => {
|
||||||
|
const info = await fetchMintAddress('https://mint.example/.well-known/lnurlw/mint', {
|
||||||
|
fetch: fixtureFetch([['https://mint.example/', mintAddressFixture]]),
|
||||||
|
})
|
||||||
|
// renamed fields have to be mapped, not spread: the spread carries the
|
||||||
|
// wire name through and the typed one reads undefined forever
|
||||||
|
expect(info.nodePubkey).toBe(MINT_PUBKEY)
|
||||||
|
expect(info.nodeCapacityMsat).toBe(500_000_000)
|
||||||
|
expect(info.nodeNumChannels).toBe(4)
|
||||||
|
expect(info.nodeNumPeers).toBe(6)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('carries node stats into the cached trusted-mint display metadata', async () => {
|
||||||
|
const info = await fetchMintAddress('https://mint.example/.well-known/lnurlw/mint', {
|
||||||
|
fetch: fixtureFetch([['https://mint.example/', mintAddressFixture]]),
|
||||||
|
})
|
||||||
|
const cached = mintAddressCacheInfo(info, 'mint')
|
||||||
|
expect(cached?.nodeCapacityMsat).toBe(500_000_000)
|
||||||
|
expect(cached?.nodeNumChannels).toBe(4)
|
||||||
|
expect(cached?.nodeNumPeers).toBe(6)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("surfaces the mock mint's mint-address node stats through prepareMint", async () => {
|
||||||
|
const m = await mint()
|
||||||
|
const prepared = await prepareMint(`mint@127.0.0.1:${m.port}`, 21_000)
|
||||||
|
// the metadata is advertised at the mint-address endpoint itself -
|
||||||
|
// the payRequest never carried it
|
||||||
|
expect(prepared.nodeInfo?.nodePubkey).toBe(m.state.pubkey)
|
||||||
|
expect(prepared.nodeInfo?.nodeCapacityMsat).toBe(500_000_000)
|
||||||
|
expect(prepared.nodeInfo?.nodeNumChannels).toBe(4)
|
||||||
|
expect(prepared.nodeInfo?.nodeNumPeers).toBe(6)
|
||||||
|
const cached = mintAddressCacheInfo(prepared.nodeInfo, prepared.username)
|
||||||
|
expect(cached?.nodeCapacityMsat).toBe(500_000_000)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('withdraw-link forms', () => {
|
||||||
|
it('accepts the lnurlw:// withdraw link the conformance mock mint advertises', async () => {
|
||||||
|
const m = await mint({testHooks: true})
|
||||||
|
const prepared = await prepareMint(`mint@127.0.0.1:${m.port}`, 21_000)
|
||||||
|
// published conformance 0.1.1 emits lnurlw:// by default - NOT https
|
||||||
|
expect(prepared.withdrawLink).toMatch(/^lnurlw:\/\//)
|
||||||
|
|
||||||
|
// and the link is fully usable: settle the invoice, claim the note
|
||||||
|
const preimage = await settleLastInvoice(m)
|
||||||
|
const claimed = await claimMintedNote(prepared, {
|
||||||
|
intervalMs: 10,
|
||||||
|
intervalCapMs: 50,
|
||||||
|
maxWaitMs: 5_000,
|
||||||
|
})
|
||||||
|
expect(claimed.rotated).toBe(true)
|
||||||
|
expect(claimed.note.amount).toBe(21_000)
|
||||||
|
expect(m.state.noteState(preimage)).toBe('burned')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('accepts an HTTPS withdraw link', async () => {
|
||||||
|
const fetch = fixtureFetch([
|
||||||
|
['https://mint.example/.well-known/lnurlw/mint', mintAddressFixture],
|
||||||
|
[
|
||||||
|
'https://mint.example/.well-known/lnurlp/mint',
|
||||||
|
{
|
||||||
|
tag: 'payRequest',
|
||||||
|
callback: 'https://mint.example/pay',
|
||||||
|
minSendable: 1_000,
|
||||||
|
maxSendable: 100_000_000_000,
|
||||||
|
withdrawLink: 'https://mint.example/note',
|
||||||
|
metadata: '[]',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// amount-less invoice: the kit skips its amount cross-check
|
||||||
|
['https://mint.example/pay', {pr: 'lnmock1fixture', verify: null}],
|
||||||
|
])
|
||||||
|
const prepared = await prepareMint('mint@mint.example', 21_000, {fetch})
|
||||||
|
expect(prepared.withdrawLink).toBe('https://mint.example/note')
|
||||||
|
// the mint-address payLink is authoritative - the payRequest came from it
|
||||||
|
expect(prepared.mintUrl).toBe('https://mint.example/.well-known/lnurlp/mint')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('builds the same note URL from both withdraw-link forms', () => {
|
||||||
|
const k1 = 'ab'.repeat(32)
|
||||||
|
expect(buildNoteUrl('lnurlw://mint.example/note', k1, 21_000)).toBe(
|
||||||
|
buildNoteUrl('https://mint.example/note', k1, 21_000),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user