1// SPDX-License-Identifier: ISC
2// Copyright (c) 2014-2020 Bitmark Inc.
3// Use of this source code is governed by an ISC
4// license that can be found in the LICENSE file.
5
6package main
7
8import (
9	"fmt"
10	"io"
11	"os"
12	"path"
13	"strings"
14
15	"github.com/urfave/cli"
16
17	"github.com/bitmark-inc/bitmarkd/command/bitmark-cli/configuration"
18)
19
20type metadata struct {
21	file             string
22	config           *configuration.Configuration
23	connectionOffset int
24	save             bool
25	testnet          bool
26	verbose          bool
27	e                io.Writer
28	w                io.Writer
29}
30
31// set by the linker: go build -ldflags "-X main.version=M.N" ./...
32var version = "zero" // do not change this value
33
34func main() {
35
36	app := cli.NewApp()
37	app.Name = "bitmark-cli"
38	app.Usage = "connect to bitmark network to send transactions and perform queries"
39	app.Version = version
40	app.HideVersion = true
41
42	app.Writer = os.Stdout
43	app.ErrWriter = os.Stderr
44
45	app.Flags = []cli.Flag{
46		cli.BoolFlag{
47			Name:  "verbose, v",
48			Usage: " verbose result",
49		},
50		cli.BoolFlag{
51			Name:  "version, V",
52			Usage: " show version",
53		},
54		cli.StringFlag{
55			Name:  "network, n",
56			Value: "",
57			Usage: " connect to bitmark `NETWORK` [bitmark|testing|local]",
58		},
59		cli.IntFlag{
60			Name:  "connection, c",
61			Value: 0,
62			Usage: " connection offset `N` [0]",
63		},
64		cli.StringFlag{
65			Name:  "identity, i",
66			Value: "",
67			Usage: " identity `NAME` [default identity]",
68		},
69		cli.StringFlag{
70			Name:  "password, p",
71			Value: "",
72			Usage: " identity `PASSWORD`",
73		},
74		cli.StringFlag{
75			Name:  "use-agent, u",
76			Value: "",
77			Usage: " executable program that returns the password `EXE`",
78		},
79		cli.BoolFlag{
80			Name:  "zero-agent-cache, z",
81			Usage: " force re-entry of agent password",
82		},
83	}
84	app.Commands = []cli.Command{
85		{
86			Name:      "setup",
87			Usage:     "initialise bitmark-cli configuration",
88			ArgsUsage: "\n   (* = required, + = select one)",
89			Flags: []cli.Flag{
90				cli.StringFlag{
91					Name:  "connect, c",
92					Value: "",
93					Usage: "*bitmarkd host/IP and port, `HOST:PORT`",
94				},
95				cli.StringFlag{
96					Name:  "description, d",
97					Value: "",
98					Usage: "*identity description `STRING`",
99				},
100				cli.BoolFlag{
101					Name:  "new, n",
102					Usage: "+generate a new seed and account",
103				},
104				cli.StringFlag{
105					Name:  "seed, s",
106					Value: "",
107					Usage: "+recover account from existing `SEED`",
108				},
109			},
110			Action: runSetup,
111		},
112		{
113			Name:      "add",
114			Usage:     "add a new identity to config file",
115			ArgsUsage: "\n   (* = required, + = select one )",
116			Flags: []cli.Flag{
117				cli.StringFlag{
118					Name:  "description, d",
119					Value: "",
120					Usage: "*identity description `STRING`",
121				},
122				cli.BoolFlag{
123					Name:  "new, n",
124					Usage: "+generate a new seed and account",
125				},
126				cli.StringFlag{
127					Name:  "seed, s",
128					Value: "",
129					Usage: "+recover account from existing `SEED`",
130				},
131				cli.StringFlag{
132					Name:  "account, a",
133					Value: "",
134					Usage: "+add read-only `ACCOUNT`",
135				},
136			},
137			Action: runAdd,
138		},
139		{
140			Name:      "create",
141			Usage:     "create one or more new bitmarks",
142			ArgsUsage: "\n   (* = required)",
143			Flags: []cli.Flag{
144				cli.StringFlag{
145					Name:  "asset, a",
146					Value: "",
147					Usage: "*asset name `STRING`",
148				},
149				cli.StringFlag{
150					Name:  "metadata, m",
151					Value: "",
152					Usage: "*asset metadata `META`",
153				},
154				cli.StringFlag{
155					Name:  "fingerprint, f",
156					Value: "",
157					Usage: "*asset fingerprint `STRING`",
158				},
159				cli.BoolFlag{
160					Name:  "zero, z",
161					Usage: " only try to issue the free zero nonce",
162				},
163				cli.IntFlag{
164					Name:  "quantity, q",
165					Value: 1,
166					Usage: " quantity to create `COUNT`",
167				},
168			},
169			Action: runCreate,
170		},
171		{
172			Name:      "transfer",
173			Usage:     "transfer a bitmark to another account",
174			ArgsUsage: "\n   (* = required)",
175			Flags: []cli.Flag{
176				cli.StringFlag{
177					Name:  "txid, t",
178					Value: "",
179					Usage: "*transaction id to transfer `TXID`",
180				},
181				cli.StringFlag{
182					Name:  "receiver, r",
183					Value: "",
184					Usage: "*identity name to receive the bitmark `ACCOUNT`",
185				},
186				cli.BoolFlag{
187					Name:  "unratified, u",
188					Usage: " perform an unratified transfer (default is output single signed hex)",
189				},
190			},
191			Action: runTransfer,
192		},
193		{
194			Name:      "countersign",
195			Usage:     "countersign a transaction using current identity",
196			ArgsUsage: "\n   (* = required)",
197			Flags: []cli.Flag{
198				cli.StringFlag{
199					Name:  "transaction, t",
200					Value: "",
201					Usage: "*sender signed transfer `HEX` code",
202				},
203			},
204			Action: runCountersign,
205		},
206		{
207			Name:      "blocktransfer",
208			Usage:     "transfer a bitmark to another account",
209			ArgsUsage: "\n   (* = required)",
210			Flags: []cli.Flag{
211				cli.StringFlag{
212					Name:  "txid, t",
213					Value: "",
214					Usage: "*transaction id to transfer `TXID`",
215				},
216				cli.StringFlag{
217					Name:  "receiver, r",
218					Value: "",
219					Usage: "*identity name to receive the block `ACCOUNT`",
220				},
221				cli.StringFlag{
222					Name:  "bitcoin, b",
223					Value: "",
224					Usage: "*address receive the bitcoin payment `ADDRESS`",
225				},
226				cli.StringFlag{
227					Name:  "litecoin, l",
228					Value: "",
229					Usage: "*address to receive the litecoin payment `ADDRESS`",
230				},
231			},
232			Action: runBlockTransfer,
233		},
234		{
235			Name:      "provenance",
236			Usage:     "list provenance of a bitmark",
237			ArgsUsage: "\n   (* = required)",
238			Flags: []cli.Flag{
239				cli.StringFlag{
240					Name:  "txid, t",
241					Value: "",
242					Usage: "*transaction id to list provenance `TXID`",
243				},
244				cli.IntFlag{
245					Name:  "count, c",
246					Value: 20,
247					Usage: " maximum records to output `COUNT`",
248				},
249			},
250			Action: runProvenance,
251		},
252		{
253			Name:      "owned",
254			Usage:     "list bitmarks owned",
255			ArgsUsage: "\n   (* = required)",
256			Flags: []cli.Flag{
257				cli.StringFlag{
258					Name:  "owner, o",
259					Value: "",
260					Usage: " identity name `ACCOUNT` default is global identity",
261				},
262				cli.Uint64Flag{
263					Name:  "start, s",
264					Value: 0,
265					Usage: " start point `COUNT`",
266				},
267				cli.IntFlag{
268					Name:  "count, c",
269					Value: 20,
270					Usage: " maximum records to output `COUNT`",
271				},
272			},
273			Action: runOwned,
274		},
275		{
276			Name:      "share",
277			Usage:     "convert a bitmark into a share",
278			ArgsUsage: "\n   (* = required)",
279			Flags: []cli.Flag{
280				cli.StringFlag{
281					Name:  "txid, t",
282					Value: "",
283					Usage: "*transaction id to convert `TXID`",
284				},
285				cli.IntFlag{
286					Name:  "quantity, q",
287					Value: 0,
288					Usage: "*quantity to create `NUMBER`",
289				},
290			},
291			Action: runShare,
292		},
293		{
294			Name:      "grant",
295			Usage:     "grant some shares of a bitmark to a receiver",
296			ArgsUsage: "\n   (* = required)",
297			Flags: []cli.Flag{
298				cli.StringFlag{
299					Name:  "receiver, r",
300					Value: "",
301					Usage: "*identity name to receive the block `ACCOUNT`",
302				},
303				cli.StringFlag{
304					Name:  "share-id, s",
305					Value: "",
306					Usage: "*transaction id of share `SHAREID`",
307				},
308				cli.Uint64Flag{
309					Name:  "quantity, q",
310					Value: 1,
311					Usage: " quantity to grant `NUMBER`",
312				},
313				cli.Uint64Flag{
314					Name:  "before-block, b",
315					Value: 0,
316					Usage: " must confirm before this block `NUMBER`",
317				},
318			},
319			Action: runGrant,
320		},
321		{
322			Name:      "swap",
323			Usage:     "swap some shares of a bitmark with a receiver",
324			ArgsUsage: "\n   (* = required)",
325			Flags: []cli.Flag{
326				cli.StringFlag{
327					Name:  "receiver, r",
328					Value: "",
329					Usage: "*identity name to receive the block `ACCOUNT`",
330				},
331				cli.StringFlag{
332					Name:  "share-id-one, s",
333					Value: "",
334					Usage: "*transaction id of share one `SHAREID`",
335				},
336				cli.Uint64Flag{
337					Name:  "quantity-one, q",
338					Value: 1,
339					Usage: " quantity of share one `NUMBER`",
340				},
341				cli.StringFlag{
342					Name:  "share-id-two, S",
343					Value: "",
344					Usage: "*transaction id of share two `SHAREID`",
345				},
346				cli.Uint64Flag{
347					Name:  "quantity-two, Q",
348					Value: 1,
349					Usage: " quantity of share two `NUMBER`",
350				},
351				cli.Uint64Flag{
352					Name:  "before-block, b",
353					Value: 0,
354					Usage: " must confirm before this block `NUMBER`",
355				},
356			},
357			Action: runSwap,
358		},
359		{
360			Name:      "balance",
361			Usage:     "display balance of some shares",
362			ArgsUsage: "\n   (* = required)",
363			Flags: []cli.Flag{
364				cli.StringFlag{
365					Name:  "owner, o",
366					Value: "",
367					Usage: " identity name `ACCOUNT` default is global identity",
368				},
369				cli.StringFlag{
370					Name:  "share-id, s",
371					Value: "",
372					Usage: " starting from share `SHAREID`",
373				},
374				cli.IntFlag{
375					Name:  "count, c",
376					Value: 20,
377					Usage: " maximum records to output `COUNT`",
378				},
379			},
380			Action: runBalance,
381		},
382		{
383			Name:      "status",
384			Usage:     "display the status of a bitmark",
385			ArgsUsage: "\n   (* = required)",
386			Flags: []cli.Flag{
387				cli.StringFlag{
388					Name:  "txid, t",
389					Value: "",
390					Usage: "*transaction id to check status `TXID`",
391				},
392			},
393			Action: runTransactionStatus,
394		},
395		{
396			Name:  "list",
397			Usage: "list bitmark-cli identities",
398			Flags: []cli.Flag{
399				cli.BoolFlag{
400					Name:  "connections, c",
401					Usage: "list connections instead",
402				},
403				cli.BoolFlag{
404					Name:  "json, j",
405					Usage: "output as JSON",
406				},
407			},
408			Action: runList,
409		},
410		{
411			Name:   "bitmarkd",
412			Usage:  "display bitmarkd information",
413			Action: runBitmarkdInfo,
414		},
415		{
416			Name:  "seed",
417			Usage: "decrypt and display an identity's recovery seed",
418			Flags: []cli.Flag{
419				cli.BoolFlag{
420					Name:  "recovery, r",
421					Usage: "display recovery phrase",
422				},
423			},
424			Action: runSeed,
425		},
426		{
427			Name:   "password",
428			Usage:  "change an identity's password",
429			Action: runChangePassword,
430		},
431		{
432			Name:      "fingerprint",
433			Usage:     "fingerprint a file (version 01 SHA3-512 algorithm)",
434			ArgsUsage: "\n   (* = required)",
435			Flags: []cli.Flag{
436				cli.StringFlag{
437					Name:  "file, f",
438					Value: "",
439					Usage: "*`FILE` of data to fingerprint",
440				},
441			},
442			Action: runFingerprint,
443		},
444		{
445			Name:      "sign",
446			Usage:     "sign file",
447			ArgsUsage: "\n   (* = required)",
448			Flags: []cli.Flag{
449				cli.StringFlag{
450					Name:  "file, f",
451					Value: "",
452					Usage: "*`FILE` of data to sign",
453				},
454			},
455			Action: runSign,
456		},
457		{
458			Name:      "verify",
459			Usage:     "verify file signature file",
460			ArgsUsage: "\n   (* = required)",
461			Flags: []cli.Flag{
462				cli.StringFlag{
463					Name:  "file, f",
464					Value: "",
465					Usage: "*`FILE` of data to sign",
466				},
467				cli.StringFlag{
468					Name:  "owner, o",
469					Value: "",
470					Usage: "*owner `ACCOUNT` that signed the file",
471				},
472				cli.StringFlag{
473					Name:  "signature, s",
474					Value: "",
475					Usage: "*signature from a sign command `HEX`",
476				},
477			},
478			Action: runVerify,
479		},
480		{
481			Name:  "version",
482			Usage: "display bitmark-cli version",
483			Action: func(c *cli.Context) error {
484				fmt.Fprintf(c.App.Writer, "%s\n", version)
485				return nil
486			},
487		},
488	}
489
490	app.Action = func(c *cli.Context) error {
491		w := c.App.Writer
492		if c.GlobalBool("version") {
493			fmt.Fprintf(w, "%s\n", version)
494		}
495		return nil
496	}
497
498	// read the configuration
499	app.Before = func(c *cli.Context) error {
500
501		e := c.App.ErrWriter
502		w := c.App.Writer
503		verbose := c.GlobalBool("verbose")
504
505		if c.GlobalBool("version") {
506			return nil
507		}
508
509		// to suppress reading config params for certain commands
510		command := c.Args().Get(0)
511		switch command {
512		case "help", "h", "version":
513			return nil
514		}
515
516		// only want one of these
517		network := strings.ToLower(c.GlobalString("network"))
518		switch network {
519		case "bitmark", "testing", "local":
520		default:
521			return fmt.Errorf("network: %q can only be bitmark/testing/local", network)
522		}
523
524		p := os.Getenv("XDG_CONFIG_HOME")
525		if "" == p {
526			return fmt.Errorf("XDG_CONFIG_HOME environment is not set")
527		}
528		dir, err := checkFileExists(p)
529		if nil != err {
530			return err
531		}
532		if !dir {
533			return fmt.Errorf("not a directory: %q", p)
534		}
535		file := path.Join(p, app.Name, network+"-cli.json")
536
537		if verbose {
538			fmt.Fprintf(e, "file: %q\n", file)
539		}
540
541		if "setup" == command {
542			// do not run setup if there is an existing configuration
543			if _, err := checkFileExists(file); nil == err {
544				return fmt.Errorf("not overwriting existing configuration: %q", file)
545			}
546
547			c.App.Metadata["config"] = &metadata{
548				file:    file,
549				save:    false,
550				testnet: network != "bitmark",
551				verbose: verbose,
552				e:       e,
553				w:       w,
554			}
555
556		} else {
557
558			if verbose {
559				fmt.Fprintf(e, "reading config file: %s\n", file)
560			}
561
562			configuration, err := configuration.Load(file)
563			if nil != err {
564				return err
565			}
566			connectionOffset := c.GlobalInt("connection")
567			if connectionOffset < 0 || connectionOffset >= len(configuration.Connections) {
568				return fmt.Errorf("connection: %d outside: %d to %d", connectionOffset, 0, len(configuration.Connections)-1)
569			}
570
571			c.App.Metadata["config"] = &metadata{
572				file:             file,
573				config:           configuration,
574				connectionOffset: connectionOffset,
575				testnet:          configuration.TestNet,
576				save:             false,
577				verbose:          verbose,
578				e:                e,
579				w:                w,
580			}
581		}
582
583		return nil
584	}
585
586	// update the configuration if required
587	app.After = func(c *cli.Context) error {
588		e := c.App.ErrWriter
589		m, ok := c.App.Metadata["config"].(*metadata)
590		if !ok {
591			return nil
592		}
593		if m.save {
594			if c.GlobalBool("verbose") {
595				fmt.Fprintf(e, "updating config file: %s\n", m.file)
596			}
597			err := configuration.Save(m.file, m.config)
598			if nil != err {
599				return err
600			}
601		}
602		return nil
603	}
604
605	err := app.Run(os.Args)
606	if nil != err {
607		fmt.Fprintf(app.ErrWriter, "terminated with error: %s\n", err)
608		os.Exit(1)
609	}
610}
611