decred.org/dcrdex@v1.0.5/tatanka/cmd/demo/main.go (about)

     1  package main
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"net"
    10  	"os"
    11  	"os/signal"
    12  	"os/user"
    13  	"path/filepath"
    14  	"sync"
    15  	"time"
    16  
    17  	"decred.org/dcrdex/dex"
    18  	dexbtc "decred.org/dcrdex/dex/networks/btc"
    19  	"decred.org/dcrdex/server/comms"
    20  	"decred.org/dcrdex/tatanka"
    21  	"decred.org/dcrdex/tatanka/chain/utxo"
    22  	tankaclient "decred.org/dcrdex/tatanka/client"
    23  	"decred.org/dcrdex/tatanka/mj"
    24  	"decred.org/dcrdex/tatanka/tanka"
    25  	"decred.org/dcrdex/tatanka/tcp"
    26  	"github.com/decred/dcrd/dcrec/secp256k1/v4"
    27  )
    28  
    29  var (
    30  	logMaker      *dex.LoggerMaker
    31  	usr, _        = user.Current()
    32  	dextestDir    = filepath.Join(usr.HomeDir, "dextest")
    33  	decredRPCPath = filepath.Join(dextestDir, "dcr", "alpha", "rpc.cert")
    34  	chains        = []tatanka.ChainConfig{
    35  		{
    36  			Symbol: "dcr",
    37  			Config: mustEncode(&utxo.DecredConfigFile{
    38  				RPCUser:   "user",
    39  				RPCPass:   "pass",
    40  				RPCListen: "127.0.0.1:19561",
    41  				RPCCert:   decredRPCPath,
    42  			}),
    43  		},
    44  		{
    45  			Symbol: "btc",
    46  			Config: mustEncode(&utxo.BitcoinConfigFile{
    47  				RPCConfig: dexbtc.RPCConfig{
    48  					RPCUser: "user",
    49  					RPCPass: "pass",
    50  					RPCBind: "127.0.0.1",
    51  					RPCPort: 20556,
    52  				},
    53  			}),
    54  		},
    55  	}
    56  )
    57  
    58  func main() {
    59  	if err := mainErr(); err != nil {
    60  		fmt.Fprintln(os.Stderr, err)
    61  	}
    62  }
    63  
    64  func mainErr() (err error) {
    65  	var wg sync.WaitGroup
    66  	defer func() {
    67  		wg.Wait()
    68  	}()
    69  
    70  	ctx, cancel := context.WithCancel(context.Background())
    71  	defer cancel()
    72  
    73  	killChan := make(chan os.Signal, 1)
    74  	signal.Notify(killChan, os.Interrupt)
    75  	go func() {
    76  		<-killChan
    77  		fmt.Println("Shutting down...")
    78  		cancel()
    79  	}()
    80  
    81  	tmpDir, _ := os.MkdirTemp("", "")
    82  	defer os.RemoveAll(tmpDir)
    83  
    84  	logMaker, err = dex.NewLoggerMaker(os.Stdout, dex.LevelTrace.String())
    85  	if err != nil {
    86  		return fmt.Errorf("failed to create custom logger: %w", err)
    87  	}
    88  
    89  	comms.UseLogger(logMaker.NewLogger("COMMS", dex.LevelTrace))
    90  
    91  	addrs, err := findOpenAddrs(2)
    92  	if err != nil {
    93  		return fmt.Errorf("findOpenAddrs error: %w", err)
    94  	}
    95  
    96  	genKey := func(dir string) (*secp256k1.PrivateKey, tanka.PeerID) {
    97  		priv, _ := secp256k1.GeneratePrivateKey()
    98  		var peerID tanka.PeerID
    99  		copy(peerID[:], priv.PubKey().SerializeCompressed())
   100  		os.MkdirAll(dir, 0755)
   101  		os.WriteFile(filepath.Join(dir, "priv.key"), priv.Serialize(), 0644)
   102  		return priv, peerID
   103  	}
   104  	dir0 := filepath.Join(tmpDir, "tatanka1")
   105  	priv0, pid0 := genKey(dir0)
   106  	dir1 := filepath.Join(tmpDir, "tatanka2")
   107  	priv1, pid1 := genKey(dir1)
   108  
   109  	wg.Add(1)
   110  	go func() {
   111  		defer wg.Done()
   112  		defer cancel()
   113  
   114  		runServer(ctx, dir0, addrs[0], addrs[1], priv1.PubKey().SerializeCompressed(), true)
   115  	}()
   116  
   117  	time.Sleep(time.Second)
   118  
   119  	wg.Add(1)
   120  	go func() {
   121  		defer wg.Done()
   122  		defer cancel()
   123  
   124  		runServer(ctx, dir1, addrs[1], addrs[0], priv0.PubKey().SerializeCompressed(), true)
   125  	}()
   126  
   127  	time.Sleep(time.Second)
   128  
   129  	cl1, err := newClient(ctx, addrs[0].String(), pid0, 0)
   130  	if err != nil {
   131  		return fmt.Errorf("error making first connected client: %v", err)
   132  	}
   133  
   134  	cl2, err := newClient(ctx, addrs[1].String(), pid1, 1)
   135  	if err != nil {
   136  		return fmt.Errorf("error making second connected client: %v", err)
   137  	}
   138  
   139  	// cm.Disconnect()
   140  
   141  	if err := cl1.SubscribeMarket(42, 0); err != nil {
   142  		return fmt.Errorf("SubscribeMarket error: %v", err)
   143  	}
   144  
   145  	// Miss the relayed new subscription broadcast.
   146  	time.Sleep(time.Second)
   147  
   148  	if err := cl2.SubscribeMarket(42, 0); err != nil {
   149  		return fmt.Errorf("SubscribeMarket error: %v", err)
   150  	}
   151  
   152  	// Client 1 should receive a notification.
   153  	select {
   154  	case bcastI := <-cl1.Next():
   155  		bcast, is := bcastI.(mj.Broadcast)
   156  		if !is {
   157  			return fmt.Errorf("expected new subscription Broadcast, got %T", bcastI)
   158  		}
   159  		if bcast.MessageType != mj.MessageTypeNewSubscriber {
   160  			return fmt.Errorf("expected new subscription message type, got %s", bcast.MessageType)
   161  		}
   162  		fmt.Printf("Client 1's bounced broadcast received: %+v \n", bcastI)
   163  	case <-time.After(time.Second):
   164  		return errors.New("timed out waiting for client 1 to receive new subscriber notification")
   165  	}
   166  
   167  	mktName, err := dex.MarketName(42, 0)
   168  	if err != nil {
   169  		return fmt.Errorf("error constructing market name: %w", err)
   170  	}
   171  
   172  	if err := cl1.Broadcast(mj.TopicMarket, tanka.Subject(mktName), mj.MessageTypeTrollBox, &mj.Troll{Msg: "trollin'"}); err != nil {
   173  		return fmt.Errorf("broadcast error: %w", err)
   174  	}
   175  
   176  	select {
   177  	case bcastI := <-cl1.Next():
   178  		bcast, is := bcastI.(mj.Broadcast)
   179  		if !is {
   180  			return fmt.Errorf("client 1 expected trollbox Broadcast bounceback, got %T", bcastI)
   181  		}
   182  		if bcast.MessageType != mj.MessageTypeTrollBox {
   183  			return fmt.Errorf("client 1 expected trollbox message type for bounced message, got %s", bcast.MessageType)
   184  		}
   185  		fmt.Printf("Client 1's bounced broadcast received: %+v \n", bcastI)
   186  	case <-time.After(time.Second):
   187  		return errors.New("timed out waiting for client 1 broadcast to bounce back")
   188  	}
   189  
   190  	select {
   191  	case bcastI := <-cl2.Next():
   192  		bcast, is := bcastI.(mj.Broadcast)
   193  		if !is {
   194  			return fmt.Errorf("client 2 expected trollbox Broadcast, got %T", bcastI)
   195  		}
   196  		if bcast.MessageType != mj.MessageTypeTrollBox {
   197  			return fmt.Errorf("client 2 expected trollbox message type, got %s", bcast.MessageType)
   198  		}
   199  		fmt.Printf("Client 2's received the broadcast: %+v \n", bcastI)
   200  	case <-time.After(time.Second):
   201  		return errors.New("timed out waiting for client 2 to receive broadcast from client 1")
   202  	}
   203  
   204  	// Connect clients
   205  	if _, err := cl1.ConnectPeer(cl2.ID()); err != nil {
   206  		return fmt.Errorf("error connecting peers: %w", err)
   207  	}
   208  
   209  	select {
   210  	case newPeerI := <-cl2.Next():
   211  		if _, is := newPeerI.(*tankaclient.IncomingPeerConnect); !is {
   212  			return fmt.Errorf("expected IncomingPeerConnect, got %T", newPeerI)
   213  		}
   214  		fmt.Printf("Client 2's received the new peer notification: %+v \n", newPeerI)
   215  	case <-time.After(time.Second):
   216  		return errors.New("timed out waiting for client 2 to receive broadcast from client 1")
   217  	}
   218  
   219  	// Send a tankagram
   220  	const testRoute = "test_route"
   221  	respC := make(chan interface{})
   222  	go func() {
   223  		msg := mj.MustRequest(testRoute, true)
   224  		r, resB, err := cl1.SendTankagram(cl2.ID(), msg)
   225  		if r.Result != mj.TRTTransmitted {
   226  			respC <- fmt.Errorf("not transmitted. %q", r.Result)
   227  			return
   228  		}
   229  		if err != nil {
   230  			respC <- err
   231  			return
   232  		}
   233  		respC <- resB
   234  	}()
   235  
   236  	select {
   237  	case gramI := <-cl2.Next():
   238  		gram, is := gramI.(*tankaclient.IncomingTankagram)
   239  		if !is {
   240  			return fmt.Errorf("expected IncomingTankagram, got a %T", gramI)
   241  		}
   242  		if gram.Msg.Route != testRoute || !bytes.Equal(gram.Msg.Payload, []byte("true")) {
   243  			return fmt.Errorf("tankagram ain't right %s, %s", gram.Msg.Route, string(gram.Msg.Payload))
   244  		}
   245  		gram.Respond("ok")
   246  	case <-time.After(time.Second):
   247  		return errors.New("timed out waiting for tankagram from client 1")
   248  	}
   249  
   250  	select {
   251  	case respI := <-respC:
   252  		switch resp := respI.(type) {
   253  		case error:
   254  			return fmt.Errorf("error sending tankagram: %v", resp)
   255  		case json.RawMessage:
   256  			var s string
   257  			if err := json.Unmarshal(resp, &s); err != nil {
   258  				return fmt.Errorf("tankagram response didn't unmarshal: %w", err)
   259  			}
   260  			if s != "ok" {
   261  				return fmt.Errorf("wrong tankagram response %q", s)
   262  			}
   263  		}
   264  	case <-time.After(time.Second):
   265  		return errors.New("timed out waiting for SendTankagram to return")
   266  	}
   267  
   268  	// Fiat rate live test requires internet connection.
   269  	fmt.Println("Testing fiat rates...")
   270  	if err := cl1.SubscribeToFiatRates(); err != nil {
   271  		return err
   272  	}
   273  
   274  	// Wait for rate message.
   275  	<-cl1.Next()
   276  
   277  	want := len(chains)
   278  	got := 0
   279  	for _, c := range chains {
   280  		assetID, _ := dex.BipSymbolID(c.Symbol)
   281  		fiatRate := cl1.FiatRate(assetID) // check if rate has been cached.
   282  		if fiatRate != 0 {
   283  			got++
   284  			fmt.Printf("\nRate found for %s\n", dex.BipIDSymbol(assetID))
   285  		}
   286  	}
   287  
   288  	if got < want {
   289  		fmt.Printf("\nGot fiat rates for %d out of %d assets\n", got, want)
   290  	}
   291  
   292  	fmt.Println("!!!!!!!! Test Success !!!!!!!!")
   293  
   294  	cancel()
   295  
   296  	wg.Wait()
   297  
   298  	cl1.cm.Wait()
   299  	cl2.cm.Wait()
   300  
   301  	return nil
   302  }
   303  
   304  type connectedClient struct {
   305  	*tankaclient.TankaClient
   306  	cm *dex.ConnectionMaster
   307  }
   308  
   309  func findOpenAddrs(n int) ([]net.Addr, error) {
   310  	addrs := make([]net.Addr, 0, n)
   311  	for i := 0; i < n; i++ {
   312  		addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
   313  		if err != nil {
   314  			return nil, err
   315  		}
   316  
   317  		l, err := net.ListenTCP("tcp", addr)
   318  		if err != nil {
   319  			return nil, err
   320  		}
   321  		defer l.Close()
   322  		addrs = append(addrs, l.Addr())
   323  	}
   324  
   325  	return addrs, nil
   326  }
   327  
   328  func runServer(ctx context.Context, dir string, addr, peerAddr net.Addr, peerID []byte, disableMessariFiatRateSource bool) {
   329  	n := newBootNode(peerAddr.String(), peerID)
   330  
   331  	log := logMaker.Logger(fmt.Sprintf("SRV[%s]", addr))
   332  
   333  	os.MkdirAll(dir, 0755)
   334  
   335  	ttCfg := &tatanka.ConfigFile{Chains: chains}
   336  	rawCfg, _ := json.Marshal(ttCfg)
   337  	cfgPath := filepath.Join(dir, "config.json")
   338  	if err := os.WriteFile(cfgPath, rawCfg, 0644); err != nil {
   339  		log.Errorf("WriteFile error: %v", err)
   340  		return
   341  	}
   342  
   343  	cfg := &tatanka.Config{
   344  		Net:     dex.Simnet,
   345  		DataDir: dir,
   346  		Logger:  log,
   347  		RPC: comms.RPCConfig{
   348  			ListenAddrs: []string{addr.String()},
   349  			NoTLS:       true,
   350  		},
   351  		ConfigPath: cfgPath,
   352  		WhiteList:  []tatanka.BootNode{n},
   353  	}
   354  
   355  	if disableMessariFiatRateSource {
   356  		// Requesting multiple rates from the same IP can trigger a 429 HTTP
   357  		// error.
   358  		cfg.FiatOracleConfig.DisabledFiatSources = "Messari"
   359  	}
   360  
   361  	t, err := tatanka.New(cfg)
   362  	if err != nil {
   363  		log.Errorf("error creating Tatanka node: %v", err)
   364  		return
   365  	}
   366  
   367  	cm := dex.NewConnectionMaster(t)
   368  	if err := cm.ConnectOnce(ctx); err != nil {
   369  		log.Errorf("ConnectOnce error: %v", err)
   370  		return
   371  	}
   372  
   373  	cm.Wait()
   374  }
   375  
   376  func newBootNode(addr string, peerID []byte) tatanka.BootNode {
   377  	tcpCfg, _ := json.Marshal(&tcp.RemoteNodeConfig{
   378  		URL: "ws://" + addr,
   379  		// Cert: ,
   380  	})
   381  	return tatanka.BootNode{
   382  		Protocol: "ws",
   383  		Config:   tcpCfg,
   384  		PeerID:   peerID,
   385  	}
   386  }
   387  
   388  func newClient(ctx context.Context, addr string, peerID tanka.PeerID, i int) (*connectedClient, error) {
   389  	log := logMaker.NewLogger(fmt.Sprintf("tCL[%d:%s]", i, addr), dex.LevelTrace)
   390  	priv, _ := secp256k1.GeneratePrivateKey()
   391  
   392  	tc := tankaclient.New(&tankaclient.Config{
   393  		Logger:     log.SubLogger("tTC"),
   394  		PrivateKey: priv,
   395  	})
   396  
   397  	cm := dex.NewConnectionMaster(tc)
   398  	if err := cm.ConnectOnce(ctx); err != nil {
   399  		return nil, fmt.Errorf("ConnectOnce error: %w", err)
   400  	}
   401  
   402  	if err := tc.AddTatankaNode(ctx, peerID, "ws://"+addr, nil); err != nil {
   403  		cm.Disconnect()
   404  		return nil, fmt.Errorf("error adding server %q", addr)
   405  	}
   406  
   407  	if err := tc.PostBond(&tanka.Bond{
   408  		PeerID:     tc.ID(),
   409  		AssetID:    42,
   410  		CoinID:     nil,
   411  		Strength:   1,
   412  		Expiration: time.Now().Add(time.Hour * 24 * 365),
   413  	}); err != nil {
   414  		cm.Disconnect()
   415  		return nil, fmt.Errorf("PostBond error: %v", err)
   416  	}
   417  
   418  	if err := tc.Auth(peerID); err != nil {
   419  		cm.Disconnect()
   420  		return nil, fmt.Errorf("auth error: %v", err)
   421  	}
   422  
   423  	return &connectedClient{
   424  		TankaClient: tc,
   425  		cm:          cm,
   426  	}, nil
   427  }
   428  
   429  func mustEncode(thing interface{}) json.RawMessage {
   430  	b, err := json.Marshal(thing)
   431  	if err != nil {
   432  		panic("mustEncode: " + err.Error())
   433  	}
   434  	return b
   435  }