Files
kisstron/main.go
T

310 lines
9.7 KiB
Go
Raw Normal View History

2026-09-19 18:26:07 +03:00
// 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 <https://unlicense.org>\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
}