github.com/holochain/holochain-proto@v0.1.0-alpha-26.0.20200915073418-5c83169c9b5b/service.go (about)

     1  // Copyright (C) 2013-2017, The MetaCurrency Project (Eric Harris-Braun, Arthur Brock, et. al.)
     2  // Use of this source code is governed by GPLv3 found in the LICENSE file
     3  //----------------------------------------------------------------------------------------
     4  // Service implements functions and data that provide Holochain services in a unix file based environment
     5  
     6  package holochain
     7  
     8  import (
     9  	"bytes"
    10  	"encoding/base64"
    11  	"encoding/json"
    12  	"errors"
    13  	"fmt"
    14  	"github.com/BurntSushi/toml"
    15  	"github.com/google/uuid"
    16  	. "github.com/holochain/holochain-proto/hash"
    17  	"io"
    18  	"io/ioutil"
    19  	"net/http"
    20  	"os"
    21  	"path/filepath"
    22  	"regexp"
    23  	"sort"
    24  	"strconv"
    25  	"strings"
    26  	"time"
    27  )
    28  
    29  // System settings, directory, and file names
    30  const (
    31  	DefaultDirectoryName string = ".holochain"  // Directory for storing config data
    32  	ChainDataDir         string = "db"          // Sub-directory for all chain content files
    33  	ChainDNADir          string = "dna"         // Sub-directory for all chain definition files
    34  	ChainUIDir           string = "ui"          // Sub-directory for all chain user interface files
    35  	ChainTestDir         string = "test"        // Sub-directory for all chain test files
    36  	DNAFileName          string = "dna"         // Definition of the Holochain
    37  	ConfigFileName       string = "config"      // Settings of the Holochain
    38  	SysFileName          string = "system.conf" // Server & System settings
    39  	AgentFileName        string = "agent.txt"   // User ID info
    40  	PrivKeyFileName      string = "priv.key"    // Signing key - private
    41  	StoreFileName        string = "chain.db"    // Filename for local data store
    42  	DNAHashFileName      string = "dna.hash"    // Filename for storing the hash of the holochain
    43  	DHTStoreFileName     string = "dht.db"      // Filname for storing the dht
    44  	BridgeDBFileName     string = "bridge.db"   // Filname for storing bridge keys
    45  
    46  	TestConfigFileName string = "_config.json"
    47  
    48  	DefaultDHTPort         = 6283
    49  	DefaultBootstrapServer = "bootstrap.holochain.net:10000"
    50  
    51  	DefaultHashType HashType = HashType("sha2-256") // default hashing algo if not provided in DNA
    52  
    53  	CloneWithNewUUID  = true
    54  	CloneWithSameUUID = false
    55  	InitializeDB      = true
    56  	SkipInitializeDB  = false
    57  )
    58  
    59  //
    60  type CloneSpec struct {
    61  	Role   string
    62  	Number int
    63  }
    64  
    65  // TestConfig holds the configuration options for a test
    66  type TestConfig struct {
    67  	GossipInterval int // interval in milliseconds between gossips
    68  	Duration       int // if non-zero number of seconds to keep all nodes alive
    69  	Clone          []CloneSpec
    70  }
    71  
    72  // ServiceConfig holds the service settings
    73  type ServiceConfig struct {
    74  	DefaultPeerModeAuthor  bool
    75  	DefaultPeerModeDHTNode bool
    76  	DefaultBootstrapServer string
    77  	DefaultEnableMDNS      bool
    78  	DefaultEnableNATUPnP   bool
    79  }
    80  
    81  // A Service is a Holochain service data structure
    82  type Service struct {
    83  	Settings     ServiceConfig
    84  	DefaultAgent Agent
    85  	Path         string
    86  }
    87  
    88  type EntryDefFile struct {
    89  	Name       string
    90  	DataFormat string
    91  	Schema     string
    92  	SchemaFile string // file name of schema or language schema directive
    93  	Sharing    string
    94  }
    95  
    96  type ZomeFile struct {
    97  	Name         string
    98  	Description  string
    99  	CodeFile     string
   100  	RibosomeType string
   101  	BridgeFuncs  []string // functions in zome that can be bridged to by fromApp
   102  	Config       map[string]interface{}
   103  	Entries      []EntryDefFile
   104  	Functions    []FunctionDef
   105  }
   106  
   107  type DNAFile struct {
   108  	Version              int
   109  	UUID                 uuid.UUID
   110  	Name                 string
   111  	Properties           map[string]string
   112  	PropertiesSchemaFile string
   113  	BasedOn              Hash // references hash of another holochain that these schemas and code are derived from
   114  	RequiresVersion      int
   115  	DHTConfig            DHTConfig
   116  	Progenitor           Progenitor
   117  	Zomes                []ZomeFile
   118  }
   119  
   120  // AgentFixture defines an agent for the purposes of tests
   121  type AgentFixture struct {
   122  	Hash     string
   123  	Identity string
   124  }
   125  
   126  // TestFixtures defines data needed to run tests
   127  type TestFixtures struct {
   128  	Agents []AgentFixture
   129  }
   130  
   131  // TestSet holds a set of tests plus configuration and fixture data for those tests
   132  type TestSet struct {
   133  	Tests     []TestData
   134  	Identity  string
   135  	Fixtures  TestFixtures
   136  	Benchmark bool // activate benchmarking for all tests
   137  }
   138  
   139  // TestData holds a test entry for a chain
   140  type TestData struct {
   141  	Convey    string        // a human readable description of the tests intent
   142  	Zome      string        // the zome in which to find the function
   143  	FnName    string        // the function to call
   144  	Input     interface{}   // the function's input
   145  	Output    interface{}   // the expected output to match against (full match)
   146  	Err       interface{}   // the expected error to match against
   147  	ErrMsg    string        // the expected error message to match against
   148  	Regexp    string        // the expected out to match again (regular expression)
   149  	Time      time.Duration // offset in milliseconds from the start of the test at which to run this test.
   150  	Wait      time.Duration // time in milliseconds to wait before running this test from when the previous ran
   151  	Exposure  string        // the exposure context for the test call (defaults to ZOME_EXPOSURE)
   152  	Raw       bool          // set to true if we should ignore fnName and just call input as raw code in the zome, useful for testing helper functions and validation functions
   153  	Repeat    int           // number of times to repeat this test, useful for scenario testing
   154  	Benchmark bool          // activate benchmarking for this test
   155  }
   156  
   157  // IsInitialized checks a path for a correctly set up .holochain directory
   158  func IsInitialized(root string) bool {
   159  	return DirExists(root) && FileExists(filepath.Join(root, SysFileName)) && FileExists(filepath.Join(root, AgentFileName))
   160  }
   161  
   162  // Init initializes service defaults including a signing key pair for an agent
   163  // and writes them out to configuration files in the root path (making the
   164  // directory if necessary)
   165  func Init(root string, identity AgentIdentity, seed io.Reader) (service *Service, err error) {
   166  	//TODO this is in the wrong place it should be where HeadersEntryDef gets initialized
   167  	if HeadersEntryDef.validator == nil {
   168  		err = HeadersEntryDef.BuildJSONSchemaValidatorFromString(HeadersEntryDef.Schema)
   169  	}
   170  	if AgentEntryDef.validator == nil {
   171  		err = AgentEntryDef.BuildJSONSchemaValidatorFromString(AgentEntryDef.Schema)
   172  	}
   173  	if DelEntryDef.validator == nil {
   174  		err = DelEntryDef.BuildJSONSchemaValidatorFromString(DelEntryDef.Schema)
   175  	}
   176  	if MigrateEntryDef.validator == nil {
   177  		err = MigrateEntryDef.BuildJSONSchemaValidatorFromString(MigrateEntryDef.Schema)
   178  	}
   179  	if err != nil {
   180  		return
   181  	}
   182  
   183  	err = os.MkdirAll(root, os.ModePerm)
   184  	if err != nil {
   185  		return
   186  	}
   187  	s := Service{
   188  		Settings: ServiceConfig{
   189  			DefaultPeerModeDHTNode: true,
   190  			DefaultPeerModeAuthor:  true,
   191  			DefaultBootstrapServer: DefaultBootstrapServer,
   192  			DefaultEnableMDNS:      true,
   193  			DefaultEnableNATUPnP:   true,
   194  		},
   195  		Path: root,
   196  	}
   197  
   198  	var val string
   199  
   200  	val = os.Getenv("HC_DEFAULT_BOOTSTRAPSERVER")
   201  	if val != "" {
   202  		s.Settings.DefaultBootstrapServer = val
   203  		Infof("Using HC_DEFAULT_BOOTSTRAPSERVER--configuring default bootstrap server as: %s\n", s.Settings.DefaultBootstrapServer)
   204  	}
   205  
   206  	val = os.Getenv("HC_DEFAULT_ENABLEMDNS")
   207  	if val != "" {
   208  		s.Settings.DefaultEnableMDNS = val == "true"
   209  		Infof("Using HC_DEFAULT_ENABLEMDNS--configuring default MDNS use as: %v.\n", s.Settings.DefaultEnableMDNS)
   210  	}
   211  
   212  	val = os.Getenv("HC_DEFAULT_ENABLENATUPNP")
   213  	if val != "" {
   214  		s.Settings.DefaultEnableNATUPnP = val == "true"
   215  		Infof("Using HC_DEFAULT_ENABLENATUPNP--configuring default UPnP use as: %v.\n", s.Settings.DefaultEnableNATUPnP)
   216  	}
   217  
   218  	err = writeToml(root, SysFileName, s.Settings, false)
   219  	if err != nil {
   220  		return
   221  	}
   222  
   223  	a, err := NewAgent(LibP2P, identity, seed)
   224  	if err != nil {
   225  		return
   226  	}
   227  	err = SaveAgent(root, a)
   228  	if err != nil {
   229  		return
   230  	}
   231  
   232  	s.DefaultAgent = a
   233  
   234  	service = &s
   235  	return
   236  }
   237  
   238  // LoadService creates a service object from a configuration file
   239  func LoadService(path string) (service *Service, err error) {
   240  	agent, err := LoadAgent(path)
   241  	if err != nil {
   242  		return
   243  	}
   244  	s := Service{
   245  		Path:         path,
   246  		DefaultAgent: agent,
   247  	}
   248  
   249  	_, err = toml.DecodeFile(filepath.Join(path, SysFileName), &s.Settings)
   250  	if err != nil {
   251  		return
   252  	}
   253  
   254  	if err = s.Settings.Validate(); err != nil {
   255  		return
   256  	}
   257  
   258  	service = &s
   259  	return
   260  }
   261  
   262  // Validate validates settings values
   263  func (c *ServiceConfig) Validate() (err error) {
   264  	if !(c.DefaultPeerModeAuthor || c.DefaultPeerModeDHTNode) {
   265  		err = errors.New(SysFileName + ": At least one peer mode must be set to true.")
   266  		return
   267  	}
   268  	return
   269  }
   270  
   271  // ConfiguredChains returns a list of the configured chains for the given service
   272  func (s *Service) ConfiguredChains() (chains map[string]*Holochain, err error) {
   273  	files, err := ioutil.ReadDir(s.Path)
   274  	if err != nil {
   275  		return
   276  	}
   277  	chains = make(map[string]*Holochain)
   278  	for _, f := range files {
   279  		if f.IsDir() {
   280  			h, err := s.Load(f.Name())
   281  			if err == nil {
   282  				chains[f.Name()] = h
   283  			}
   284  		}
   285  	}
   286  	return
   287  }
   288  
   289  // Find the DNA files
   290  func findDNA(path string) (f string, err error) {
   291  	p := filepath.Join(path, DNAFileName)
   292  
   293  	matches, err := filepath.Glob(p + ".*")
   294  	if err != nil {
   295  		return
   296  	}
   297  	for _, fn := range matches {
   298  		f = EncodingFormat(fn)
   299  		if f != "" {
   300  			break
   301  		}
   302  	}
   303  
   304  	if f == "" {
   305  		err = fmt.Errorf("No DNA file in %s%s", path, string(os.PathSeparator))
   306  		return
   307  	}
   308  	return
   309  }
   310  
   311  // IsConfigured checks a directory for correctly set up holochain configuration file
   312  func (s *Service) IsConfigured(name string) (f string, err error) {
   313  	root := filepath.Join(s.Path, name)
   314  
   315  	f, err = findDNA(filepath.Join(root, ChainDNADir))
   316  	if err != nil {
   317  		return
   318  	}
   319  	//@todo check other things?
   320  
   321  	return
   322  }
   323  
   324  // Load instantiates a Holochain instance from disk
   325  func (s *Service) Load(name string) (h *Holochain, err error) {
   326  	f, err := s.IsConfigured(name)
   327  	if err != nil {
   328  		return
   329  	}
   330  	h, err = s.load(name, f)
   331  	return
   332  }
   333  
   334  // loadDNA decodes a DNA from a directory hierarchy as specified by a DNAFile
   335  func (s *Service) loadDNA(path string, filename string, format string) (dnaP *DNA, err error) {
   336  	var dnaFile DNAFile
   337  	var dna DNA
   338  	dnafile := filepath.Join(path, filename+"."+format)
   339  	f, err := os.Open(dnafile)
   340  	if err != nil {
   341  		err = fmt.Errorf("error opening DNA file %s: %v", dnafile, err)
   342  		return
   343  	}
   344  	defer f.Close()
   345  
   346  	err = Decode(f, format, &dnaFile)
   347  	if err != nil {
   348  		err = fmt.Errorf("error decoding DNA file %s: %v", dnafile, err)
   349  		return
   350  	}
   351  
   352  	var validator SchemaValidator
   353  	var propertiesSchema []byte
   354  	if dnaFile.PropertiesSchemaFile != "" {
   355  		propertiesSchema, err = ReadFile(path, dnaFile.PropertiesSchemaFile)
   356  		if err != nil {
   357  			err = fmt.Errorf("error reading properties Schema file %s: %v", dnaFile.PropertiesSchemaFile, err)
   358  			return
   359  		}
   360  		schemapath := filepath.Join(path, dnaFile.PropertiesSchemaFile)
   361  		validator, err = BuildJSONSchemaValidatorFromFile(schemapath)
   362  		if err != nil {
   363  			err = fmt.Errorf("error building validator for %s: %v", schemapath, err)
   364  			return
   365  		}
   366  	}
   367  
   368  	dna.Version = dnaFile.Version
   369  	dna.UUID = dnaFile.UUID
   370  	dna.Name = dnaFile.Name
   371  	dna.BasedOn = dnaFile.BasedOn
   372  	dna.RequiresVersion = dnaFile.RequiresVersion
   373  	dna.DHTConfig = dnaFile.DHTConfig
   374  	dna.Progenitor = dnaFile.Progenitor
   375  	dna.Properties = dnaFile.Properties
   376  	dna.PropertiesSchema = string(propertiesSchema)
   377  	dna.propertiesSchemaValidator = validator
   378  
   379  	// Fallback to the default hash type
   380  	var emptyHashType HashType
   381  	if dna.DHTConfig.HashType == emptyHashType {
   382  		dna.DHTConfig.HashType = DefaultHashType
   383  	}
   384  
   385  	err = dna.check()
   386  	if err != nil {
   387  		err = fmt.Errorf("dna failed check with: %v", err)
   388  		return
   389  	}
   390  
   391  	dna.Zomes = make([]Zome, len(dnaFile.Zomes))
   392  	for i, zome := range dnaFile.Zomes {
   393  		if zome.CodeFile == "" {
   394  			var ext string
   395  			switch zome.RibosomeType {
   396  			case "js":
   397  				ext = ".js"
   398  			case "zygo":
   399  				ext = ".zy"
   400  			}
   401  			dnaFile.Zomes[i].CodeFile = zome.Name + ext
   402  		}
   403  
   404  		zomePath := filepath.Join(path, zome.Name)
   405  		codeFilePath := filepath.Join(zomePath, zome.CodeFile)
   406  		if !FileExists(codeFilePath) {
   407  			return nil, errors.New("DNA specified code file missing: " + zome.CodeFile)
   408  		}
   409  
   410  		dna.Zomes[i].Name = zome.Name
   411  		dna.Zomes[i].Description = zome.Description
   412  		dna.Zomes[i].RibosomeType = zome.RibosomeType
   413  		dna.Zomes[i].Functions = zome.Functions
   414  		dna.Zomes[i].Config = zome.Config
   415  		dna.Zomes[i].BridgeFuncs = zome.BridgeFuncs
   416  
   417  		var code []byte
   418  		code, err = ReadFile(zomePath, zome.CodeFile)
   419  		if err != nil {
   420  			return
   421  		}
   422  		dna.Zomes[i].Code = string(code[:])
   423  
   424  		dna.Zomes[i].Entries = make([]EntryDef, len(zome.Entries))
   425  		for j, entry := range zome.Entries {
   426  			dna.Zomes[i].Entries[j].Name = entry.Name
   427  			dna.Zomes[i].Entries[j].DataFormat = entry.DataFormat
   428  			dna.Zomes[i].Entries[j].Sharing = entry.Sharing
   429  			dna.Zomes[i].Entries[j].Schema = entry.Schema
   430  			if entry.Schema == "" && entry.SchemaFile != "" {
   431  				schemaFilePath := filepath.Join(zomePath, entry.SchemaFile)
   432  				if !FileExists(schemaFilePath) {
   433  					return nil, errors.New("DNA specified schema file missing: " + schemaFilePath)
   434  				}
   435  				var schema []byte
   436  				schema, err = ReadFile(zomePath, entry.SchemaFile)
   437  				if err != nil {
   438  					return
   439  				}
   440  				dna.Zomes[i].Entries[j].Schema = string(schema)
   441  				if strings.HasSuffix(entry.SchemaFile, ".json") {
   442  					if err = dna.Zomes[i].Entries[j].BuildJSONSchemaValidator(schemaFilePath); err != nil {
   443  						err = fmt.Errorf("error building validator for %s: %v", schemaFilePath, err)
   444  						return nil, err
   445  					}
   446  				}
   447  			}
   448  		}
   449  	}
   450  
   451  	dnaP = &dna
   452  	return
   453  }
   454  
   455  // load unmarshals a holochain structure for the named chain and format
   456  func (s *Service) load(name string, format string) (hP *Holochain, err error) {
   457  	var h Holochain
   458  	root := filepath.Join(s.Path, name)
   459  
   460  	// load the config
   461  	var f *os.File
   462  	f, err = os.Open(filepath.Join(root, ConfigFileName+"."+format))
   463  	if err != nil {
   464  		return
   465  	}
   466  	defer f.Close()
   467  	err = Decode(f, format, &h.Config)
   468  	if err != nil {
   469  		return
   470  	}
   471  	if err = h.Config.Setup(); err != nil {
   472  		return
   473  	}
   474  
   475  	dna, err := s.loadDNA(filepath.Join(root, ChainDNADir), DNAFileName, format)
   476  	if err != nil {
   477  		return
   478  	}
   479  
   480  	h.encodingFormat = format
   481  	h.rootPath = root
   482  	h.nucleus = NewNucleus(&h, dna)
   483  
   484  	// try and get the holochain-specific agent info
   485  	agent, err := LoadAgent(root)
   486  	if err != nil {
   487  		// if not specified for this app, get the default from the Agent.txt file for all apps
   488  		agent, err = LoadAgent(filepath.Dir(root))
   489  	}
   490  
   491  	// TODO verify Agent identity against schema
   492  	if err != nil {
   493  		return
   494  	}
   495  	h.agent = agent
   496  
   497  	// once the agent is set up we can calculate the id
   498  	h.nodeID, h.nodeIDStr, err = agent.NodeID()
   499  	if err != nil {
   500  		return
   501  	}
   502  
   503  	if err = h.PrepareHashType(); err != nil {
   504  		return
   505  	}
   506  
   507  	h.chain, err = NewChainFromFile(h.hashSpec, filepath.Join(h.DBPath(), StoreFileName))
   508  	if err != nil {
   509  		return
   510  	}
   511  
   512  	// if the chain has been started there should be a DNAHashFile which
   513  	// we can load to check against the actual hash of the DNA entry
   514  	if len(h.chain.Headers) > 0 {
   515  		h.dnaHash = h.chain.Headers[0].EntryLink.Clone()
   516  
   517  		var b []byte
   518  		b, err = ReadFile(h.rootPath, DNAHashFileName)
   519  		if err == nil {
   520  			if h.dnaHash.String() != string(b) {
   521  				err = errors.New("DNA doesn't match file!")
   522  				return
   523  			}
   524  		}
   525  	}
   526  
   527  	// @TODO compare value from file to actual hash
   528  
   529  	if h.chain.Length() > 0 {
   530  		h.agentHash = h.chain.Headers[1].EntryLink
   531  		_, topHeader := h.chain.TopType(AgentEntryType)
   532  		h.agentTopHash = topHeader.EntryLink
   533  	}
   534  	hP = &h
   535  	return
   536  }
   537  
   538  // gen calls a make function which should build the holochain structure and supporting files
   539  func gen(root string, initDB bool, makeH func(root string) (hP *Holochain, err error)) (h *Holochain, err error) {
   540  	if DirExists(root) {
   541  		return nil, mkErr(root + " already exists")
   542  	}
   543  	if err := os.MkdirAll(root, os.ModePerm); err != nil {
   544  		return nil, err
   545  	}
   546  
   547  	// cleanup the directory if we enounter an error while generating
   548  	defer func() {
   549  		if err != nil {
   550  			os.RemoveAll(root)
   551  		}
   552  	}()
   553  
   554  	h, err = makeH(root)
   555  	if err != nil {
   556  		return nil, err
   557  	}
   558  
   559  	if initDB {
   560  		if err := os.MkdirAll(h.DBPath(), os.ModePerm); err != nil {
   561  			return nil, err
   562  		}
   563  
   564  		h.chain, err = NewChainFromFile(h.hashSpec, filepath.Join(h.DBPath(), StoreFileName))
   565  		if err != nil {
   566  			return nil, err
   567  		}
   568  	}
   569  	return
   570  }
   571  
   572  func suffixByRibosomeType(ribosomeType string) (suffix string) {
   573  	switch ribosomeType {
   574  	case JSRibosomeType:
   575  		suffix = ".js"
   576  	case ZygoRibosomeType:
   577  		suffix = ".zy"
   578  	default:
   579  	}
   580  	return
   581  }
   582  
   583  func _makeConfig(s *Service) (config Config, err error) {
   584  	config = Config{
   585  		DHTPort:         DefaultDHTPort,
   586  		PeerModeDHTNode: s.Settings.DefaultPeerModeDHTNode,
   587  		PeerModeAuthor:  s.Settings.DefaultPeerModeAuthor,
   588  		BootstrapServer: s.Settings.DefaultBootstrapServer,
   589  		EnableNATUPnP:   s.Settings.DefaultEnableNATUPnP,
   590  		EnableMDNS:      s.Settings.DefaultEnableMDNS,
   591  		Loggers: Loggers{
   592  			Debug:      Logger{Name: "Debug", Format: "HC: %{file}.%{line}: %{message}", Enabled: false},
   593  			App:        Logger{Name: "App", Format: "%{color:cyan}%{message}", Enabled: false},
   594  			DHT:        Logger{Name: "DHT", Format: "%{color:yellow}%{time} DHT: %{message}"},
   595  			Gossip:     Logger{Name: "Gossip", Format: "%{color:blue}%{time} Gossip: %{message}"},
   596  			TestPassed: Logger{Name: "TestPassed", Format: "%{color:green}%{message}", Enabled: true},
   597  			TestFailed: Logger{Name: "TestFailed", Format: "%{color:red}%{message}", Enabled: true},
   598  			TestInfo:   Logger{Name: "TestInfo", Format: "%{message}", Enabled: true},
   599  		},
   600  	}
   601  
   602  	val := os.Getenv("HOLOCHAINCONFIG_DHTPORT")
   603  	if val != "" {
   604  		Debugf("makeConfig: using environment variable to set port to: %s", val)
   605  		config.DHTPort, err = strconv.Atoi(val)
   606  		if err != nil {
   607  			return
   608  		}
   609  		Debugf("makeConfig: using environment variable to set port to: %v\n", val)
   610  	}
   611  	val = os.Getenv("HOLOCHAINCONFIG_BOOTSTRAP")
   612  	if val != "" {
   613  		if val == "_" {
   614  			val = ""
   615  		}
   616  		config.BootstrapServer = val
   617  		if val == "" {
   618  			val = "NO BOOTSTRAP SERVER"
   619  		}
   620  		Debugf("makeConfig: using environment variable to set bootstrap server to: %s", val)
   621  	}
   622  
   623  	val = os.Getenv("HOLOCHAINCONFIG_ENABLEMDNS")
   624  	if val != "" {
   625  		Debugf("makeConfig: using environment variable to set enableMDNS to: %s", val)
   626  		config.EnableMDNS = val == "true"
   627  	}
   628  
   629  	val = os.Getenv("HOLOCHAINCONFIG_ENABLENATUPNP")
   630  	if val != "" {
   631  		Debugf("makeConfig: using environment variable to set enableNATUPnP to: %s", val)
   632  		config.EnableNATUPnP = val == "true"
   633  	}
   634  	return
   635  }
   636  
   637  func makeConfig(h *Holochain, s *Service) (err error) {
   638  	h.Config, err = _makeConfig(s)
   639  	if err != nil {
   640  		return
   641  	}
   642  	p := filepath.Join(h.rootPath, ConfigFileName+"."+h.encodingFormat)
   643  	f, err := os.Create(p)
   644  	if err != nil {
   645  		return err
   646  	}
   647  	defer f.Close()
   648  
   649  	if err = Encode(f, h.encodingFormat, &h.Config); err != nil {
   650  		return
   651  	}
   652  	if err = h.Config.Setup(); err != nil {
   653  		return
   654  	}
   655  	return
   656  }
   657  
   658  func (service *Service) InitAppDir(root string, encodingFormat string) (err error) {
   659  	var config Config
   660  	config, err = _makeConfig(service)
   661  	if err != nil {
   662  		return
   663  	}
   664  	p := filepath.Join(root, ConfigFileName+"."+encodingFormat)
   665  	var f, f1 *os.File
   666  	f, err = os.Create(p)
   667  	if err != nil {
   668  		return
   669  	}
   670  	defer f.Close()
   671  
   672  	if err = Encode(f, encodingFormat, &config); err != nil {
   673  		return
   674  	}
   675  
   676  	if err = os.MkdirAll(filepath.Join(root, ChainDataDir), os.ModePerm); err != nil {
   677  		return
   678  	}
   679  
   680  	f1, err = os.Create(filepath.Join(root, ChainDataDir, StoreFileName))
   681  	if err != nil {
   682  		return
   683  	}
   684  	defer f1.Close()
   685  	return
   686  }
   687  
   688  // MakeTestingApp generates a holochain used for testing purposes
   689  func (s *Service) MakeTestingApp(root string, encodingFormat string, initDB bool, newUUID bool, agent Agent) (h *Holochain, err error) {
   690  	if DirExists(root) {
   691  		return nil, mkErr(root + " already exists")
   692  	}
   693  	appPackageReader := bytes.NewBuffer([]byte(TestingAppAppPackage()))
   694  
   695  	if filepath.IsAbs(root) {
   696  		origPath := s.Path
   697  		s.Path = filepath.Dir(root)
   698  		defer func() { s.Path = origPath }()
   699  	}
   700  
   701  	name := filepath.Base(root)
   702  	_, err = s.SaveFromAppPackage(appPackageReader, root, "test", agent, TestingAppDecodingFormat, encodingFormat, newUUID)
   703  	if err != nil {
   704  		return
   705  	}
   706  	if err = mkChainDirs(root, initDB); err != nil {
   707  		return
   708  	}
   709  
   710  	if initDB {
   711  		var config Config
   712  		config, err = _makeConfig(s)
   713  		if err != nil {
   714  			return
   715  		}
   716  		p := filepath.Join(root, ConfigFileName+"."+encodingFormat)
   717  		var f, f1 *os.File
   718  		f, err = os.Create(p)
   719  		if err != nil {
   720  			return
   721  		}
   722  		defer f.Close()
   723  
   724  		if err = Encode(f, encodingFormat, &config); err != nil {
   725  			return
   726  		}
   727  
   728  		f1, err = os.Create(filepath.Join(root, ChainDataDir, StoreFileName))
   729  		if err != nil {
   730  			return
   731  		}
   732  		defer f1.Close()
   733  
   734  		h, err = s.Load(name)
   735  		if err != nil {
   736  			return
   737  		}
   738  		if err = h.Config.Setup(); err != nil {
   739  			return
   740  		}
   741  	}
   742  	return
   743  }
   744  
   745  // if the directories don't exist, make the place to store chains
   746  func mkChainDirs(root string, initDB bool) (err error) {
   747  	if initDB {
   748  		if err = os.MkdirAll(filepath.Join(root, ChainDataDir), os.ModePerm); err != nil {
   749  			return err
   750  		}
   751  	}
   752  	if err = os.MkdirAll(filepath.Join(root, ChainUIDir), os.ModePerm); err != nil {
   753  		return
   754  	}
   755  	if err = os.MkdirAll(filepath.Join(root, ChainTestDir), os.ModePerm); err != nil {
   756  		return
   757  	}
   758  	return
   759  }
   760  
   761  // Clone copies DNA files from a source directory
   762  // bool new indicates if this clone should create a new DNA (when true) or act as a Join
   763  func (s *Service) Clone(srcPath string, root string, agent Agent, new bool, initDB bool) (hP *Holochain, err error) {
   764  	hP, err = gen(root, initDB, func(root string) (*Holochain, error) {
   765  		var h Holochain
   766  		srcDNAPath := filepath.Join(srcPath, ChainDNADir)
   767  
   768  		format, err := findDNA(srcDNAPath)
   769  		if err != nil {
   770  			return nil, err
   771  		}
   772  
   773  		dna, err := s.loadDNA(srcDNAPath, DNAFileName, format)
   774  		if err != nil {
   775  			return nil, err
   776  		}
   777  
   778  		h.nucleus = NewNucleus(&h, dna)
   779  		h.encodingFormat = format
   780  		h.rootPath = root
   781  
   782  		// create the DNA directory and copy
   783  		if err := os.MkdirAll(h.DNAPath(), os.ModePerm); err != nil {
   784  			return nil, err
   785  		}
   786  
   787  		// TODO verify identity against schema?
   788  		h.agent = agent
   789  
   790  		// once the agent is set up we can calculate the id
   791  		h.nodeID, h.nodeIDStr, err = agent.NodeID()
   792  		if err != nil {
   793  			return nil, err
   794  		}
   795  
   796  		// make a config file
   797  		if err = makeConfig(&h, s); err != nil {
   798  			return nil, err
   799  		}
   800  
   801  		if new {
   802  			h.nucleus.dna.NewUUID()
   803  
   804  			// use the path as the name
   805  			h.nucleus.dna.Name = filepath.Base(root)
   806  
   807  			// change the progenitor to self because this is a clone!
   808  			pk, err := agent.PubKey().Bytes()
   809  			if err != nil {
   810  				return nil, err
   811  			}
   812  			h.nucleus.dna.Progenitor = Progenitor{Identity: string(agent.Identity()), PubKey: pk}
   813  		}
   814  
   815  		// save out the DNA file
   816  		if err = s.saveDNAFile(h.rootPath, h.nucleus.dna, h.encodingFormat, true); err != nil {
   817  			return nil, err
   818  		}
   819  
   820  		// and the agent
   821  		err = SaveAgent(h.rootPath, h.agent)
   822  		if err != nil {
   823  			return nil, err
   824  		}
   825  
   826  		// copy any UI files
   827  		srcUIPath := filepath.Join(srcPath, ChainUIDir)
   828  		if DirExists(srcUIPath) {
   829  			if err = CopyDir(srcUIPath, h.UIPath()); err != nil {
   830  				return nil, err
   831  			}
   832  		}
   833  
   834  		// copy any test files
   835  		srcTestDir := filepath.Join(srcPath, ChainTestDir)
   836  		if DirExists(srcTestDir) {
   837  			if err = CopyDir(srcTestDir, filepath.Join(root, ChainTestDir)); err != nil {
   838  				return nil, err
   839  			}
   840  		}
   841  		return &h, nil
   842  	})
   843  	return
   844  }
   845  
   846  func DNAHashofUngenedChain(h *Holochain) (DNAHash Hash, err error) {
   847  	var buf bytes.Buffer
   848  
   849  	err = h.EncodeDNA(&buf)
   850  	e := GobEntry{C: buf.Bytes()}
   851  
   852  	err = h.PrepareHashType()
   853  	if err != nil {
   854  		return
   855  	}
   856  
   857  	var dnaHeader *Header
   858  	_, dnaHeader, err = newHeader(h.hashSpec, time.Now(), DNAEntryType, &e, h.agent.PrivKey(), NullHash(), NullHash(), NullHash())
   859  	if err != nil {
   860  		return
   861  	}
   862  	DNAHash = dnaHeader.EntryLink.Clone()
   863  	return
   864  }
   865  
   866  // GenChain adds the genesis entries to a newly cloned or joined chain
   867  func (s *Service) GenChain(name string) (h *Holochain, err error) {
   868  	h, err = s.Load(name)
   869  	if err != nil {
   870  		return
   871  	}
   872  	_, err = h.GenChain()
   873  	if err != nil {
   874  		return
   875  	}
   876  	//	go h.DHT().HandleChangeReqs()
   877  	return
   878  }
   879  
   880  // List chains produces a textual representation of the chains in the .holochain directory
   881  func (s *Service) ListChains() (list string) {
   882  	chains, _ := s.ConfiguredChains()
   883  	l := len(chains)
   884  	if l > 0 {
   885  		keys := make([]string, l)
   886  		i := 0
   887  		for k := range chains {
   888  			keys[i] = k
   889  			i++
   890  		}
   891  		sort.Strings(keys)
   892  		list = "installed holochains:\n"
   893  		for _, k := range keys {
   894  			id := chains[k].DNAHash()
   895  			var sid = "<not-started>"
   896  			if id.String() != "" {
   897  				sid = id.String()
   898  			}
   899  			list += fmt.Sprintf("    %v %v\n", k, sid)
   900  			bridges, _ := chains[k].GetBridges()
   901  			if bridges != nil {
   902  				for _, b := range bridges {
   903  					if b.Side == BridgeCaller {
   904  						list += fmt.Sprintf("        bridged to: %s (%v)\n", b.CalleeName, b.CalleeApp)
   905  					} else {
   906  						list += fmt.Sprintf("        bridged from by token: %v\n", b.Token)
   907  					}
   908  				}
   909  			}
   910  		}
   911  
   912  	} else {
   913  		list = "no installed chains"
   914  	}
   915  	return
   916  }
   917  
   918  // saveDNAFile writes out holochain DNA to files
   919  func (s *Service) saveDNAFile(root string, dna *DNA, encodingFormat string, overwrite bool) (err error) {
   920  	dnaPath := filepath.Join(root, ChainDNADir)
   921  	p := filepath.Join(dnaPath, DNAFileName+"."+encodingFormat)
   922  	if !overwrite && FileExists(p) {
   923  		return mkErr(p + " already exists")
   924  	}
   925  
   926  	f, err := os.Create(p)
   927  	if err != nil {
   928  		return err
   929  	}
   930  	defer f.Close()
   931  
   932  	dnaFile := DNAFile{
   933  		Version:              dna.Version,
   934  		UUID:                 dna.UUID,
   935  		Name:                 dna.Name,
   936  		Properties:           dna.Properties,
   937  		PropertiesSchemaFile: dna.PropertiesSchemaFile,
   938  		BasedOn:              dna.BasedOn,
   939  		RequiresVersion:      dna.RequiresVersion,
   940  		DHTConfig:            dna.DHTConfig,
   941  		Progenitor:           dna.Progenitor,
   942  	}
   943  	for _, z := range dna.Zomes {
   944  		zpath := filepath.Join(dnaPath, z.Name)
   945  		if err = os.MkdirAll(zpath, os.ModePerm); err != nil {
   946  			return
   947  		}
   948  		if err = WriteFile([]byte(z.Code), zpath, z.Name+suffixByRibosomeType(z.RibosomeType)); err != nil {
   949  			return
   950  		}
   951  
   952  		zomeFile := ZomeFile{Name: z.Name,
   953  			Description:  z.Description,
   954  			CodeFile:     z.CodeFileName(),
   955  			RibosomeType: z.RibosomeType,
   956  			Functions:    z.Functions,
   957  			BridgeFuncs:  z.BridgeFuncs,
   958  			Config:       z.Config,
   959  		}
   960  
   961  		for _, e := range z.Entries {
   962  			entryDefFile := EntryDefFile{
   963  				Name:       e.Name,
   964  				DataFormat: e.DataFormat,
   965  				Sharing:    e.Sharing,
   966  			}
   967  			if e.DataFormat == DataFormatJSON && e.Schema != "" {
   968  				entryDefFile.SchemaFile = e.Name + ".json"
   969  				if err = WriteFile([]byte(e.Schema), zpath, e.Name+".json"); err != nil {
   970  					return
   971  				}
   972  			}
   973  
   974  			zomeFile.Entries = append(zomeFile.Entries, entryDefFile)
   975  		}
   976  		dnaFile.Zomes = append(dnaFile.Zomes, zomeFile)
   977  	}
   978  
   979  	if dna.PropertiesSchema != "" {
   980  		if err = WriteFile([]byte(dna.PropertiesSchema), dnaPath, "properties_schema.json"); err != nil {
   981  			return
   982  		}
   983  	}
   984  
   985  	err = Encode(f, encodingFormat, dnaFile)
   986  	return
   987  }
   988  
   989  // MakeAppPackage creates a package blob from a given holochain
   990  func (service *Service) MakeAppPackage(h *Holochain) (data []byte, err error) {
   991  	appPackage := AppPackage{
   992  		Version:   AppPackageVersion,
   993  		Generator: "holochain " + VersionStr,
   994  		DNA:       *h.nucleus.dna,
   995  	}
   996  
   997  	var testsmap map[string]TestSet
   998  	testsmap, err = LoadTestFiles(h.TestPath())
   999  	if err != nil {
  1000  		return
  1001  	}
  1002  	appPackage.TestSets = make([]AppPackageTests, 0)
  1003  	for name, t := range testsmap {
  1004  		appPackage.TestSets = append(appPackage.TestSets, AppPackageTests{Name: name, TestSet: t})
  1005  	}
  1006  
  1007  	var scenarioFiles map[string]*os.FileInfo
  1008  	scenarioFiles, err = GetTestScenarios(h)
  1009  	if err != nil {
  1010  		return
  1011  	}
  1012  	appPackage.Scenarios = make([]AppPackageScenario, 0)
  1013  	for name, _ := range scenarioFiles {
  1014  		scenarioPath := filepath.Join(h.TestPath(), name)
  1015  		var rolemap map[string]TestSet
  1016  		rolemap, err = LoadTestFiles(scenarioPath)
  1017  		if err != nil {
  1018  			return
  1019  		}
  1020  		roles := make([]AppPackageTests, 0)
  1021  		for name, tests := range rolemap {
  1022  			roles = append(roles,
  1023  				AppPackageTests{Name: name, TestSet: tests})
  1024  
  1025  		}
  1026  		scenario := AppPackageScenario{Name: name, Roles: roles}
  1027  		if FileExists(scenarioPath, TestConfigFileName) {
  1028  			var config *TestConfig
  1029  			config, err = LoadTestConfig(scenarioPath)
  1030  			if err != nil {
  1031  				return
  1032  			}
  1033  			scenario.Config = *config
  1034  		}
  1035  		appPackage.Scenarios = append(appPackage.Scenarios, scenario)
  1036  	}
  1037  
  1038  	var files []os.FileInfo
  1039  	files, err = ioutil.ReadDir(h.UIPath())
  1040  	if err != nil {
  1041  		return
  1042  	}
  1043  
  1044  	appPackage.UI = make([]AppPackageUIFile, 0)
  1045  	for _, f := range files {
  1046  		// TODO handle subdirectories
  1047  		if f.Mode().IsRegular() {
  1048  			var file []byte
  1049  			file, err = ReadFile(h.UIPath(), f.Name())
  1050  			if err != nil {
  1051  				return
  1052  			}
  1053  			uiFile := AppPackageUIFile{FileName: f.Name()}
  1054  			contentType := http.DetectContentType(file)
  1055  			if encodeAsBinary(contentType) {
  1056  				uiFile.Data = base64.StdEncoding.EncodeToString([]byte(file))
  1057  
  1058  				uiFile.Encoding = "base64"
  1059  			} else {
  1060  				uiFile.Data = string(file)
  1061  			}
  1062  			appPackage.UI = append(appPackage.UI, uiFile)
  1063  
  1064  		}
  1065  	}
  1066  
  1067  	data, err = json.MarshalIndent(appPackage, "", "  ")
  1068  	return
  1069  }
  1070  
  1071  func encodeAsBinary(contentType string) bool {
  1072  	if strings.HasPrefix(contentType, "text") {
  1073  		return false
  1074  	}
  1075  	return true
  1076  }
  1077  
  1078  // SaveFromAppPackage writes out a holochain application based on appPackage file to path
  1079  func (service *Service) SaveFromAppPackage(reader io.Reader, path string, name string, agent Agent, decodingFormat string, encodingFormat string, newUUID bool) (appPackage *AppPackage, err error) {
  1080  	appPackage, err = LoadAppPackage(reader, decodingFormat)
  1081  	if err != nil {
  1082  		return
  1083  	}
  1084  	err = service.saveFromAppPackage(appPackage, path, name, encodingFormat, newUUID)
  1085  	if err != nil {
  1086  		return
  1087  	}
  1088  	if agent == nil {
  1089  		agent = service.DefaultAgent
  1090  	}
  1091  	err = SaveAgent(path, agent)
  1092  	if err != nil {
  1093  		return
  1094  	}
  1095  	return
  1096  }
  1097  
  1098  func (service *Service) saveFromAppPackage(appPackage *AppPackage, path string, name string, encodingFormat string, newUUID bool) (err error) {
  1099  
  1100  	dna := &appPackage.DNA
  1101  	err = MakeDirs(path)
  1102  	if err != nil {
  1103  		return
  1104  	}
  1105  	if newUUID {
  1106  		dna.NewUUID()
  1107  	}
  1108  	dna.Name = name
  1109  
  1110  	err = service.saveDNAFile(path, dna, encodingFormat, false)
  1111  	if err != nil {
  1112  		return
  1113  	}
  1114  
  1115  	testPath := filepath.Join(path, ChainTestDir)
  1116  	for _, test := range appPackage.TestSets {
  1117  		p := filepath.Join(testPath, test.Name+".json")
  1118  		var f *os.File
  1119  		f, err = os.Create(p)
  1120  		if err != nil {
  1121  			return
  1122  		}
  1123  		defer f.Close()
  1124  		err = Encode(f, "json", test.TestSet)
  1125  		if err != nil {
  1126  			return
  1127  		}
  1128  	}
  1129  
  1130  	for _, scenario := range appPackage.Scenarios {
  1131  		scenarioPath := filepath.Join(testPath, scenario.Name)
  1132  		err = os.MkdirAll(scenarioPath, os.ModePerm)
  1133  		if err != nil {
  1134  			return
  1135  		}
  1136  		for _, role := range scenario.Roles {
  1137  			p := filepath.Join(scenarioPath, role.Name+".json")
  1138  			var f *os.File
  1139  			f, err = os.Create(p)
  1140  			if err != nil {
  1141  				return
  1142  			}
  1143  			defer f.Close()
  1144  			err = Encode(f, "json", &role.TestSet)
  1145  			if err != nil {
  1146  				return
  1147  			}
  1148  		}
  1149  		if scenario.Config.Duration != 0 {
  1150  			p := filepath.Join(scenarioPath, TestConfigFileName)
  1151  			var f *os.File
  1152  			f, err = os.Create(p)
  1153  			if err != nil {
  1154  				return
  1155  			}
  1156  			defer f.Close()
  1157  			err = Encode(f, "json", &scenario.Config)
  1158  		}
  1159  	}
  1160  
  1161  	p := filepath.Join(path, ChainUIDir)
  1162  	for _, ui := range appPackage.UI {
  1163  		var data []byte
  1164  		if ui.Encoding == "base64" {
  1165  			data, err = base64.StdEncoding.DecodeString(ui.Data)
  1166  			if err != nil {
  1167  				return
  1168  			}
  1169  		} else {
  1170  			data = []byte(ui.Data)
  1171  		}
  1172  
  1173  		if err = WriteFile(data, p, ui.FileName); err != nil {
  1174  			return
  1175  		}
  1176  	}
  1177  
  1178  	return
  1179  }
  1180  
  1181  //MakeDirs creates the directory structure of an application
  1182  func MakeDirs(devPath string) error {
  1183  	err := os.MkdirAll(devPath, os.ModePerm)
  1184  	if err != nil {
  1185  		return err
  1186  	}
  1187  	err = os.MkdirAll(filepath.Join(devPath, ChainDNADir), os.ModePerm)
  1188  	if err != nil {
  1189  		return err
  1190  	}
  1191  	err = os.MkdirAll(filepath.Join(devPath, ChainUIDir), os.ModePerm)
  1192  	if err != nil {
  1193  		return err
  1194  	}
  1195  	err = os.MkdirAll(filepath.Join(devPath, ChainTestDir), os.ModePerm)
  1196  	if err != nil {
  1197  		return err
  1198  	}
  1199  
  1200  	return nil
  1201  }
  1202  
  1203  // LoadTestFile unmarshals test json data
  1204  func LoadTestFile(dir string, file string) (tests TestSet, err error) {
  1205  	var v []byte
  1206  	v, err = ReadFile(dir, file)
  1207  	if err != nil {
  1208  		return
  1209  	}
  1210  	err = json.Unmarshal(v, &tests)
  1211  
  1212  	if err != nil {
  1213  		return
  1214  	}
  1215  	return
  1216  }
  1217  
  1218  // LoadTestConfig unmarshals test json data
  1219  func LoadTestConfig(dir string) (config *TestConfig, err error) {
  1220  	c := TestConfig{GossipInterval: 2000, Duration: 0}
  1221  	config = &c
  1222  	// if no config file return default values
  1223  	if !FileExists(dir, TestConfigFileName) {
  1224  		return
  1225  	}
  1226  	var v []byte
  1227  	v, err = ReadFile(dir, TestConfigFileName)
  1228  	if err != nil {
  1229  		return nil, err
  1230  	}
  1231  	err = json.Unmarshal(v, &c)
  1232  
  1233  	if err != nil {
  1234  		return nil, err
  1235  	}
  1236  	return
  1237  }
  1238  
  1239  // LoadTestFiles searches a path for .json test files and loads them into an array
  1240  func LoadTestFiles(path string) (map[string]TestSet, error) {
  1241  	files, err := ioutil.ReadDir(path)
  1242  	if err != nil {
  1243  		return nil, err
  1244  	}
  1245  
  1246  	re := regexp.MustCompile(`(.*)\.json`)
  1247  	var tests = make(map[string]TestSet)
  1248  	for _, f := range files {
  1249  		if f.Mode().IsRegular() {
  1250  			x := re.FindStringSubmatch(f.Name())
  1251  			if len(x) > 0 {
  1252  				if f.Name() != TestConfigFileName {
  1253  					name := x[1]
  1254  					tests[name], err = LoadTestFile(path, x[0])
  1255  					if err != nil {
  1256  						return nil, err
  1257  					}
  1258  				}
  1259  			}
  1260  		}
  1261  	}
  1262  
  1263  	if len(tests) == 0 {
  1264  		return nil, errors.New("no test files found in: " + path)
  1265  	}
  1266  
  1267  	return tests, err
  1268  }
  1269  
  1270  // TestScenarioList returns a list of paths to scenario directories
  1271  func GetTestScenarios(h *Holochain) (scenarios map[string]*os.FileInfo, err error) {
  1272  	dirContentList := []os.FileInfo{}
  1273  	scenarios = make(map[string]*os.FileInfo)
  1274  
  1275  	dirContentList, err = ioutil.ReadDir(h.TestPath())
  1276  	if err != nil {
  1277  		return scenarios, err
  1278  	}
  1279  	for _, fileOrDir := range dirContentList {
  1280  		if fileOrDir.Mode().IsDir() {
  1281  			scenarios[fileOrDir.Name()] = &fileOrDir
  1282  		}
  1283  	}
  1284  
  1285  	return scenarios, err
  1286  }
  1287  
  1288  // GetTestScenarioRoles returns a list of scenario roles
  1289  func GetTestScenarioRoles(h *Holochain, scenarioName string) (roleNameList []string, err error) {
  1290  	return GetAllTestRoles(filepath.Join(h.TestPath(), scenarioName))
  1291  }
  1292  
  1293  // GetAllTestRoles  retuns a list of the roles in a scenario
  1294  func GetAllTestRoles(path string) (roleNameList []string, err error) {
  1295  	roleNameList = []string{}
  1296  
  1297  	files, err := ioutil.ReadDir(path)
  1298  	if err != nil {
  1299  		return nil, err
  1300  	}
  1301  
  1302  	// ignore all files that start with an underscore, these are either the config file or
  1303  	// bridge specs
  1304  	re := regexp.MustCompile(`^([^_].*)\.json`)
  1305  	for _, f := range files {
  1306  		if f.Mode().IsRegular() {
  1307  			x := re.FindStringSubmatch(f.Name())
  1308  			if len(x) > 0 {
  1309  				roleNameList = append(roleNameList, x[1])
  1310  			}
  1311  		}
  1312  	}
  1313  	return
  1314  }
  1315  
  1316  const (
  1317  	TestingAppDecodingFormat = "json"
  1318  )
  1319  
  1320  func TestingAppAppPackage() string {
  1321  	return `{
  1322  "Version": "` + AppPackageVersion + `",
  1323  "Generator": "holochain service.go",
  1324  "DNA": {
  1325    "Version": 1,
  1326    "UUID": "00000000-0000-0000-0000-000000000000",
  1327    "Name": "testingApp",
  1328    "RequiresVersion": ` + VersionStr + `,
  1329    "Properties": {
  1330      "description": "a bogus test holochain",
  1331      "language": "en"
  1332    },
  1333    "PropertiesSchemaFile": "properties_schema.json",
  1334    "DHTConfig": {
  1335      "HashType": "sha2-256"
  1336    },
  1337    "Progenitor": {
  1338        "Identity": "Progenitor Agent <progenitore@example.com>",
  1339        "PubKey": [8, 1, 18, 32, 193, 43, 31, 148, 23, 249, 163, 154, 128, 25, 237, 167, 253, 63, 214, 220, 206, 131, 217, 74, 168, 30, 215, 237, 231, 160, 69, 89, 48, 17, 104, 210]
  1340    },
  1341    "Zomes": [
  1342      {
  1343        "Name": "zySampleZome",
  1344        "Description": "this is a zygomas test zome",
  1345        "RibosomeType": "zygo",
  1346        "BridgeFuncs" :["testStrFn1"],
  1347              "Entries": [
  1348                  {
  1349                      "Name": "evenNumbers",
  1350                      "DataFormat": "zygo",
  1351                      "Sharing": "public"
  1352                  },
  1353                  {
  1354                      "Name": "primes",
  1355                      "DataFormat": "json",
  1356                      "Schema": "` + jsSanitizeString(primesSchema) + `",
  1357                      "Sharing": "public"
  1358                  },
  1359                  {
  1360                      "Name": "profile",
  1361                      "DataFormat": "json",
  1362                      "Schema": "` + jsSanitizeString(profileSchema) + `",
  1363                      "Sharing": "public"
  1364                  },
  1365                  {
  1366                    "Name": "privateData",
  1367                    "DataFormat": "string",
  1368                    "Sharing": "private"
  1369                  }
  1370              ],
  1371              "Functions": [
  1372                  {
  1373                      "Name": "getDNA",
  1374                      "CallingType": "string",
  1375                      "Exposure": ""
  1376                  },
  1377                  {
  1378                      "Name": "addEven",
  1379                      "CallingType": "string",
  1380                      "Exposure": "public"
  1381                  },
  1382                  {
  1383                      "Name": "addPrime",
  1384                      "CallingType": "json",
  1385                      "Exposure": "public"
  1386                  },
  1387                  {
  1388                      "Name": "confirmOdd",
  1389                      "CallingType": "string",
  1390                      "Exposure": "public"
  1391                  },
  1392                  {
  1393                      "Name": "testStrFn1",
  1394                      "CallingType": "string",
  1395                      "Exposure": ""
  1396                  },
  1397                  {
  1398                      "Name": "testStrFn2",
  1399                      "CallingType": "string",
  1400                      "Exposure": ""
  1401                  },
  1402                  {
  1403                      "Name": "testJsonFn1",
  1404                      "CallingType": "json",
  1405                      "Exposure": ""
  1406                  },
  1407                  {
  1408                      "Name": "testJsonFn2",
  1409                      "CallingType": "json",
  1410                      "Exposure": ""
  1411                  },
  1412                  {
  1413                      "Name": "myIdentity",
  1414                      "CallingType": "string"
  1415                  }
  1416              ],
  1417        "Code": "` + jsSanitizeString(zygoZomeCode) + `"
  1418      },
  1419      {
  1420        "Name": "jsSampleZome",
  1421        "Description": "this is a javascript test zome",
  1422        "RibosomeType": "js",
  1423        "BridgeFuncs" :["getProperty"],
  1424              "Entries": [
  1425                  {
  1426                      "Name": "oddNumbers",
  1427                      "DataFormat": "js",
  1428                      "Sharing": "public"
  1429                  },
  1430                  {
  1431                      "Name": "profile",
  1432                      "DataFormat": "json",
  1433                      "Schema": "` + jsSanitizeString(profileSchema) + `",
  1434                      "Sharing": "public"
  1435                  },
  1436                  {
  1437                      "Name": "rating",
  1438                      "DataFormat": "links"
  1439                  },
  1440                  {
  1441                      "Name": "review",
  1442                      "DataFormat": "string",
  1443                      "Sharing": "public"
  1444                  },
  1445                  {
  1446                      "Name": "secret",
  1447                      "DataFormat": "string"
  1448                  }
  1449              ],
  1450              "Functions": [
  1451                  {
  1452                      "Name": "getProperty",
  1453                      "CallingType": "string",
  1454                      "Exposure": "public"
  1455                  },
  1456                  {
  1457                      "Name": "addOdd",
  1458                      "CallingType": "string",
  1459                      "Exposure": "public"
  1460                  },
  1461                  {
  1462                      "Name": "addProfile",
  1463                      "CallingType": "json",
  1464                      "Exposure": "public"
  1465                  },
  1466                  {
  1467                      "Name": "testStrFn1",
  1468                      "CallingType": "string",
  1469                      "Exposure": ""
  1470                  },
  1471                  {
  1472                      "Name": "testStrFn2",
  1473                      "CallingType": "string",
  1474                      "Exposure": ""
  1475                  },
  1476                  {
  1477                      "Name": "testJsonFn1",
  1478                      "CallingType": "json",
  1479                      "Exposure": ""
  1480                  },
  1481                  {
  1482                      "Name": "testJsonFn2",
  1483                      "CallingType": "json",
  1484                      "Exposure": ""
  1485                  },
  1486                  {
  1487                      "Name": "throwError",
  1488                      "CallingType": "string",
  1489                      "Exposure": "public"
  1490                  }
  1491              ],
  1492        "Code": "` + jsSanitizeString(jsZomeCode) + `"
  1493      }
  1494    ]},
  1495  "TestSets":[{"Name":"testSet1","TestSet":{
  1496  "Identity": "123-456-7890",
  1497  "Tests":[
  1498      {
  1499          "Zome":   "zySampleZome",
  1500          "FnName": "addEven",
  1501          "Input":  "2",
  1502          "Output": "%h%"
  1503      },
  1504      {
  1505          "Zome":   "zySampleZome",
  1506          "FnName": "addEven",
  1507          "Input":  "4",
  1508          "Output": "%h%"
  1509      },
  1510      {
  1511          "Zome":   "zySampleZome",
  1512          "FnName": "addEven",
  1513          "Input":  "5",
  1514          "Err":    "Error calling 'commit': Validation Failed: 5 is not even"
  1515      },
  1516      {
  1517          "Zome":   "zySampleZome",
  1518          "FnName": "addPrime",
  1519          "Input":  {"prime":7},
  1520          "Output": "%h%"
  1521      },
  1522      {
  1523          "Zome":   "zySampleZome",
  1524          "FnName": "addPrime",
  1525          "Input":  {"prime":4},
  1526          "Err":    "Error calling 'commit': Validation Failed"
  1527      },
  1528      {
  1529  	"Zome":   "jsSampleZome",
  1530  	"FnName": "addProfile",
  1531  	"Input":  {"firstName":"Art","lastName":"Brock"},
  1532  	"Output": "%h%"
  1533      },
  1534      {
  1535  	"Zome":   "zySampleZome",
  1536  	"FnName": "getDNA",
  1537  	"Input":  "",
  1538  	"Output": "%dna%"
  1539      },
  1540      {
  1541  	"Zome":     "zySampleZome",
  1542  	"FnName":   "getDNA",
  1543  	"Input":    "",
  1544  	"Err":      "function not available",
  1545  	"Exposure":  "public"
  1546      },
  1547      {
  1548  	"Zome":     "zySampleZome",
  1549  	"FnName":   "myIdentity",
  1550  	"Input":    "",
  1551  	"Output":   "123-456-7890"
  1552      }
  1553  ]}},
  1554  {"Name":"testSet2","TestSet":{
  1555  "Fixtures":{
  1556    "Agents":[{"Hash":"QmVGtdTZdTFaLsaj2RwdVG8jcjNNcp1DE914DKZ2kHmXHx","Identity":"agent@foo.com"}]
  1557  },
  1558  "Tests":[
  1559      {
  1560  	"Zome":   "jsSampleZome",
  1561  	"FnName": "addOdd",
  1562  	"Input":  "7",
  1563  	"Output": "%h%"
  1564      },
  1565      {
  1566  	"Zome":   "jsSampleZome",
  1567  	"FnName": "addOdd",
  1568  	"Input":  "2",
  1569  	"Err":    {"errorMessage":"Validation Failed: 2 is not odd","function":"commit","name":"` + HolochainErrorPrefix + `","source":{"column":"28","functionName":"addOdd","line":"45"}}
  1570      },
  1571      {
  1572  	"Zome":   "jsSampleZome",
  1573  	"FnName": "addOdd",
  1574  	"Input":  "2",
  1575  	"ErrMsg":  "Validation Failed: 2 is not odd"
  1576      },
  1577      {
  1578  	"Zome":   "zySampleZome",
  1579  	"FnName": "confirmOdd",
  1580  	"Input":  "9",
  1581  	"Output": "false"
  1582      },
  1583      {
  1584  	"Zome":   "zySampleZome",
  1585  	"FnName": "confirmOdd",
  1586  	"Input":  "7",
  1587  	"Output": "true"
  1588      },
  1589      {
  1590  	"Zome":   "jsSampleZome",
  1591  	"Input":  "unexposed(\"this is a\")",
  1592  	"Output": "this is a fish",
  1593  	"Raw":    true
  1594      },
  1595      {
  1596  	"Convey": "test the output of a function that returns json",
  1597  	"Zome":   "jsSampleZome",
  1598  	"FnName": "testJsonFn2",
  1599  	"Input": "",
  1600  	"Output": [{"a":"b"}]
  1601      },
  1602      {
  1603     	"Convey": "agent fixture substitution works",
  1604  	"Zome":   "jsSampleZome",
  1605  	"Input":  "\"%agent0%--%agent0_str%\"",
  1606  	"Output": "QmVGtdTZdTFaLsaj2RwdVG8jcjNNcp1DE914DKZ2kHmXHx--agent@foo.com",
  1607  	"Raw":    true
  1608      }
  1609  ]}}],
  1610  "UI":[
  1611  {"FileName":"index.html",
  1612   "Data":"` + jsSanitizeString(SampleHTML) + `"
  1613  },
  1614  {"FileName":"hc.js",
  1615   "Data":"` + jsSanitizeString(SampleJS) + `"
  1616  },
  1617  {"FileName":"logo.png",
  1618   "Data":"` + jsSanitizeString(SampleBinary) + `",
  1619   "Encoding":"base64"
  1620  }],
  1621  "Scenarios":[
  1622          {"Name":"sampleScenario",
  1623           "Roles":[
  1624               {"Name":"speaker",
  1625                "TestSet":{"Tests":[
  1626                    {"Convey":"add an odd",
  1627                     "Zome":   "jsSampleZome",
  1628  	           "FnName": "addOdd",
  1629  	           "Input":  "7",
  1630  	           "Output": "%h%"
  1631                    },
  1632                    {"Convey":"raw function with scenario substitutions that fails",
  1633                     "Zome":   "jsSampleZome",
  1634  	           "Raw": true,
  1635  	           "Input": "'foo'",
  1636  	           "Output":  "%server% %listener_hash% %listener_str%"
  1637                    }
  1638                 ]}},
  1639               {"Name":"listener",
  1640                "TestSet":{"Tests":[
  1641                    {"Convey":"confirm prime exists",
  1642                     "Zome":   "zySampleZome",
  1643  	           "FnName": "confirmOdd",
  1644  	           "Input":  "7",
  1645  	           "Output": "true",
  1646                     "Time" : 1500
  1647                    }
  1648                 ]}}
  1649            ],
  1650           "Config":{"Duration":5,"GossipInterval":300}}]
  1651  }
  1652  `
  1653  }
  1654  
  1655  const (
  1656  	profileSchema = `{
  1657  	"title": "Profile Schema",
  1658  	"type": "object",
  1659  	"properties": {
  1660  		"firstName": {
  1661  			"type": "string"
  1662  		},
  1663  		"lastName": {
  1664  			"type": "string"
  1665  		},
  1666  		"age": {
  1667  			"description": "Age in years",
  1668  			"type": "integer",
  1669  			"minimum": 0
  1670  		}
  1671  	},
  1672  	"required": ["firstName", "lastName"]
  1673  }`
  1674  
  1675  	primesSchema = `
  1676  {
  1677  	"title": "Prime Schema",
  1678  	"type": "object",
  1679  	"properties": {
  1680  		"prime": {
  1681  			"type": "integer"
  1682  		}
  1683  	},
  1684  	"required": ["prime"]
  1685  }`
  1686  
  1687  	jsZomeCode = `
  1688  function unexposed(x) {return x+" fish";};
  1689  function testStrFn1(x) {return "result: "+x};
  1690  function testStrFn2(x){ return parseInt(x)+2};
  1691  function testJsonFn1(x){ x.output = x.input*2; return x;};
  1692  function testJsonFn2(x){ return [{a:'b'}] };
  1693  
  1694  function getProperty(x) {return property(x)};
  1695  function addOdd(x) {return commit("oddNumbers",x);}
  1696  function addProfile(x) {return commit("profile",x);}
  1697  function throwError(x) {throw new Error(x)}
  1698  function validatePut(entry_type,entry,header,pkg,sources) {
  1699    return validate(entry_type,entry,header,sources);
  1700  }
  1701  function validateMod(entry_type,entry,header,replaces,pkg,sources) {
  1702    return true;
  1703  }
  1704  function validateDel(entry_type,hash,pkg,sources) {
  1705    return true;
  1706  }
  1707  function validateCommit(entry_type,entry,header,pkg,sources) {
  1708    if (entry_type == "rating") {return true}
  1709    return validate(entry_type,entry,header,sources);
  1710  }
  1711  function validate(entry_type,entry,header,sources) {
  1712    if (entry_type=="oddNumbers") {
  1713      return (entry%2 != 0) ? true : entry+" is not odd"
  1714    }
  1715    if (entry_type=="profile") {
  1716      return true
  1717    }
  1718    if (entry_type=="review") {
  1719      return true
  1720    }
  1721    if (entry_type=="secret") {
  1722      return true
  1723    }
  1724    if (entry_type=="rating") {
  1725      return true
  1726    }
  1727    return false
  1728  }
  1729  function validateLink(linkEntryType,baseHash,linkHash,tag,pkg,sources){return true}
  1730  function validatePutPkg(entry_type) {
  1731    req = {};
  1732    req[HC.PkgReq.Chain]=HC.PkgReq.ChainOpt.Full;
  1733    return req;
  1734  }
  1735  function validateModPkg(entry_type) { return null}
  1736  function validateDelPkg(entry_type) { return null}
  1737  function validateLinkPkg(entry_type) { return null}
  1738  
  1739  function genesis() {
  1740    debug("running jsZome genesis")
  1741    return true
  1742  }
  1743  function bridgeGenesis(side,app,data) {
  1744  testGetBridges();
  1745  return true
  1746  }
  1747  
  1748  function bundleCanceled(reason,userParam) {
  1749       debug(userParam+"debug message during bundleCanceled with reason: "+reason);
  1750    if (userParam == 'debugit') {
  1751       debug("debug message during bundleCanceled with reason: "+reason);
  1752    } else if (userParam == 'cancelit') {
  1753       debug("debug message during bundleCanceled: canceling cancel!");
  1754       return HC.BundleCancel.Response.Commit
  1755    }
  1756    return HC.BundleCancel.Response.OK
  1757  }
  1758  
  1759  function receive(from,message) {
  1760    // if the message requests blocking run an infinite loop
  1761    // this is used by the async send test to force the condition where
  1762    // the receiver doesn't return soon enough so that the send will timeout
  1763    if (message.block) {
  1764      while(true){};
  1765    }
  1766  
  1767    // send back a pong message of what came in the ping message!
  1768    return {pong:message.ping}
  1769  }
  1770  
  1771  function testGetBridges() {
  1772    debug("testGetBridges:"+JSON.stringify(getBridges()))
  1773  }
  1774  
  1775  function asyncPing(message,id) {
  1776    debug("async result of message with "+id+" was: "+JSON.stringify(message))
  1777  }
  1778  `
  1779  	zygoZomeCode = `
  1780  (defn testStrFn1 [x] (concat "result: " x))
  1781  (defn testStrFn2 [x] (+ (atoi x) 2))
  1782  (defn testJsonFn1 [x] (begin (hset x output: (* (-> x input:) 2)) x))
  1783  (defn testJsonFn2 [x] (unjson (raw "[{\"a\":\"b\"}]"))) (defn getDNA [x] App_DNA_Hash)
  1784  (defn addEven [x] (commit "evenNumbers" x))
  1785  (defn addPrime [x] (commit "primes" x))
  1786  (defn confirmOdd [x]
  1787    (letseq [h (makeHash "oddNumbers" x)
  1788             r (get h)
  1789             err (cond (hash? r) (hget r %error "") "found")]
  1790       (cond (== err "") "true" "false")
  1791    )
  1792  )
  1793  (defn validateCommit [entryType entry header pkg sources]
  1794    (validate entryType entry header sources))
  1795  (defn validatePut [entryType entry header pkg sources]
  1796    (validate entryType entry header sources))
  1797  (defn validateMod [entryType entry header replaces pkg sources] true)
  1798  (defn validateDel [entryType hash pkg sources] true)
  1799  (defn validate [entryType entry header sources]
  1800    (cond (== entryType "evenNumbers")  (cond (== (mod entry 2) 0) true (concat (str entry) " is not even"))
  1801          (== entryType "primes")  (isprime (hget entry %prime))
  1802          (== entryType "profile") ""
  1803          false)
  1804  )
  1805  (defn validateLink [linkEntryType baseHash links pkg sources] true)
  1806  (defn validatePutPkg [entryType] nil)
  1807  (defn validateModPkg [entryType] nil)
  1808  (defn validateDelPkg [entryType] nil)
  1809  (defn validateLinkPkg [entryType] nil)
  1810  (defn genesis []
  1811    (debug "running zyZome genesis")
  1812    true
  1813  )
  1814  (defn bridgeGenesis [side app data] (begin (debug (concat "bridge genesis " (cond (== side HC_Bridge_Caller) "from" "to") "-- other side is:" app " bridging data:" data))  true))
  1815  (defn receive [from message]
  1816  	(hash pong: (hget message %ping)))
  1817  
  1818  (defn testGetBridges []
  1819    (debug (str (getBridges))))
  1820  
  1821  (defn asyncPing [message,id]
  1822    (debug (concat "async result of message with " id " was:" (str message)))
  1823  )
  1824  
  1825  (defn myIdentity [x] App_Agent_String)
  1826  `
  1827  
  1828  	SampleHTML = `
  1829  <html>
  1830    <head>
  1831      <title>Test</title>
  1832      <script type="text/javascript" src="http://code.jquery.com/jquery-latest.js"></script>
  1833      <script type="text/javascript" src="/hc.js">
  1834      </script>
  1835    </head>
  1836    <body>
  1837      <img src="logo.png">
  1838      <select id="zome" name="zome">
  1839        <option value="zySampleZome">zySampleZome</option>
  1840        <option value="jsSampleZome">jsSampleZome</option>
  1841      </select>
  1842      <select id="fn" name="fn">
  1843        <option value="addEven">addEven</option>
  1844        <option value="getProperty">getProperty</option>
  1845        <option value="addPrime">addPrime</option>
  1846      </select>
  1847      <input id="data" name="data">
  1848      <button onclick="send();">Send</button>
  1849      send an even number and get back a hash, send and odd end get a error
  1850  
  1851      <div id="result"></div>
  1852      <div id="err"></div>
  1853    </body>
  1854  </html>`
  1855  	SampleJS = `
  1856       function send() {
  1857           $.post(
  1858               "/fn/"+$('select[name=zome]').val()+"/"+$('select[name=fn]').val(),
  1859               $('#data').val(),
  1860               function(data) {
  1861                   $("#result").html("result:"+data)
  1862                   $("#err").html("")
  1863               }
  1864           ).error(function(response) {
  1865               $("#err").html(response.responseText)
  1866               $("#result").html("")
  1867           })
  1868           ;
  1869       };
  1870  `
  1871  	SampleBinary = `iVBORw0KGgoAAAANSUhEUgAAANIAAAC0CAYAAADhNHIFAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJ
  1872  bWFnZVJlYWR5ccllPAAAClxJREFUeNrsnU1u004Yhw3qEqnZwoYgsSecgCCxRKIsum9P0HIDumbB
  1873  xwXSNZuEE6TZIzUcACWskUh7Av/9i5jK+O8k/nhnbLfPI40obdp4Pp53bM87zr04jj9GUTSIAKAy
  1874  e38lGtIUANW5TxMAIBIAIgEgEgAgEgAiASASACIBACIBIBIAIgEgEgAgEgAiASASACIBACIBhGOP
  1875  JijMeVK+JeUqKW+ScpSUXtcrNZ/Po4uLi+j6+jra39+PBoNBNBwO6e2yxHE8jWEji8UiTgbWNGkq
  1876  ldXfMn306NHo58+fq67Wazqdxv1+P9YQyBZ9fzwe0/klQKQtjEajuNfr5Q42Ff1Mr+kakmRTndLl
  1877  9PSUQYBI9SN2kcGm0qXovVqttgaHLtetSbjZsIGzs7PCrz0+Po6urq66caF3fl7qWMu0A3ft4B+W
  1878  y+X6ArwoGpiTyaQTdZvNZqVvRqg9AJEqieR7gDZFlZkTkRCp1fIBIgEAIgEgEgAiASASACASACIB
  1879  IBIAIgEAIgEgEgAiASASACASACIBIBIAIgEAIgEgEgAiASASACASACIBIBIAIBIAIgEgEgAiAQAi
  1880  ASASACIBIBIAIBIAIgEgEgAiAQAiASASACIBIBIAIBJAYPas/+B8Po++ffsW7e/vR4PB4Ob7+rrX
  1881  69HigEjbuLq6it6+fRtdXFxsfV2/318XifX48eP1v8Ph8NY0qNrh3bt366Dx4sWLdd2aDiDL5XLd
  1882  5lZBUri6tYBlMuaWv3//jh48eNB/+PBhPx3AgxHH8TQ2IGnUWH+uakkqH79//z6+vLyMQzEej+Oj
  1883  o6M4Gejrov+L6XRa+vhVf/Hx48f//ezg4CAejUbxarUKUi+9j45DbeqOQXWq2k/ud9VW6e+rzfQ9
  1884  9/MQaHycnp6u66H3zzveJGjc9GUoTETSIKkjUZ5U+pu+BpmEVWNvkqGOSPrbm17jBt5isfA2yLKD
  1885  3RUdV12Rtv2u2tNXsNjWZ0WOuzMiKeJaipQeeIqsFmjwbhpkoURKF0uhdLy7BPEtUrrP3HtZCbRp
  1886  5ikSkDslUtWKFi2KRFWjSxGBmhDJFZ2mVI3imoGKihFKJKvTKwVQi3Hla/bPct/qAtv3xfLLly/X
  1887  NzPKvNenT5+i58+fR+fn5629OaFjfPLkSTSZTEq19/Hx8bpuu27uNHmDQ/2lftPXZW5oqF66YWMx
  1888  rsq8951ZR9Jg06DbNXjUAepAq84IcadPg65IoFDd1QZtDg7Z45UYChi7ODs7W79WMlkK3RmRLG6t
  1889  lhl0TpJNKFq3NVLXCRQaYKp7F4JD3pLAtmNX3ZPTT/P3DrX0YCJScg3SyCnRpo7p8sKvCxSKznn1
  1890  6nLd3GyaN+P4qleoIG8i0snJSdTEIpg7bch2THKh2sjxWKLonD3V06BILvw7Hyjyrlu1uDsajcwl
  1891  CjUOTERSx6qDm5iZ3I2I9MW6jufy8rKR47E+1cterGtgdF0md/qtkj2zkUxWdbMWM8jNBlVeB75Y
  1892  LNbRNGRqjLtYz0Y5HU9omZT2ZIm7i5WedSWT2jn0rGvdn+qvPJksAkUyBi+CpjBZpQjtWu/QWoRW
  1893  vrWeofWIsqvUUYl1maqZF26tpc46ktaE0qk5keHidDZ9Su9VdI3M/W6ddSS93+Hh4Sr53ioyzmTJ
  1894  rqXpeCu24+LDhw9h84OsFmTrZBto4c1aKg2usjJpgLnOrCNSOtfNR8ZHXurUtkVgl7pTJycyuxj+
  1895  58+fVZ2Mg6Iy6f/b2lB10891LF++fFl8/fpVB3rZxFhuVKRsmotlJN8U5bIDKS/psq5IeQmklsEi
  1896  L21KsqQH9qZ8RQuR0oGwbrLyrlnX1c21n16js45QGQudE6lIdK0iU16D63vbUo4sRcrWzSqK5826
  1897  LlhsSzmyFCmdRW9Vr3QWfpdonUg1z48LR7lds6MPkayjeN6s62O7S5E8x12nYRazLiK1YHYq0zE+
  1898  RUonZVoFijIJvVUGe5m/bzk76VhD7eG61SK52cnq+kKDvci5tV5jcbcwZN2Kbl3Qe5YZ6FW2IljO
  1899  TnUy/xHJY8cU3eOkztN1iORzJe90Ux2t11WNnPo9y1O9ooNOM4fadFPdVK+6+4qyN0DqXhO2eXZq
  1900  lUiaCdR5KnkDwup0KO+2sG/cGppK3qxYdD0oCrxpsEggUL+oXnntaTnruk2DbRSqFSK56F/kdEWv
  1901  tVy/8LlF2m39zh6v/p836Ky37Pt8noILetm6pdfj0rJZLm04odp0C7wRkdSwOrXQdcWuaJXXMZZ3
  1902  9bIDT8dV5zStaL023QApew1TNFjomOrUzQWx7ENVfGRhlCmvXr1aHR4eThOxRt+/f1d0amRB9p7a
  1903  R8m31rlvyg1TsuWvX79ukktV9P2y+2mUxZsMgn9yy9x2A8tNYLuOYVtKftX9Ty5RM5uIq9xBX3VL
  1904  PxJtV06b6uX6s0rSaDbXUfuSimzyq8PTp0+Xr1+/7mtXQrC9cpYzUpGHcFje5vUV5UKXKukxXSl1
  1905  ch+7tLhrJlKoxsm7tlBndX3A6fQrb+H4NtQt746bj1PYTTJ1RqQq6y7WMlneam2qbMs1u62zro9r
  1906  3WwJMSuZiNRE1PSdWtRU0emc78Xbpsqm9Tvf48fqOXveRQo9eDclbLoo1+XToW3R03LxtonZdtvt
  1907  autljU6KFLJDijaKOqZLEbxMzpzlwnSIkreE4TuDBZG2nGOXzeLuyuxU5WmrXTiNrfrIac3KlkEw
  1908  RAaLiUg+Tzcsnv9tvQHNMlLXzTywerRviDt1ZYOg1f6tEBkQJiJZbnfwmVflc50rtEC+Bl3b8vzK
  1909  PLu97PV060RSR1p1otsi7TMx0XVO6IHn+7OEfGxrLxr0fG//rvLRLlU2Pja+IFvnrositAZAE0mI
  1910  6Q8b87HIqr8d8kPG0tdQ7olNvuRxuYlt7LOiNzhamWunHDE9alcPNszm06Vz1fS8Mfexl216Iqry
  1911  yVRms1nph9QrZy2Jyuuv9bGQu3LzQqOcOdXt+vr6Ji/Q5T9uI/1suDb2m+szl9Opzy7WcYY+Pi9J
  1912  q11HA00JsWVQ5+nBhnA32aMJ7ibZ2ahtMygiQavRabceE1x0K8vBwYHp87hvK/dpgrvF58+fS+0H
  1913  k3ih9nwhEgAiAQAiASASACIBACIBIBIAIgEgEgAgEgAiASASACIBACIBIBIAIgEgEgAgEgAiASAS
  1914  ACIBACIBIBIAIgEgEgAgEgAiASASACASACIBIBIAIgEAIgEgEgAiASASACASACIBIBIAIgEAIgEg
  1915  UhMMBoOo1+uV+p03b950om7D4bD075RtC0SCm4EzGo0Kv77f70dHR0edqNvJyck6UBTl4OCg1OsR
  1916  Cf43gMbj8VqSXRF+Op12JmrrOHW8qt8uTk9PSwWUu8y9OI6nGg80xWYmk0k0m82i+Xx+MxgVpXU6
  1917  1+VovVwu13X78ePH+ms3uz579mwt2q4gAogEwKkdACIBIBIAIBIAIgEgEgAiAQAiASASACIBIBIA
  1918  IBIAIgEgEgAiAQAiASASACIBIBIAIBIAIgEgEgAgEgAiAbSGvaTMaQaAevwnwAB7EAC08iWPKwAA
  1919  AABJRU5ErkJggg==
  1920  `
  1921  )