// Command kisstron is a KISS-oriented TRON CLI wallet. It is a Go port of the // original Python kisstron.py, preserving the command-line interface, output // format and operation semantics. package main import ( "fmt" "os" "path/filepath" "strconv" "strings" "code.luxferre.top/luxferre/kisstron/cli" "code.luxferre.top/luxferre/kisstron/ops" "code.luxferre.top/luxferre/kisstron/tron" ) // options holds the parsed command-line parameters. type options struct { op string coin string network string node string tgkey string feeLimit float64 address string amount float64 pk string offline bool passphrase string wordsCount int derivPath string noConfirm bool } // usageText builds the argparse-style usage block for prog. func usageText(prog string) string { return "usage: " + prog + " [-h] [-net NETWORK] [-node TRON_NODE]\n" + " [-tgkey TRONGRID_API_KEY] [-fee FEE_LIMIT] [-addr ADDRESS]\n" + " [-amt AMOUNT] [-pk PRIVATE_KEY] [-o] [-pp PASSPHRASE]\n" + " [-wc WORDS_COUNT] [-dp DERIVATION_PATH] [-nc]\n" + " op coin\n" } // helpText builds the argparse-style full help output for prog. func helpText(prog string) string { return usageText(prog) + "\n" + "kisstron: a simple CLI wallet for Tron blockchain\n\n" + "positional arguments:\n" + " op Operation: new, import, info, send, fre[eze],\n" + " unf[reeze], bro[adcast]\n" + " coin Coin to operate on: trx (default), usdt, usdc\n\n" + "options:\n" + " -h, --help show this help message and exit\n" + " -net, --network NETWORK\n" + " (op: all) Network: mainnet (default), nile, shasta\n" + " -node, --tron-node TRON_NODE\n" + " (op: all) Custom Tron node HTTP address (default\n" + " " + tron.DefaultNodeAddr + ")\n" + " -tgkey, --trongrid-api-key TRONGRID_API_KEY\n" + " (op: all) TronGrid API key (default None)\n" + " -fee, --fee-limit FEE_LIMIT\n" + " (op: send, fre, unf) Custom fee limit for mainnet\n" + " transactions, in TRX (default 100)\n" + " -addr, --address ADDRESS\n" + " (op: info, send) Tron address to send to or display\n" + " information about (-pk overrides this to display own\n" + " info)\n" + " -amt, --amount AMOUNT\n" + " (op: send, fre, unf) Amount to send/freeze/unfreeze\n" + " -pk, --private-key PRIVATE_KEY\n" + " (op: info, send, fre, unf) Private key for\n" + " transactions or own information, in hex\n" + " -o, --offline (op: send, fre, unf) Build and sign the transaction in\n" + " offline mode and output its JSON\n" + " -pp, --passphrase PASSPHRASE\n" + " (op: new, import) Mnemonic encryption passphrase\n" + " (default empty)\n" + " -wc, --words-count WORDS_COUNT\n" + " (op: new) Word count in the mnemonic: 12 (default),\n" + " 15, 18, 21, 24\n" + " -dp, --derivation-path DERIVATION_PATH\n" + " (op: new, import) BIP-39 derivation path for mnemonic\n" + " (default is m/44'/195'/0'/0/0)\n" + " -nc, --no-confirm (op: send, fre, unf) Do not ask to type \"yes\" for\n" + " transaction confirmation (dangerous)\n\n" + "(c) Luxferre 2024 --- No rights reserved \n" } func main() { prog := filepath.Base(os.Args[0]) args, err := parseArgs(prog, os.Args[1:]) if err != nil { fmt.Fprint(os.Stderr, usageText(prog)) fmt.Fprintln(os.Stderr, err) os.Exit(2) } os.Exit(run(args)) } // canonical maps a flag token to its canonical name. var canonical = map[string]string{ "-h": "help", "--help": "help", "-net": "net", "--network": "net", "-node": "node", "--tron-node": "node", "-tgkey": "tgkey", "--trongrid-api-key": "tgkey", "-fee": "fee", "--fee-limit": "fee", "-addr": "addr", "--address": "addr", "-amt": "amt", "--amount": "amt", "-pk": "pk", "--private-key": "pk", "-o": "offline", "--offline": "offline", "-pp": "pp", "--passphrase": "pp", "-wc": "wc", "--words-count": "wc", "-dp": "dp", "--derivation-path": "dp", "-nc": "nc", "--no-confirm": "nc", } // flagTakesValue reports whether a canonical flag consumes the next argument. func flagTakesValue(name string) bool { switch name { case "net", "node", "tgkey", "fee", "addr", "amt", "pk", "pp", "wc", "dp": return true } return false } // errRequired formats argparse's missing-argument error. func errRequired(prog string, names ...string) error { return fmt.Errorf("%s: error: the following arguments are required: %s", prog, strings.Join(names, ", ")) } func parseArgs(prog string, argv []string) (options, error) { o := options{ coin: "trx", network: "mainnet", feeLimit: 100.0, wordsCount: 12, derivPath: "m/44'/195'/0'/0/0", } var positionals []string i := 0 for i < len(argv) { tok := argv[i] name, inlineVal, hasInline := tok, "", false if strings.HasPrefix(tok, "--") { if eq := strings.IndexByte(tok, '='); eq >= 0 { name = tok[:eq] inlineVal = tok[eq+1:] hasInline = true } } canon, isFlag := canonical[name] if !isFlag { if len(tok) > 0 && tok[0] == '-' { return o, fmt.Errorf("%s: error: unrecognized arguments: %s", prog, tok) } positionals = append(positionals, tok) i++ continue } if canon == "help" { fmt.Print(helpText(prog)) os.Exit(0) } if flagTakesValue(canon) { var val string if hasInline { val = inlineVal } else { if i+1 >= len(argv) { return o, fmt.Errorf("%s: error: argument %s: expected one argument", tok, prog) } val = argv[i+1] i++ } if err := assignValue(&o, prog, canon, val, tok); err != nil { return o, err } } else { switch canon { case "offline": o.offline = true case "nc": o.noConfirm = true } } i++ } switch len(positionals) { case 0: return o, errRequired(prog, "op", "coin") case 1: return o, errRequired(prog, "coin") case 2: o.op = strings.ToLower(positionals[0]) o.coin = strings.ToLower(positionals[1]) default: return o, fmt.Errorf("%s: error: unrecognized arguments: %s", prog, strings.Join(positionals[2:], " ")) } return o, nil } func assignValue(o *options, prog, canon, val, raw string) error { switch canon { case "net": o.network = strings.ToLower(val) case "node": o.node = val case "tgkey": o.tgkey = val case "addr": o.address = val case "pk": o.pk = val case "pp": o.passphrase = val case "dp": o.derivPath = val case "fee": f, err := strconv.ParseFloat(val, 64) if err != nil { return fmt.Errorf("%s: error: argument -fee/--fee-limit: invalid float value: '%s'", prog, val) } o.feeLimit = f case "amt": f, err := strconv.ParseFloat(val, 64) if err != nil { return fmt.Errorf("%s: error: argument -amt/--amount: invalid float value: '%s'", prog, val) } o.amount = f case "wc": n, err := strconv.Atoi(val) if err != nil { return fmt.Errorf("%s: error: argument -wc/--words-count: invalid int value: '%s'", prog, val) } o.wordsCount = n } return nil } // run dispatches the operation and prints the result. It returns the process // exit code. func run(o options) int { cfg := ops.Config{ Network: o.network, Node: o.node, APIKey: o.tgkey, FeeLimit: int64(o.feeLimit * tron.TrxScale), Passphrase: o.passphrase, WordsCount: o.wordsCount, Derivation: o.derivPath, } var res cli.Result switch o.op { case "new": res = ops.NewWallet(cfg) case "import": mnemonic := cli.ReadLine("Your mnemonic phrase: ") res = ops.ImportWallet(cfg, mnemonic) case "info": if o.offline { fmt.Println("Info operation is not available in offline mode!") return 1 } if o.pk == "" { res = ops.InfoByAddr(cfg, o.address) } else { res = ops.InfoByPK(cfg, o.pk) } case "send": if !o.noConfirm { cli.Confirm("Enter \"yes\" to confirm sending funds: ") } if o.coin == "trx" { res = ops.SendTrx(cfg, o.pk, o.address, o.amount, o.offline) } else { res = ops.SendTrc20(cfg, o.pk, o.coin, o.address, o.amount, o.offline) } case "fre", "freeze": if !o.noConfirm { cli.Confirm("Enter \"yes\" to confirm freezing funds: ") } res = ops.FreezeTrx(cfg, o.pk, o.amount, o.offline) case "unf", "unfreeze": if !o.noConfirm { cli.Confirm("Enter \"yes\" to confirm unfreezing funds: ") } res = ops.UnfreezeTrx(cfg, o.pk, o.amount, o.offline) case "bro", "broadcast": if o.offline { fmt.Println("Broadcast operation is not available in offline mode!") return 1 } tjson := cli.ReadLine("Input transaction JSON: ") if !o.noConfirm { cli.Confirm("Enter \"yes\" to confirm sending the transaction: ") } res = ops.BroadcastJSON(cfg, tjson) default: fmt.Println("Unknown operation!") fmt.Println("Run the script with -h parameter to see available options") return 1 } cli.PrintResult(res) return 0 }