@@ -7,8 +7,12 @@ import {
77 shouldQuote ,
88 validateProviderOptions ,
99} from "./provider.ts" ;
10- import { buildQueryRequestEvent } from "./events.ts" ;
1110import {
11+ buildQueryRequestEvent ,
12+ buildSelectionFeedbackEvent ,
13+ } from "./events.ts" ;
14+ import {
15+ decryptNip44 ,
1216 generateKeypair ,
1317 type Event ,
1418 type Filter ,
@@ -206,8 +210,10 @@ test("Provider.serve publishes a kind 7000 quote when handler returns a Provider
206210 let onEventRef : ( ( e : Event ) => void ) | null = null ;
207211
208212 const relayClient = makeRelayClient ( {
209- subscribe : ( _filter : Filter , onEvent : ( e : Event ) => void ) : Subscription => {
210- onEventRef = onEvent ;
213+ subscribe : ( filter : Filter , onEvent : ( e : Event ) => void ) : Subscription => {
214+ // Capture only the request subscription (kinds: [5300]); ignore
215+ // the per-job selection subscription so its timeout fires fast.
216+ if ( ( filter . kinds ?? [ ] ) . includes ( 5300 ) ) onEventRef = onEvent ;
211217 return { close : ( ) => { } } ;
212218 } ,
213219 publish : async ( event : Event ) : Promise < PublishResult > => {
@@ -216,7 +222,11 @@ test("Provider.serve publishes a kind 7000 quote when handler returns a Provider
216222 } ,
217223 } ) ;
218224
219- const provider = createProvider ( { ...validOptions ( ) , relayClient } ) ;
225+ const provider = createProvider ( {
226+ ...validOptions ( ) ,
227+ relayClient,
228+ selectionTimeoutMs : 30 ,
229+ } ) ;
220230 const servePromise = provider . serve ( async ( ) => ( {
221231 amountSats : 250 ,
222232 produce : async ( ) => ( { data : null , proof : "p" } ) ,
@@ -238,7 +248,9 @@ test("Provider.serve publishes a kind 7000 quote when handler returns a Provider
238248 locktime_seconds : Math . floor ( Date . now ( ) / 1000 ) + 3600 ,
239249 expires_at : Date . now ( ) + 60_000 ,
240250 } ) ) ;
241- await new Promise ( ( r ) => setTimeout ( r , 10 ) ) ;
251+ // Wait long enough for the per-job selection timeout (30ms) to fire,
252+ // so no setTimeout leaks across to the test runner.
253+ await new Promise ( ( r ) => setTimeout ( r , 60 ) ) ;
242254 await provider . stop ( ) ;
243255 await servePromise ;
244256
@@ -288,6 +300,157 @@ test("Provider.serve declines requests where handler returns null (no publish)",
288300 expect ( published ) . toHaveLength ( 0 ) ;
289301} ) ;
290302
303+ test ( "Provider.serve waits for selection, runs producer, and publishes encrypted kind 6300 result" , async ( ) => {
304+ const published : Event [ ] = [ ] ;
305+ let onRequestEvent : ( ( e : Event ) => void ) | null = null ;
306+ let onSelectionEvent : ( ( e : Event ) => void ) | null = null ;
307+ let producerCalled = false ;
308+
309+ const relayClient = makeRelayClient ( {
310+ subscribe : ( filter : Filter , onEvent : ( e : Event ) => void ) : Subscription => {
311+ // First subscription is the request listener (kind 5300 only).
312+ // Second subscription (per job) is the selection listener
313+ // (kinds: [7000], #e: [requestId], authors: [customer]).
314+ const kinds = filter . kinds ?? [ ] ;
315+ if ( kinds . includes ( 5300 ) ) {
316+ onRequestEvent = onEvent ;
317+ } else if ( kinds . includes ( 7000 ) ) {
318+ onSelectionEvent = onEvent ;
319+ }
320+ return { close : ( ) => { } } ;
321+ } ,
322+ publish : async ( event : Event ) : Promise < PublishResult > => {
323+ published . push ( event ) ;
324+ return { successes : [ "wss://relay.example.org" ] , failures : [ ] } ;
325+ } ,
326+ } ) ;
327+
328+ const provider = createProvider ( {
329+ ...validOptions ( ) ,
330+ relayClient,
331+ selectionTimeoutMs : 200 ,
332+ } ) ;
333+ const servePromise = provider . serve ( async ( ) => ( {
334+ amountSats : 200 ,
335+ produce : async ( ) => {
336+ producerCalled = true ;
337+ return { data : { hello : "world" } , proof : "pf-bytes" } ;
338+ } ,
339+ } ) ) ;
340+
341+ await new Promise ( ( r ) => setTimeout ( r , 5 ) ) ;
342+ if ( onRequestEvent === null ) throw new Error ( "request subscribe was not called" ) ;
343+ const fireRequest = onRequestEvent as ( e : Event ) => void ;
344+
345+ // Customer sends a request.
346+ const requestEvent = buildQueryRequestEvent ( customerKey , {
347+ query_id : "q1" ,
348+ schema : "io.anchr.tlsn-https.v1" ,
349+ predicate : { foo : "bar" } ,
350+ customer_pubkey : customerKey . publicKey ,
351+ oracle_pubkey : ORACLE_A ,
352+ mint_url : "https://mint.example.org" ,
353+ bounty_token : "cashuB" ,
354+ max_amount_sats : 1000 ,
355+ locktime_seconds : Math . floor ( Date . now ( ) / 1000 ) + 3600 ,
356+ expires_at : Date . now ( ) + 60_000 ,
357+ } ) ;
358+ fireRequest ( requestEvent ) ;
359+
360+ // Wait for the provider to publish its quote and open the selection
361+ // subscription before we deliver the selection event.
362+ await new Promise ( ( r ) => setTimeout ( r , 30 ) ) ;
363+ if ( onSelectionEvent === null ) throw new Error ( "selection subscribe was not called" ) ;
364+ const fireSelection = onSelectionEvent as ( e : Event ) => void ;
365+
366+ // Customer announces selecting this provider, with a bound token.
367+ const selectionEvent = buildSelectionFeedbackEvent ( customerKey , requestEvent . id , {
368+ status : "processing" ,
369+ selected_provider_pubkey : providerKey . publicKey ,
370+ bound_token : "cashuBbound" ,
371+ } ) ;
372+ fireSelection ( selectionEvent ) ;
373+
374+ await new Promise ( ( r ) => setTimeout ( r , 30 ) ) ;
375+ await provider . stop ( ) ;
376+ await servePromise ;
377+
378+ expect ( producerCalled ) . toBe ( true ) ;
379+ // Two publishes from the provider: kind 7000 quote + kind 6300 result.
380+ expect ( published ) . toHaveLength ( 2 ) ;
381+ expect ( published [ 0 ] . kind ) . toBe ( 7000 ) ;
382+ expect ( published [ 1 ] . kind ) . toBe ( 6300 ) ;
383+
384+ // The kind 6300 content is NIP-44-encrypted to the customer.
385+ const decrypted = decryptNip44 (
386+ published [ 1 ] . content ,
387+ customerKey . secretKey ,
388+ providerKey . publicKey ,
389+ ) ;
390+ const payload = JSON . parse ( decrypted ) ;
391+ expect ( payload . schema ) . toBe ( "io.anchr.tlsn-https.v1" ) ;
392+ expect ( payload . data ) . toEqual ( { hello : "world" } ) ;
393+ expect ( payload . proof ) . toBe ( "pf-bytes" ) ;
394+ } ) ;
395+
396+ test ( "Provider.serve never runs the producer when no selection event arrives within timeout" , async ( ) => {
397+ const published : Event [ ] = [ ] ;
398+ let onRequestEvent : ( ( e : Event ) => void ) | null = null ;
399+ let producerCalled = false ;
400+
401+ const relayClient = makeRelayClient ( {
402+ subscribe : ( filter : Filter , onEvent : ( e : Event ) => void ) : Subscription => {
403+ const kinds = filter . kinds ?? [ ] ;
404+ if ( kinds . includes ( 5300 ) ) {
405+ onRequestEvent = onEvent ;
406+ }
407+ return { close : ( ) => { } } ;
408+ } ,
409+ publish : async ( event : Event ) : Promise < PublishResult > => {
410+ published . push ( event ) ;
411+ return { successes : [ "wss://relay.example.org" ] , failures : [ ] } ;
412+ } ,
413+ } ) ;
414+
415+ const provider = createProvider ( {
416+ ...validOptions ( ) ,
417+ relayClient,
418+ selectionTimeoutMs : 30 ,
419+ } ) ;
420+ const servePromise = provider . serve ( async ( ) => ( {
421+ amountSats : 100 ,
422+ produce : async ( ) => {
423+ producerCalled = true ;
424+ return { data : null , proof : "x" } ;
425+ } ,
426+ } ) ) ;
427+
428+ await new Promise ( ( r ) => setTimeout ( r , 5 ) ) ;
429+ if ( onRequestEvent === null ) throw new Error ( "request subscribe was not called" ) ;
430+ ( onRequestEvent as ( e : Event ) => void ) ( buildQueryRequestEvent ( customerKey , {
431+ query_id : "q1" ,
432+ schema : "io.anchr.tlsn-https.v1" ,
433+ predicate : { } ,
434+ customer_pubkey : customerKey . publicKey ,
435+ oracle_pubkey : ORACLE_A ,
436+ mint_url : "https://mint.example.org" ,
437+ bounty_token : "cashuB" ,
438+ max_amount_sats : 1000 ,
439+ locktime_seconds : Math . floor ( Date . now ( ) / 1000 ) + 3600 ,
440+ expires_at : Date . now ( ) + 60_000 ,
441+ } ) ) ;
442+
443+ // Wait beyond the selection timeout without delivering a selection.
444+ await new Promise ( ( r ) => setTimeout ( r , 80 ) ) ;
445+ await provider . stop ( ) ;
446+ await servePromise ;
447+
448+ expect ( producerCalled ) . toBe ( false ) ;
449+ // Only the kind 7000 quote was published; no kind 6300 result.
450+ expect ( published ) . toHaveLength ( 1 ) ;
451+ expect ( published [ 0 ] . kind ) . toBe ( 7000 ) ;
452+ } ) ;
453+
291454test ( "Provider.serve does not publish a quote that exceeds the request's maxAmountSats" , async ( ) => {
292455 const published : Event [ ] = [ ] ;
293456 let onEventRef : ( ( e : Event ) => void ) | null = null ;
0 commit comments