386 lines
12 KiB
Go
386 lines
12 KiB
Go
// 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"
|
||
|
|
}
|