@@ -26,6 +26,23 @@ import { migrateNetworksFile } from 'utils/migrations';
2626import { isLinux , isMac } from 'utils/system' ;
2727import ComposeFile from './composeFile' ;
2828
29+ type SimulationNode = {
30+ id : string ;
31+ address : string ;
32+ macaroon ?: string ;
33+ cert ?: string ;
34+ ca_cert ?: string ;
35+ client_cert ?: string ;
36+ client_key ?: string ;
37+ } ;
38+
39+ type SimulationActivity = {
40+ source : string ;
41+ destination : string ;
42+ interval_secs : number ;
43+ amount_msat : number ;
44+ } ;
45+
2946let dockerInst : Dockerode | undefined ;
3047/**
3148 * Creates a new Dockerode instance by detecting the docker socket
@@ -160,6 +177,10 @@ class DockerService implements DockerLibrary {
160177 }
161178 } ) ;
162179
180+ if ( network . simulationActivities . length > 0 ) {
181+ file . addSimLn ( network . id ) ;
182+ }
183+
163184 const yml = yaml . dump ( file . content ) ;
164185 const path = join ( network . path , 'docker-compose.yml' ) ;
165186 await write ( path , yml ) ;
@@ -176,8 +197,37 @@ class DockerService implements DockerLibrary {
176197
177198 info ( `Starting docker containers for ${ network . name } ` ) ;
178199 info ( ` - path: ${ network . path } ` ) ;
179- const result = await this . execute ( compose . upAll , this . getArgs ( network ) ) ;
180- info ( `Network started:\n ${ result . out || result . err } ` ) ;
200+
201+ // we don't want to start the simln service when starting the network
202+ // because it depends on the running lightning nodes and the simulation
203+ // activity should be started separately based on user preference
204+ const servicesToStart = this . getServicesToStart (
205+ [ ...bitcoin , ...lightning , ...tap ] ,
206+ [ 'simln' ] ,
207+ ) ;
208+
209+ for ( const service of servicesToStart ) {
210+ const result = await this . execute ( compose . upOne , service , this . getArgs ( network ) ) ;
211+ info ( `Network started: ${ service } \n ${ result . out || result . err } ` ) ;
212+ }
213+ }
214+
215+ /**
216+ * Filter out services based on exclude list and return a list of service names to start
217+ * @param nodes Array of all nodes
218+ * @param exclude Array of container names to exclude
219+ */
220+ private getServicesToStart (
221+ nodes :
222+ | CommonNode [ ]
223+ | {
224+ name : 'simln' ;
225+ } [ ] ,
226+ exclude : string [ ] ,
227+ ) : string [ ] {
228+ return nodes
229+ . map ( node => node . name )
230+ . filter ( serviceName => ! exclude . includes ( serviceName ) ) ;
181231 }
182232
183233 /**
@@ -317,6 +367,112 @@ class DockerService implements DockerLibrary {
317367 }
318368 }
319369
370+ /**
371+ * Constructs the contents of sim.json file for the simulation activity
372+ * @param network the network to start
373+ */
374+ constructSimJson ( network : Network ) {
375+ const simJson : {
376+ nodes : SimulationNode [ ] ;
377+ activity : SimulationActivity [ ] ;
378+ } = {
379+ nodes : [ ] ,
380+ activity : [ ] ,
381+ } ;
382+
383+ network . simulationActivities . forEach ( activity => {
384+ const { source, destination } = activity ;
385+ const nodeArray = [ source , destination ] ;
386+
387+ for ( const node of nodeArray ) {
388+ let simNode : SimulationNode ;
389+
390+ // split the macaroon and cert path at "volumes/" to get the relative path
391+ // to the docker volume. This is necessary because the docker volumes are
392+ // mounted as a different path in the container.
393+ switch ( node . implementation ) {
394+ case 'LND' :
395+ const lnd = node as LndNode ;
396+ simNode = {
397+ id : lnd . name ,
398+ macaroon : `/home/simln/.${ lnd . paths . adminMacaroon . split ( 'volumes/' ) . pop ( ) } ` ,
399+ address : `https://host.docker.internal:${ lnd . ports . grpc } ` ,
400+ cert : `/home/simln/.${ lnd . paths . tlsCert . split ( 'volumes/' ) . pop ( ) } ` ,
401+ } ;
402+ break ;
403+
404+ case 'c-lightning' :
405+ const cln = node as CLightningNode ;
406+ simNode = {
407+ id : cln . name ,
408+ address : `https://host.docker.internal:${ cln . ports . grpc } ` ,
409+ ca_cert : `/home/simln/.${ cln . paths ?. tlsCert ?. split ( 'volumes/' ) . pop ( ) } ` ,
410+ client_cert : `/home/simln/.${ cln . paths ?. tlsClientCert
411+ ?. split ( 'volumes/' )
412+ . pop ( ) } `,
413+ client_key : `/home/simln/.${ cln . paths ?. tlsClientKey
414+ ?. split ( 'volumes/' )
415+ . pop ( ) } `,
416+ } ;
417+ break ;
418+
419+ default :
420+ throw new Error ( `unsupported node type ${ node . implementation } ` ) ;
421+ }
422+
423+ // console.log(`simNode >> \n ${JSON.stringify(simNode)}`);
424+ // Add the node to the nodes Set (duplicates are automatically handled)
425+ simJson . nodes . push ( simNode ) ;
426+ }
427+
428+ // Add the activity
429+ const simActivity : SimulationActivity = {
430+ source : activity . source . name ,
431+ destination : activity . destination . name ,
432+ interval_secs : activity . intervalSecs ,
433+ amount_msat : activity . amountMsat * 1000 ,
434+ } ;
435+
436+ // console.log(`simActivity >> \n ${JSON.stringify(simActivity)}`);
437+ // Add the activity to the activity Set (duplicates are automatically handled)
438+ simJson . activity . push ( simActivity ) ;
439+ } ) ;
440+ return {
441+ nodes : [ ...new Map ( simJson . nodes . map ( node => [ node [ 'id' ] , node ] ) ) . values ( ) ] ,
442+ activity : [
443+ ...new Map (
444+ simJson . activity . map ( activity => [
445+ `${ activity . source } -${ activity . destination } ` ,
446+ activity ,
447+ ] ) ,
448+ ) . values ( ) ,
449+ ] ,
450+ } ;
451+ }
452+
453+ /**
454+ * Start a simulation activity in the network using docker compose
455+ * @param network the network containing the simulation activity
456+ */
457+ async startSimulationActivity ( network : Network ) {
458+ const simJson = this . constructSimJson ( network ) ;
459+ // console.log(`simJson >> \n ${JSON.stringify(simJson)}`);
460+ await this . ensureDirs ( network , [
461+ ...network . nodes . bitcoin ,
462+ ...network . nodes . lightning ,
463+ ...network . nodes . tap ,
464+ ] ) ;
465+ const simjsonPath = nodePath ( network , 'simln' , 'sim.json' ) ;
466+ await write ( simjsonPath , JSON . stringify ( simJson ) ) ;
467+ console . log ( `simjsonPath >> \n ${ JSON . stringify ( simjsonPath ) } ` ) ;
468+ const result = await this . execute ( compose . upOne , 'simln' , this . getArgs ( network ) ) ;
469+ info ( `Simulation activity started:\n ${ result . out || result . err } ` ) ;
470+ }
471+
472+ async stopSimulationActivity ( network : Network ) {
473+ info ( `[stopSimulationActivity] \n ${ network } ` ) ;
474+ }
475+
320476 /**
321477 * Helper method to trap and format exceptions thrown and
322478 * @param cmd the compose function to call
0 commit comments