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