// Package ops implements the kisstron wallet operations (new, import, info, // send, freeze, unfreeze, broadcast) mirroring the functions in kisstron.py. package ops import ( "errors" "fmt" "math" "math/big" "strings" "time" "code.luxferre.top/luxferre/kisstron/cli" "code.luxferre.top/luxferre/kisstron/tron" ) // Config carries the resolved command-line parameters shared by all operations. type Config struct { Network string Node string APIKey string FeeLimit int64 Passphrase string WordsCount int Derivation string } // client builds a TRON client for the operation's network, reproducing the // tronclient() endpoint and fee-limit selection logic of kisstron.py. func (c Config) client() *tron.Client { var endpoint string fee := int64(tron.FeeLimitTestnet) if c.Network == "mainnet" { fee = c.FeeLimit switch { case c.APIKey != "": endpoint = "https://api.trongrid.io" case c.Node != "": endpoint = c.Node default: endpoint = tron.DefaultNodeAddr } } else { endpoint = tron.DefaultEndpoint(c.Network) } return tron.NewClient(c.Network, endpoint, fee, c.APIKey) } // NewWallet creates a fresh wallet and returns its details. func NewWallet(cfg Config) cli.Result { if !tron.ValidWordCount(cfg.WordsCount) { return cli.Result{Message: "Invalid word count for mnemonic generation"} } key, mnemonic, err := tron.GenerateWithMnemonic(cfg.Passphrase, cfg.WordsCount, cfg.Derivation) if err != nil { return cli.Result{Message: err.Error()} } return walletResult(key, mnemonic, "Wallet created, save this info in a secure place!") } // ImportWallet restores a wallet from a mnemonic. func ImportWallet(cfg Config, mnemonic string) cli.Result { if !tron.ValidateMnemonic(mnemonic) { return cli.Result{Message: "Invalid mnemonic phrase!"} } key, err := tron.DeriveFromMnemonic(mnemonic, cfg.Passphrase, cfg.Derivation) if err != nil { return cli.Result{Message: "Invalid mnemonic phrase!"} } return walletResult(key, strings.Join(strings.Fields(mnemonic), " "), "Wallet imported, save this info in a secure place!") } func walletResult(key *tron.PrivateKey, mnemonic, banner string) cli.Result { info := tron.GenerateAddressInfo(key) msg := banner + "\n\n" msg += fmt.Sprintf("Address: %s\n", info["base58check_address"]) msg += fmt.Sprintf("Address (hex): %s\n", info["hex_address"]) msg += fmt.Sprintf("Private key (hex): %s\n", info["private_key"]) msg += fmt.Sprintf("Public key (hex): %s\n", info["public_key"]) msg += fmt.Sprintf("Mnemonic: %s", mnemonic) return cli.Result{Success: true, Message: msg, Result: info} } // frozenEntry is a single TRX freeze record. type frozenEntry struct { Type string Amount int64 } // InfoByAddr displays account information for a TRON address. func InfoByAddr(cfg Config, address string) cli.Result { client := cfg.client() rawInfo, err := client.GetAccount(address) if err != nil { switch { case errors.Is(err, tron.ErrBadAddress): return cli.Result{Message: "Invalid address! Set the correct one with -addr flag"} case errors.Is(err, tron.ErrAddressNotFound): return cli.Result{Message: "Address not found on the blockchain! Set the correct one with -addr flag"} default: return cli.Result{Message: err.Error()} } } rawRes, err := client.GetAccountResource(address) if err != nil { return cli.Result{Message: err.Error()} } // Bandwidth computation. NOTE: this faithfully reproduces the original // kisstron.py behaviour, including the latent bug where freeNetUsed is // assigned to the freeNetLimit variable (line 210 of kisstron.py), so wallet // output stays byte-identical to the Python implementation. var freeNetLimit, freeNetUsed, netLimit, netUsed int64 if v, ok := rawRes["freeNetLimit"]; ok { freeNetLimit = tron.ValueInt64(v) } if v, ok := rawRes["freeNetUsed"]; ok { freeNetLimit = tron.ValueInt64(v) } if v, ok := rawRes["NetLimit"]; ok { netLimit = tron.ValueInt64(v) } if v, ok := rawRes["NetUsed"]; ok { netUsed = tron.ValueInt64(v) } bandwidth := freeNetLimit - freeNetUsed + netLimit - netUsed var energyLimit, energyUsed int64 if v, ok := rawRes["EnergyLimit"]; ok { energyLimit = tron.ValueInt64(v) } if v, ok := rawRes["EnergyUsed"]; ok { energyUsed = tron.ValueInt64(v) } energy := energyLimit - energyUsed addressOut := tron.ValueString(rawInfo["address"]) trxBalance := tron.ValueInt64(rawInfo["balance"]) var frozen []frozenEntry if arr, ok := rawInfo["frozenV2"].([]any); ok { for _, e := range arr { m, ok := e.(map[string]any) if !ok { continue } t, hasT := m["type"] a, hasA := m["amount"] if !hasT || !hasA { continue } amount := tron.ValueInt64(a) if amount > 0 { frozen = append(frozen, frozenEntry{Type: tron.ValueString(t), Amount: amount}) } } } type tokenBalance struct { ticker string value float64 } var balances []tokenBalance for _, token := range tron.TokensForNetwork(cfg.Network) { if !token.ShowBalance { continue } cntr, err := client.GetContract(token.Contract) if err != nil { return cli.Result{Message: err.Error()} } value, err := trc20Balance(cntr, address) if err != nil { return cli.Result{Message: err.Error()} } balances = append(balances, tokenBalance{ticker: token.Ticker, value: value}) } msg := fmt.Sprintf("Address: %s\nBalance:\n", addressOut) msg += fmt.Sprintf("%f TRX\n", float64(trxBalance)/tron.TrxScale) for _, b := range balances { msg += fmt.Sprintf("%f %s\n", b.value, b.ticker) } msg += "Frozen assets:\n" for _, f := range frozen { msg += fmt.Sprintf("%s\t%f TRX\n", f.Type, float64(f.Amount)/tron.TrxScale) } msg += fmt.Sprintf("Remaining bandwidth: %d\n", bandwidth) msg += fmt.Sprintf("Remaining energy: %d", energy) return cli.Result{Success: true, Message: msg} } // trc20Balance returns the float token balance of addr for a contract. func trc20Balance(cntr *tron.Contract, addr string) (float64, error) { precision, err := cntr.Decimals() if err != nil { return 0, err } bal, err := cntr.BalanceOf(addr) if err != nil { return 0, err } divisor := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(precision)), nil) f := new(big.Float).SetInt(bal) f.Quo(f, new(big.Float).SetInt(divisor)) out, _ := f.Float64() return out, nil } // InfoByPK displays own account information derived from a private key. func InfoByPK(cfg Config, pk string) cli.Result { key, err := tron.PrivateKeyFromHex(pk) if err != nil { return cli.Result{Message: "Invalid private key!"} } return InfoByAddr(cfg, key.Pub.ToBase58CheckAddress()) } // SendTrx sends TRX (or produces a signed offline transaction). func SendTrx(cfg Config, pk, toaddr string, amt float64, offline bool) cli.Result { if amt < 1 { return cli.Result{Message: "Nothing to transfer!"} } key, err := tron.PrivateKeyFromHex(pk) if err != nil { return cli.Result{Message: "Invalid private key!"} } builder, err := cfg.client().Transfer(key.Pub.ToBase58CheckAddress(), toaddr, int64(amt*tron.TrxScale)) if err != nil { return cli.Result{Message: err.Error()} } tx, err := builder.Build() if err != nil { return cli.Result{Message: err.Error()} } if err := tx.Sign(key); err != nil { return cli.Result{Message: err.Error()} } if offline { return cli.Result{Success: true, Message: tx.JSON(), Result: tx} } return broadcastAndWait(tx, fmt.Sprintf("Sent %f TRX to %s", amt, toaddr)) } // SendTrc20 sends a supported TRC20 token. func SendTrc20(cfg Config, pk, ticker, toaddr string, amt float64, offline bool) cli.Result { ticker = strings.ToLower(ticker) token, ok := tron.TokenByKey(cfg.Network, ticker) if !ok { return cli.Result{Message: fmt.Sprintf("Token not supported by kisstron for %s!", cfg.Network)} } if amt < 1 { return cli.Result{Message: "Nothing to transfer!"} } key, err := tron.PrivateKeyFromHex(pk) if err != nil { return cli.Result{Message: "Invalid private key!"} } client := cfg.client() cntr, err := client.GetContract(token.Contract) if err != nil { return cli.Result{Message: err.Error()} } precision, err := cntr.Decimals() if err != nil { return cli.Result{Message: err.Error()} } amount := new(big.Int).SetInt64(int64(amt * math.Pow(10, float64(precision)))) builder, err := cntr.Transfer(key.Pub.ToBase58CheckAddress(), toaddr, amount) if err != nil { return cli.Result{Message: err.Error()} } tx, err := builder.Build() if err != nil { return cli.Result{Message: err.Error()} } if err := tx.Sign(key); err != nil { return cli.Result{Message: err.Error()} } if offline { return cli.Result{Success: true, Message: tx.JSON(), Result: tx} } return broadcastAndWait(tx, fmt.Sprintf("Sent %f %s to %s", amt, token.Ticker, toaddr)) } // FreezeTrx freezes TRX for energy. func FreezeTrx(cfg Config, pk string, amt float64, offline bool) cli.Result { if amt < 1 { return cli.Result{Message: "Nothing to freeze!"} } key, err := tron.PrivateKeyFromHex(pk) if err != nil { return cli.Result{Message: "Invalid private key!"} } myaddr := key.Pub.ToBase58CheckAddress() builder, err := cfg.client().FreezeBalance(myaddr, int64(amt*tron.TrxScale), "ENERGY") if err != nil { return cli.Result{Message: err.Error()} } tx, err := builder.Build() if err != nil { return cli.Result{Message: err.Error()} } if err := tx.Sign(key); err != nil { return cli.Result{Message: err.Error()} } if offline { return cli.Result{Success: true, Message: tx.JSON(), Result: tx} } return broadcastAndWait(tx, fmt.Sprintf("Frozen %f TRX at %s", amt, myaddr)) } // UnfreezeTrx unfreezes TRX. func UnfreezeTrx(cfg Config, pk string, amt float64, offline bool) cli.Result { if amt < 1 { return cli.Result{Message: "Nothing to unfreeze!"} } key, err := tron.PrivateKeyFromHex(pk) if err != nil { return cli.Result{Message: "Invalid private key!"} } myaddr := key.Pub.ToBase58CheckAddress() builder, err := cfg.client().UnfreezeBalance(myaddr, int64(amt*tron.TrxScale), "ENERGY") if err != nil { return cli.Result{Message: err.Error()} } tx, err := builder.Build() if err != nil { return cli.Result{Message: err.Error()} } if err := tx.Sign(key); err != nil { return cli.Result{Message: err.Error()} } if offline { return cli.Result{Success: true, Message: tx.JSON(), Result: tx} } return broadcastAndWait(tx, fmt.Sprintf("Unfrozen %f TRX at %s", amt, myaddr)) } // BroadcastJSON broadcasts a transaction previously produced in offline mode. func BroadcastJSON(cfg Config, tjson string) cli.Result { tx, err := cfg.client().ParseTransaction(tjson) if err != nil { return cli.Result{Message: "Malformed transaction JSON!"} } if _, err := tx.Broadcast(); err != nil { return cli.Result{Message: err.Error()} } receipt, err := tx.Wait(30*time.Second, 1600*time.Millisecond) if err != nil { return cli.Result{Message: err.Error()} } if isOutOfEnergy(receipt) { return cli.Result{Message: "Not enough energy for transaction!\nTransaction ID " + receiptID(receipt), Result: receipt} } return cli.Result{Success: true, Message: "Transaction sent\nTransaction ID " + receiptID(receipt), Result: receipt} } // broadcastAndWait broadcasts tx, waits for the receipt and formats a success // message with the given header line. func broadcastAndWait(tx *tron.Transaction, header string) cli.Result { if _, err := tx.Broadcast(); err != nil { return cli.Result{Message: err.Error()} } receipt, err := tx.Wait(30*time.Second, 1600*time.Millisecond) if err != nil { return cli.Result{Message: err.Error()} } if isOutOfEnergy(receipt) { return cli.Result{Message: "Not enough energy for transaction!\nTransaction ID " + receiptID(receipt), Result: receipt} } return cli.Result{Success: true, Message: header + "\nTransaction ID " + receiptID(receipt), Result: receipt} } func receiptID(receipt map[string]any) string { return tron.ValueString(receipt["id"]) } func isOutOfEnergy(receipt map[string]any) bool { r, ok := receipt["receipt"].(map[string]any) if !ok { return false } return tron.ValueString(r["result"]) == "OUT_OF_ENERGY" }