go port
This commit is contained in:
@@ -0,0 +1,118 @@
|
|||||||
|
// Package base58 implements Bitcoin-style base58 and base58check encoding on
|
||||||
|
// top of math/big and crypto/sha256. It is the address encoding used by TRON.
|
||||||
|
package base58
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"errors"
|
||||||
|
"math/big"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
||||||
|
|
||||||
|
// ErrInvalidByte is returned when a byte is not part of the base58 alphabet.
|
||||||
|
var ErrInvalidByte = errors.New("base58: invalid byte")
|
||||||
|
|
||||||
|
// ErrChecksum indicates a base58check payload with a wrong checksum.
|
||||||
|
var ErrChecksum = errors.New("base58: checksum mismatch")
|
||||||
|
|
||||||
|
var decodeMap = buildDecodeMap()
|
||||||
|
|
||||||
|
func buildDecodeMap() [256]int16 {
|
||||||
|
var m [256]int16
|
||||||
|
for i := range m {
|
||||||
|
m[i] = -1
|
||||||
|
}
|
||||||
|
for i := 0; i < len(alphabet); i++ {
|
||||||
|
m[alphabet[i]] = int16(i)
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode converts a byte slice to a base58 string.
|
||||||
|
func Encode(b []byte) string {
|
||||||
|
x := new(big.Int).SetBytes(b)
|
||||||
|
base := big.NewInt(58)
|
||||||
|
mod := new(big.Int)
|
||||||
|
out := make([]byte, 0, len(b)*137/100+1)
|
||||||
|
for x.Sign() > 0 {
|
||||||
|
x.DivMod(x, base, mod)
|
||||||
|
out = append(out, alphabet[mod.Int64()])
|
||||||
|
}
|
||||||
|
for _, c := range b {
|
||||||
|
if c != 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
out = append(out, alphabet[0])
|
||||||
|
}
|
||||||
|
for i, j := 0, len(out)-1; i < j; i, j = i+1, j-1 {
|
||||||
|
out[i], out[j] = out[j], out[i]
|
||||||
|
}
|
||||||
|
return string(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode converts a base58 string to a byte slice.
|
||||||
|
func Decode(s string) ([]byte, error) {
|
||||||
|
x := big.NewInt(0)
|
||||||
|
base := big.NewInt(58)
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
d := decodeMap[s[i]]
|
||||||
|
if d < 0 {
|
||||||
|
return nil, ErrInvalidByte
|
||||||
|
}
|
||||||
|
x.Mul(x, base)
|
||||||
|
x.Add(x, big.NewInt(int64(d)))
|
||||||
|
}
|
||||||
|
was := x.Bytes()
|
||||||
|
pad := 0
|
||||||
|
for pad < len(s) && s[pad] == alphabet[0] {
|
||||||
|
pad++
|
||||||
|
}
|
||||||
|
out := make([]byte, pad+len(was))
|
||||||
|
copy(out[pad:], was)
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checksum(payload []byte) []byte {
|
||||||
|
h1 := sha256.Sum256(payload)
|
||||||
|
h2 := sha256.Sum256(h1[:])
|
||||||
|
return h2[:4]
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckEncode returns the base58check encoding of payload.
|
||||||
|
func CheckEncode(payload []byte) string {
|
||||||
|
buf := make([]byte, 0, len(payload)+4)
|
||||||
|
buf = append(buf, payload...)
|
||||||
|
buf = append(buf, checksum(payload)...)
|
||||||
|
return Encode(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckDecode decodes a base58check string, verifying its checksum.
|
||||||
|
func CheckDecode(s string) ([]byte, error) {
|
||||||
|
raw, err := Decode(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(raw) < 5 {
|
||||||
|
return nil, ErrChecksum
|
||||||
|
}
|
||||||
|
payload := raw[:len(raw)-4]
|
||||||
|
want := checksum(payload)
|
||||||
|
got := raw[len(raw)-4:]
|
||||||
|
for i := 0; i < 4; i++ {
|
||||||
|
if want[i] != got[i] {
|
||||||
|
return nil, ErrChecksum
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return payload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValid reports whether s is a well-formed base58check string.
|
||||||
|
func IsValid(s string) bool {
|
||||||
|
if len(s) == 0 || strings.IndexByte(s, ' ') >= 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, err := CheckDecode(s)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package base58
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/hex"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRoundTrip(t *testing.T) {
|
||||||
|
cases := [][]byte{
|
||||||
|
{0x00},
|
||||||
|
{0x00, 0x00, 0x01},
|
||||||
|
bytes.Repeat([]byte{0xff}, 32),
|
||||||
|
[]byte("hello world"),
|
||||||
|
{},
|
||||||
|
}
|
||||||
|
for _, in := range cases {
|
||||||
|
enc := Encode(in)
|
||||||
|
got, err := Decode(enc)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Decode(%q): %v", enc, err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got, in) {
|
||||||
|
t.Fatalf("round trip mismatch: in=%x enc=%q got=%x", in, enc, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckEncodeKnownAddress(t *testing.T) {
|
||||||
|
raw, err := hex.DecodeString("4143d75ee7cdcd7f2fcd4fc3b4eb574b6bdc2a3fcb")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decode raw: %v", err)
|
||||||
|
}
|
||||||
|
if got := CheckEncode(raw); got != "TG9vH8c2jBWq2wSnksrq5mVyq2wjhG4A1n" {
|
||||||
|
t.Fatalf("CheckEncode = %s", got)
|
||||||
|
}
|
||||||
|
payload, err := CheckDecode("TG9vH8c2jBWq2wSnksrq5mVyq2wjhG4A1n")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CheckDecode: %v", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(payload, raw) {
|
||||||
|
t.Fatalf("CheckDecode = %x, want %x", payload, raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckDecodeRejectsBadChecksum(t *testing.T) {
|
||||||
|
if _, err := CheckDecode("TG9vH8c2jBWq2wSnksrq5mVyq2wjhG4A1m"); err == nil {
|
||||||
|
t.Fatalf("expected checksum error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeRejectsInvalidByte(t *testing.T) {
|
||||||
|
if _, err := Decode("0OIl"); err == nil {
|
||||||
|
t.Fatalf("expected invalid byte error")
|
||||||
|
}
|
||||||
|
}
|
||||||
+146
@@ -0,0 +1,146 @@
|
|||||||
|
// Package bip32 implements the BIP-32 private child key derivation (CKDpriv)
|
||||||
|
// used to derive TRON keys from a BIP-39 seed. Only private derivation is
|
||||||
|
// required, so extended-key serialization is intentionally omitted.
|
||||||
|
package bip32
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha512"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"math/big"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"code.luxferre.top/luxferre/kisstron/secp256k1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HardenedKeyStart is the BIP-32 hardened derivation offset (2^31).
|
||||||
|
const HardenedKeyStart uint32 = 0x80000000
|
||||||
|
|
||||||
|
// DefaultPath is the standard TRON BIP-44 derivation path.
|
||||||
|
const DefaultPath = "m/44'/195'/0'/0/0"
|
||||||
|
|
||||||
|
// ErrInvalidPath is returned for unparsable derivation paths.
|
||||||
|
var ErrInvalidPath = errors.New("bip32: invalid derivation path")
|
||||||
|
|
||||||
|
// ErrInvalidSeed is returned when the derived key is not usable.
|
||||||
|
var ErrInvalidSeed = errors.New("bip32: invalid seed")
|
||||||
|
|
||||||
|
// ParsePath decodes a BIP-32 path such as m/44'/195'/0'/0/0 into indices.
|
||||||
|
// Nodes may use ' or H (or h) to mark hardened derivation.
|
||||||
|
func ParsePath(path string) ([]uint32, error) {
|
||||||
|
parts := strings.Split(path, "/")
|
||||||
|
if len(parts) == 0 || (parts[0] != "m" && parts[0] != "M") {
|
||||||
|
return nil, ErrInvalidPath
|
||||||
|
}
|
||||||
|
out := make([]uint32, 0, len(parts)-1)
|
||||||
|
for _, part := range parts[1:] {
|
||||||
|
if part == "" {
|
||||||
|
return nil, ErrInvalidPath
|
||||||
|
}
|
||||||
|
hardened := false
|
||||||
|
if last := part[len(part)-1]; last == '\'' || last == 'H' || last == 'h' {
|
||||||
|
hardened = true
|
||||||
|
part = part[:len(part)-1]
|
||||||
|
}
|
||||||
|
idx, err := strconv.ParseUint(part, 10, 31)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ErrInvalidPath
|
||||||
|
}
|
||||||
|
v := uint32(idx)
|
||||||
|
if hardened {
|
||||||
|
v |= HardenedKeyStart
|
||||||
|
}
|
||||||
|
out = append(out, v)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MasterKey derives the master private key and chain code from a seed.
|
||||||
|
func MasterKey(seed []byte) (key, chainCode []byte, err error) {
|
||||||
|
mac := hmac.New(sha512.New, []byte("Bitcoin seed"))
|
||||||
|
mac.Write(seed)
|
||||||
|
sum := mac.Sum(nil)
|
||||||
|
key = make([]byte, 32)
|
||||||
|
chainCode = make([]byte, 32)
|
||||||
|
copy(key, sum[:32])
|
||||||
|
copy(chainCode, sum[32:])
|
||||||
|
if new(big.Int).SetBytes(key).Sign() == 0 {
|
||||||
|
return nil, nil, ErrInvalidSeed
|
||||||
|
}
|
||||||
|
return key, chainCode, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DerivePath derives the private key at the given path from a seed.
|
||||||
|
func DerivePath(seed []byte, path string) ([]byte, error) {
|
||||||
|
indices, err := ParsePath(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
key, chainCode, err := MasterKey(seed)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, idx := range indices {
|
||||||
|
key, chainCode, err = CKDpriv(key, chainCode, idx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CKDpriv computes the child private key and chain code for a single index.
|
||||||
|
func CKDpriv(key, chainCode []byte, index uint32) (childKey, childChain []byte, err error) {
|
||||||
|
if len(key) != 32 || len(chainCode) != 32 {
|
||||||
|
return nil, nil, ErrInvalidSeed
|
||||||
|
}
|
||||||
|
var data []byte
|
||||||
|
if index >= HardenedKeyStart {
|
||||||
|
data = append([]byte{0x00}, key...)
|
||||||
|
} else {
|
||||||
|
pub, perr := compressedPublicKey(key)
|
||||||
|
if perr != nil {
|
||||||
|
return nil, nil, perr
|
||||||
|
}
|
||||||
|
data = pub
|
||||||
|
}
|
||||||
|
var ser [4]byte
|
||||||
|
binary.BigEndian.PutUint32(ser[:], index)
|
||||||
|
data = append(data, ser[:]...)
|
||||||
|
|
||||||
|
mac := hmac.New(sha512.New, chainCode)
|
||||||
|
mac.Write(data)
|
||||||
|
sum := mac.Sum(nil)
|
||||||
|
|
||||||
|
il := new(big.Int).SetBytes(sum[:32])
|
||||||
|
child := new(big.Int).Add(il, new(big.Int).SetBytes(key))
|
||||||
|
child.Mod(child, secpOrder)
|
||||||
|
if il.Cmp(secpOrder) >= 0 || child.Sign() == 0 {
|
||||||
|
return nil, nil, ErrInvalidSeed
|
||||||
|
}
|
||||||
|
childKey = make([]byte, 32)
|
||||||
|
child.FillBytes(childKey)
|
||||||
|
childChain = make([]byte, 32)
|
||||||
|
copy(childChain, sum[32:])
|
||||||
|
return childKey, childChain, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// compressedPublicKey returns the 33-byte SEC1 compressed public key for a
|
||||||
|
// 32-byte private key.
|
||||||
|
func compressedPublicKey(key []byte) ([]byte, error) {
|
||||||
|
p, err := secp256k1.PublicKey(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out := make([]byte, 33)
|
||||||
|
out[0] = 0x02
|
||||||
|
if p.Y.Bit(0) == 1 {
|
||||||
|
out[0] = 0x03
|
||||||
|
}
|
||||||
|
p.X.FillBytes(out[1:])
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var secpOrder, _ = new(big.Int).SetString("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", 16)
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package bip32
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.luxferre.top/luxferre/kisstron/base58"
|
||||||
|
"code.luxferre.top/luxferre/kisstron/bip39"
|
||||||
|
"code.luxferre.top/luxferre/kisstron/keccak"
|
||||||
|
"code.luxferre.top/luxferre/kisstron/secp256k1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// addressOf computes the TRON base58check address for an uncompressed 64-byte
|
||||||
|
// public key (x || y).
|
||||||
|
func addressOf(pub64 []byte) string {
|
||||||
|
h := keccak.Sum256(pub64)
|
||||||
|
payload := append([]byte{0x41}, h[12:]...)
|
||||||
|
return base58.CheckEncode(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestKnownDerivations locks the BIP-32 CKDpriv implementation to vectors
|
||||||
|
// produced by tronpy's HDPath on the standard "abandon ... about" mnemonic.
|
||||||
|
func TestKnownDerivations(t *testing.T) {
|
||||||
|
const mn = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||||
|
seed := bip39.Seed(mn, "")
|
||||||
|
if got := hex.EncodeToString(seed); got != "5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4" {
|
||||||
|
t.Fatalf("seed = %s", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
path string
|
||||||
|
key string
|
||||||
|
addr string
|
||||||
|
}{
|
||||||
|
{"m/44'/195'/0'/0/0", "b5a4cea271ff424d7c31dc12a3e43e401df7a40d7412a15750f3f0b6b5449a28", "TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH"},
|
||||||
|
{"m/44'/195'/0'/0/1", "edb728e259afca2ddcc428459e7681b8414668649aedbc8d25c0872da219b2e6", "TSeJkUh4Qv67VNFwY8LaAxERygNdy6NQZK"},
|
||||||
|
{"m/44'/195'/1'/0/0", "fe06186947bc68444a097c97cce54cbc6ec8344313173346f1fc6d1f68876cd9", "TLrpNTBuCpGMrB9TyVwgEhNVRhtWEQPHh4"},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.path, func(t *testing.T) {
|
||||||
|
key, err := DerivePath(seed, tc.path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DerivePath: %v", err)
|
||||||
|
}
|
||||||
|
if got := hex.EncodeToString(key); got != tc.key {
|
||||||
|
t.Fatalf("key = %s, want %s", got, tc.key)
|
||||||
|
}
|
||||||
|
point, err := secp256k1.PublicKey(key)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("PublicKey: %v", err)
|
||||||
|
}
|
||||||
|
raw := make([]byte, 64)
|
||||||
|
point.X.FillBytes(raw[0:32])
|
||||||
|
point.Y.FillBytes(raw[32:64])
|
||||||
|
if got := addressOf(raw); got != tc.addr {
|
||||||
|
t.Fatalf("addr = %s, want %s", got, tc.addr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSeedWithPassphrase(t *testing.T) {
|
||||||
|
const mn = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||||
|
seed := bip39.Seed(mn, "TREZOR")
|
||||||
|
if got := hex.EncodeToString(seed); got != "c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e53495531f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04" {
|
||||||
|
t.Fatalf("seed = %s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParsePath(t *testing.T) {
|
||||||
|
idx, err := ParsePath("m/44'/195'/0'/0/0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ParsePath: %v", err)
|
||||||
|
}
|
||||||
|
want := []uint32{0x8000002c, 0x800000c3, 0x80000000, 0, 0}
|
||||||
|
if len(idx) != len(want) {
|
||||||
|
t.Fatalf("len = %d want %d", len(idx), len(want))
|
||||||
|
}
|
||||||
|
for i := range want {
|
||||||
|
if idx[i] != want[i] {
|
||||||
|
t.Fatalf("idx[%d] = %#x want %#x", i, idx[i], want[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := ParsePath("44/195"); err == nil {
|
||||||
|
t.Fatalf("expected error for path without m")
|
||||||
|
}
|
||||||
|
if _, err := ParsePath("m/x"); err == nil {
|
||||||
|
t.Fatalf("expected error for non-numeric node")
|
||||||
|
}
|
||||||
|
}
|
||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
// Package cli provides result formatting and interactive confirmation that
|
||||||
|
// reproduce the original kisstron.py presult() and trconfirm() behaviour.
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Result is the outcome of an operation, mirroring the original
|
||||||
|
// {success, message, result} dictionary returned by every kisstron function.
|
||||||
|
type Result struct {
|
||||||
|
Success bool
|
||||||
|
Message string
|
||||||
|
Result any
|
||||||
|
}
|
||||||
|
|
||||||
|
var reader = bufio.NewReader(os.Stdin)
|
||||||
|
|
||||||
|
// formatResult renders the presult() output: a "Success:" or "Error:" header,
|
||||||
|
// the message, and a trailing blank line.
|
||||||
|
func formatResult(r Result) string {
|
||||||
|
header := "Error:"
|
||||||
|
if r.Success {
|
||||||
|
header = "Success:"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s\n%s\n\n", header, r.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrintResult reproduces presult() exactly.
|
||||||
|
func PrintResult(r Result) {
|
||||||
|
fmt.Print(formatResult(r))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadLine prints prompt and reads a single line from stdin.
|
||||||
|
func ReadLine(prompt string) string {
|
||||||
|
fmt.Print(prompt)
|
||||||
|
line, err := reader.ReadString('\n')
|
||||||
|
if err != nil && line == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.TrimRight(line, "\r\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm asks the user to type "yes" and aborts with exit code 0 otherwise,
|
||||||
|
// matching trconfirm().
|
||||||
|
func Confirm(prompt string) {
|
||||||
|
if strings.ToLower(ReadLine(prompt)) != "yes" {
|
||||||
|
fmt.Println("Transaction not confirmed, aborting!")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPrintResultSuccess(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
_ = buf
|
||||||
|
// PrintResult writes to stdout; capture via the shared writer instead of
|
||||||
|
// swapping os.Stdout to keep the test hermetic.
|
||||||
|
got := formatResult(Result{Success: true, Message: "Wallet created"})
|
||||||
|
want := "Success:\nWallet created\n\n"
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("formatResult = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrintResultError(t *testing.T) {
|
||||||
|
got := formatResult(Result{Message: "Invalid private key!"})
|
||||||
|
want := "Error:\nInvalid private key!\n\n"
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("formatResult = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadLineTrimsNewline(t *testing.T) {
|
||||||
|
reader = bufio.NewReader(strings.NewReader("hello\r\n"))
|
||||||
|
if got := ReadLine(""); got != "hello" {
|
||||||
|
t.Fatalf("ReadLine = %q, want %q", got, "hello")
|
||||||
|
}
|
||||||
|
reader = bufio.NewReader(strings.NewReader("yes\n"))
|
||||||
|
if got := ReadLine(""); got != "yes" {
|
||||||
|
t.Fatalf("ReadLine = %q, want %q", got, "yes")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
// Package keccak implements the legacy Keccak-256 hash used by TRON and
|
||||||
|
// Ethereum. The Go standard library only exposes FIPS-202 SHA3/SHAKE, which
|
||||||
|
// differ in the domain-separation padding byte (0x06 vs 0x01), so the Keccak
|
||||||
|
// permutation is implemented directly here.
|
||||||
|
package keccak
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"hash"
|
||||||
|
)
|
||||||
|
|
||||||
|
// rate is the Keccak-256 sponge rate in bytes (1600 - 2*256 bits).
|
||||||
|
const rate = 136
|
||||||
|
|
||||||
|
var roundConstants = [24]uint64{
|
||||||
|
0x0000000000000001, 0x0000000000008082, 0x800000000000808a, 0x8000000080008000,
|
||||||
|
0x000000000000808b, 0x0000000080000001, 0x8000000080008081, 0x8000000000008009,
|
||||||
|
0x000000000000008a, 0x0000000000000088, 0x0000000080008009, 0x000000008000000a,
|
||||||
|
0x000000008000808b, 0x800000000000008b, 0x8000000000008089, 0x8000000000008003,
|
||||||
|
0x8000000000008002, 0x8000000000000080, 0x000000000000800a, 0x800000008000000a,
|
||||||
|
0x8000000080008081, 0x8000000000008080, 0x0000000080000001, 0x8000000080008008,
|
||||||
|
}
|
||||||
|
|
||||||
|
// rotation offsets and pi lane permutation for the rho/pi steps.
|
||||||
|
var rhoOffsets = [24]int{1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 2, 14, 27, 41, 56, 8, 25, 43, 62, 18, 39, 61, 20, 44}
|
||||||
|
var piLanes = [24]int{10, 7, 11, 17, 18, 3, 5, 16, 8, 21, 24, 4, 15, 23, 19, 13, 12, 2, 20, 14, 22, 9, 6, 1}
|
||||||
|
|
||||||
|
func rotl(x uint64, n int) uint64 {
|
||||||
|
return (x << n) | (x >> (64 - n))
|
||||||
|
}
|
||||||
|
|
||||||
|
// permute applies the Keccak-f[1600] permutation in place.
|
||||||
|
func permute(a *[25]uint64) {
|
||||||
|
for round := 0; round < 24; round++ {
|
||||||
|
// theta
|
||||||
|
var c [5]uint64
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
c[i] = a[i] ^ a[i+5] ^ a[i+10] ^ a[i+15] ^ a[i+20]
|
||||||
|
}
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
d := c[(i+4)%5] ^ rotl(c[(i+1)%5], 1)
|
||||||
|
for j := 0; j < 25; j += 5 {
|
||||||
|
a[j+i] ^= d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// rho and pi
|
||||||
|
last := a[1]
|
||||||
|
for i := 0; i < 24; i++ {
|
||||||
|
j := piLanes[i]
|
||||||
|
last, a[j] = a[j], rotl(last, rhoOffsets[i])
|
||||||
|
}
|
||||||
|
// chi
|
||||||
|
for j := 0; j < 25; j += 5 {
|
||||||
|
var row [5]uint64
|
||||||
|
copy(row[:], a[j:j+5])
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
a[j+i] = row[i] ^ (^row[(i+1)%5] & row[(i+2)%5])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// iota
|
||||||
|
a[0] ^= roundConstants[round]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// State is a streaming Keccak-256 sponge state.
|
||||||
|
type State struct {
|
||||||
|
a [25]uint64
|
||||||
|
buf [rate]byte
|
||||||
|
n int
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a fresh Keccak-256 hash state.
|
||||||
|
func New() *State {
|
||||||
|
return &State{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *State) absorbBlock(b *[rate]byte) {
|
||||||
|
for i := 0; i < rate/8; i++ {
|
||||||
|
s.a[i] ^= binary.LittleEndian.Uint64(b[i*8:])
|
||||||
|
}
|
||||||
|
permute(&s.a)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write absorbs data into the sponge. It never returns an error.
|
||||||
|
func (s *State) Write(p []byte) (int, error) {
|
||||||
|
total := len(p)
|
||||||
|
for len(p) > 0 {
|
||||||
|
n := copy(s.buf[s.n:], p)
|
||||||
|
s.n += n
|
||||||
|
p = p[n:]
|
||||||
|
if s.n == rate {
|
||||||
|
s.absorbBlock(&s.buf)
|
||||||
|
s.n = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sum256 returns the Keccak-256 digest without disturbing the state.
|
||||||
|
func (s *State) Sum256() [32]byte {
|
||||||
|
clone := *s
|
||||||
|
var out [32]byte
|
||||||
|
clone.finalize(&out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sum appends the current digest to b, implementing hash.Hash.
|
||||||
|
func (s *State) Sum(b []byte) []byte {
|
||||||
|
d := s.Sum256()
|
||||||
|
return append(b, d[:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset clears the state to its initial value.
|
||||||
|
func (s *State) Reset() {
|
||||||
|
s.a = [25]uint64{}
|
||||||
|
s.buf = [rate]byte{}
|
||||||
|
s.n = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size returns the digest length in bytes.
|
||||||
|
func (s *State) Size() int { return 32 }
|
||||||
|
|
||||||
|
// BlockSize returns the sponge rate in bytes.
|
||||||
|
func (s *State) BlockSize() int { return rate }
|
||||||
|
|
||||||
|
func (s *State) finalize(out *[32]byte) {
|
||||||
|
var block [rate]byte
|
||||||
|
copy(block[:], s.buf[:s.n])
|
||||||
|
block[s.n] ^= 0x01
|
||||||
|
block[rate-1] ^= 0x80
|
||||||
|
s.absorbBlock(&block)
|
||||||
|
for i := 0; i < 4; i++ {
|
||||||
|
binary.LittleEndian.PutUint64(out[i*8:], s.a[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash256 returns the Keccak-256 digest of data.
|
||||||
|
func Hash256(data []byte) [32]byte {
|
||||||
|
h := New()
|
||||||
|
h.Write(data)
|
||||||
|
return h.Sum256()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sum256 is a package-level convenience matching crypto/sha256.Sum256.
|
||||||
|
func Sum256(data []byte) [32]byte {
|
||||||
|
return Hash256(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// compile-time assurance that State satisfies hash.Hash.
|
||||||
|
var _ hash.Hash = (*State)(nil)
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package keccak
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestKeccak256KnownAnswers(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
in string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"empty", "", "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"},
|
||||||
|
{
|
||||||
|
"abc",
|
||||||
|
"abc",
|
||||||
|
"4e03657aea45a94fc7d47ba826c8d667c0d1e6e33a64a036ec44f58fa12d6c45",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"longer",
|
||||||
|
"The quick brown fox jumps over the lazy dog",
|
||||||
|
"4d741b6f1eb29cb2a9b9911c82f56fa8d73b04959d3d9d222895df6c0b28aa15",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
sum := Hash256([]byte(tc.in))
|
||||||
|
got := hex.EncodeToString(sum[:])
|
||||||
|
if got != tc.want {
|
||||||
|
t.Fatalf("Hash256(%q) = %s, want %s", tc.in, got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStreamingMatchesOneShot(t *testing.T) {
|
||||||
|
data := make([]byte, 0, 500)
|
||||||
|
for i := 0; i < 500; i++ {
|
||||||
|
data = append(data, byte(i))
|
||||||
|
}
|
||||||
|
oneShot := Hash256(data)
|
||||||
|
|
||||||
|
h := New()
|
||||||
|
for i := 0; i < len(data); i += 7 {
|
||||||
|
end := i + 7
|
||||||
|
if end > len(data) {
|
||||||
|
end = len(data)
|
||||||
|
}
|
||||||
|
h.Write(data[i:end])
|
||||||
|
}
|
||||||
|
if got := h.Sum256(); got != oneShot {
|
||||||
|
t.Fatalf("streaming digest differs from one-shot")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSum256DoesNotDisturbState ensures Sum256 clones the state (hash.Hash
|
||||||
|
// semantics) so repeated calls are stable.
|
||||||
|
func TestSum256DoesNotDisturbState(t *testing.T) {
|
||||||
|
h := New()
|
||||||
|
h.Write([]byte("abc"))
|
||||||
|
first := h.Sum256()
|
||||||
|
second := h.Sum256()
|
||||||
|
if first != second {
|
||||||
|
t.Fatalf("Sum256 mutated state")
|
||||||
|
}
|
||||||
|
if first != Hash256([]byte("abc")) {
|
||||||
|
t.Fatalf("Sum256 mismatch with Hash256")
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-1
@@ -38,7 +38,7 @@ KT_FEE_LIMIT = 100000000 # transaction fee limit for mainnet (in Sun)
|
|||||||
KT_TEST_FEE_LIMIT = 1000000000 # transaction fee limit for testnets (in Sun)
|
KT_TEST_FEE_LIMIT = 1000000000 # transaction fee limit for testnets (in Sun)
|
||||||
TRX_SCALE = 1000000.0 # TRX scale (million Suns in 1 TRX)
|
TRX_SCALE = 1000000.0 # TRX scale (million Suns in 1 TRX)
|
||||||
TG_API_KEY = None # TronGrid API Key (unused by default)
|
TG_API_KEY = None # TronGrid API Key (unused by default)
|
||||||
NODE_ADDR = 'http://18.196.99.16:8090' # public Tron node (Germany)
|
NODE_ADDR = 'http://3.225.171.164:8090' # public Tron node (Germany)
|
||||||
|
|
||||||
# TRC20 token definitions (also hardcoded)
|
# TRC20 token definitions (also hardcoded)
|
||||||
# Make sure the mainnet tokens match the contract addresses
|
# Make sure the mainnet tokens match the contract addresses
|
||||||
@@ -57,6 +57,11 @@ KT_TRC20_TOKENS = {
|
|||||||
'contract': 'TEkxiTehnzSmSe2XqrBj4w32RUN966rdz8',
|
'contract': 'TEkxiTehnzSmSe2XqrBj4w32RUN966rdz8',
|
||||||
'ticker': 'USDC',
|
'ticker': 'USDC',
|
||||||
'show_balance': True
|
'show_balance': True
|
||||||
|
},
|
||||||
|
'tusd': {
|
||||||
|
'contract': 'TUpMhErZL2fhh4sVNULAbNKLokS4GjC1F4',
|
||||||
|
'ticker': 'TUSD',
|
||||||
|
'show_balance': True
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'nile': {
|
'nile': {
|
||||||
|
|||||||
@@ -0,0 +1,309 @@
|
|||||||
|
// 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
|
||||||
|
}
|
||||||
+385
@@ -0,0 +1,385 @@
|
|||||||
|
// 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"
|
||||||
|
}
|
||||||
@@ -0,0 +1,292 @@
|
|||||||
|
// Package secp256k1 implements the secp256k1 elliptic curve arithmetic and
|
||||||
|
// recoverable ECDSA signing used by TRON. The Go standard library does not
|
||||||
|
// provide this curve, so it is built directly on math/big following SEC 1.
|
||||||
|
package secp256k1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"errors"
|
||||||
|
"math/big"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Curve parameters for secp256k1.
|
||||||
|
var (
|
||||||
|
// P is the field prime.
|
||||||
|
P, _ = new(big.Int).SetString("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F", 16)
|
||||||
|
// N is the group order.
|
||||||
|
N, _ = new(big.Int).SetString("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", 16)
|
||||||
|
// Gx, Gy are the coordinates of the base point.
|
||||||
|
Gx, _ = new(big.Int).SetString("79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798", 16)
|
||||||
|
Gy, _ = new(big.Int).SetString("483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8", 16)
|
||||||
|
|
||||||
|
halfN = new(big.Int).Rsh(N, 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Errors returned by the package.
|
||||||
|
var (
|
||||||
|
ErrInvalidKey = errors.New("secp256k1: invalid private key")
|
||||||
|
ErrInvalidSig = errors.New("secp256k1: invalid signature")
|
||||||
|
ErrNoRecover = errors.New("secp256k1: public key recovery failed")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Point is an affine point on the curve. A nil point is the point at infinity.
|
||||||
|
type Point struct {
|
||||||
|
X, Y *big.Int
|
||||||
|
}
|
||||||
|
|
||||||
|
func mod(x, m *big.Int) *big.Int {
|
||||||
|
r := new(big.Int).Mod(x, m)
|
||||||
|
if r.Sign() < 0 {
|
||||||
|
r.Add(r, m)
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add returns p + q on the curve.
|
||||||
|
func Add(p, q *Point) *Point {
|
||||||
|
if p == nil {
|
||||||
|
return q
|
||||||
|
}
|
||||||
|
if q == nil {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
if p.X.Cmp(q.X) == 0 {
|
||||||
|
if p.Y.Cmp(q.Y) != 0 || p.Y.Sign() == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return double(p)
|
||||||
|
}
|
||||||
|
num := mod(new(big.Int).Sub(q.Y, p.Y), P)
|
||||||
|
den := mod(new(big.Int).Sub(q.X, p.X), P)
|
||||||
|
den.ModInverse(den, P)
|
||||||
|
lam := mod(new(big.Int).Mul(num, den), P)
|
||||||
|
return applyLambda(p, q, lam)
|
||||||
|
}
|
||||||
|
|
||||||
|
func double(p *Point) *Point {
|
||||||
|
num := new(big.Int).Mul(p.X, p.X)
|
||||||
|
num.Mul(num, big.NewInt(3))
|
||||||
|
num = mod(num, P)
|
||||||
|
den := new(big.Int).Lsh(p.Y, 1)
|
||||||
|
den = mod(den, P)
|
||||||
|
den.ModInverse(den, P)
|
||||||
|
lam := mod(new(big.Int).Mul(num, den), P)
|
||||||
|
return applyLambda(p, p, lam)
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyLambda(p, q *Point, lam *big.Int) *Point {
|
||||||
|
x3 := new(big.Int).Mul(lam, lam)
|
||||||
|
x3.Sub(x3, p.X)
|
||||||
|
x3.Sub(x3, q.X)
|
||||||
|
x3 = mod(x3, P)
|
||||||
|
y3 := new(big.Int).Sub(p.X, x3)
|
||||||
|
y3.Mul(lam, y3)
|
||||||
|
y3.Sub(y3, p.Y)
|
||||||
|
y3 = mod(y3, P)
|
||||||
|
return &Point{X: x3, Y: y3}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScalarBaseMult returns k*G.
|
||||||
|
func ScalarBaseMult(k *big.Int) *Point {
|
||||||
|
return ScalarMult(k, &Point{X: Gx, Y: Gy})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScalarMult returns k*p using a double-and-add ladder.
|
||||||
|
func ScalarMult(k *big.Int, p *Point) *Point {
|
||||||
|
var r *Point
|
||||||
|
if p == nil || k.Sign() == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
kk := mod(k, N)
|
||||||
|
for i := kk.BitLen() - 1; i >= 0; i-- {
|
||||||
|
r = Add(r, r)
|
||||||
|
if kk.Bit(i) == 1 {
|
||||||
|
r = Add(r, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsOnCurve reports whether p satisfies the curve equation.
|
||||||
|
func IsOnCurve(p *Point) bool {
|
||||||
|
if p == nil || p.X == nil || p.Y == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if p.X.Sign() < 0 || p.X.Cmp(P) >= 0 || p.Y.Sign() < 0 || p.Y.Cmp(P) >= 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
left := mod(new(big.Int).Mul(p.Y, p.Y), P)
|
||||||
|
right := new(big.Int).Mul(p.X, p.X)
|
||||||
|
right.Mul(right, p.X)
|
||||||
|
right.Add(right, big.NewInt(7))
|
||||||
|
right = mod(right, P)
|
||||||
|
return left.Cmp(right) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublicKey derives the public key point from a 32-byte private key.
|
||||||
|
func PublicKey(priv []byte) (*Point, error) {
|
||||||
|
if len(priv) != 32 {
|
||||||
|
return nil, ErrInvalidKey
|
||||||
|
}
|
||||||
|
k := new(big.Int).SetBytes(priv)
|
||||||
|
if k.Sign() == 0 || k.Cmp(N) >= 0 {
|
||||||
|
return nil, ErrInvalidKey
|
||||||
|
}
|
||||||
|
return ScalarBaseMult(k), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signature is a recoverable ECDSA signature (r, s) with recovery id V.
|
||||||
|
type Signature struct {
|
||||||
|
R, S *big.Int
|
||||||
|
V byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes returns the 65-byte compact representation r || s || v.
|
||||||
|
func (sig *Signature) Bytes() []byte {
|
||||||
|
out := make([]byte, 65)
|
||||||
|
sig.R.FillBytes(out[0:32])
|
||||||
|
sig.S.FillBytes(out[32:64])
|
||||||
|
out[64] = sig.V
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign signs a 32-byte message hash with the given private key, producing a
|
||||||
|
// deterministic recoverable signature (libsecp256k1-compatible nonce, low-s
|
||||||
|
// form). This matches coincurve/libsecp256k1's default recoverable signing.
|
||||||
|
func Sign(priv, hash []byte) (*Signature, error) {
|
||||||
|
if len(priv) != 32 {
|
||||||
|
return nil, ErrInvalidKey
|
||||||
|
}
|
||||||
|
if len(hash) != 32 {
|
||||||
|
return nil, ErrInvalidSig
|
||||||
|
}
|
||||||
|
d := new(big.Int).SetBytes(priv)
|
||||||
|
if d.Sign() == 0 || d.Cmp(N) >= 0 {
|
||||||
|
return nil, ErrInvalidKey
|
||||||
|
}
|
||||||
|
e := mod(new(big.Int).SetBytes(hash), N)
|
||||||
|
|
||||||
|
for {
|
||||||
|
k := nonceRFC6979(priv, hash)
|
||||||
|
kp := ScalarBaseMult(k)
|
||||||
|
if kp == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
r := mod(kp.X, N)
|
||||||
|
if r.Sign() == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
kinv := new(big.Int).ModInverse(k, N)
|
||||||
|
s := new(big.Int).Mul(r, d)
|
||||||
|
s.Add(s, e)
|
||||||
|
s.Mul(s, kinv)
|
||||||
|
s.Mod(s, N)
|
||||||
|
if s.Sign() == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
v := byte(kp.Y.Bit(0))
|
||||||
|
if s.Cmp(halfN) > 0 {
|
||||||
|
s.Sub(N, s)
|
||||||
|
v ^= 1
|
||||||
|
}
|
||||||
|
return &Signature{R: r, S: s, V: v}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// nonceRFC6979 reproduces libsecp256k1's default RFC 6979 nonce function. It
|
||||||
|
// feeds HMAC-SHA256 with the private key, the message and 48 zero bytes of
|
||||||
|
// optional data, exactly as secp256k1_nonce_function_rfc6979 does when no
|
||||||
|
// extra entropy is supplied.
|
||||||
|
func nonceRFC6979(priv, hash []byte) *big.Int {
|
||||||
|
// libsecp256k1's rfc6979 default nonce feeds exactly key32||msg32 (no
|
||||||
|
// extra data, no algo16), i.e. a 64-byte HMAC key.
|
||||||
|
keydata := make([]byte, 64)
|
||||||
|
copy(keydata[0:32], priv)
|
||||||
|
copy(keydata[32:64], hash)
|
||||||
|
|
||||||
|
v := make([]byte, 32)
|
||||||
|
for i := range v {
|
||||||
|
v[i] = 0x01
|
||||||
|
}
|
||||||
|
k := make([]byte, 32)
|
||||||
|
|
||||||
|
k = hmacSHA256(k, v, []byte{0x00}, keydata[:])
|
||||||
|
v = hmacSHA256(k, v)
|
||||||
|
k = hmacSHA256(k, v, []byte{0x01}, keydata[:])
|
||||||
|
v = hmacSHA256(k, v)
|
||||||
|
|
||||||
|
for {
|
||||||
|
v = hmacSHA256(k, v)
|
||||||
|
candidate := new(big.Int).SetBytes(v)
|
||||||
|
if candidate.Sign() > 0 && candidate.Cmp(N) < 0 {
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
k = hmacSHA256(k, v, []byte{0x00})
|
||||||
|
v = hmacSHA256(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hmacSHA256(key []byte, parts ...[]byte) []byte {
|
||||||
|
mac := hmac.New(sha256.New, key)
|
||||||
|
for _, p := range parts {
|
||||||
|
mac.Write(p)
|
||||||
|
}
|
||||||
|
return mac.Sum(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecoverPublicKey recovers the public key point from a message hash and a
|
||||||
|
// recoverable signature.
|
||||||
|
func RecoverPublicKey(hash []byte, sig *Signature) (*Point, error) {
|
||||||
|
if sig == nil || sig.R == nil || sig.S == nil || len(hash) != 32 {
|
||||||
|
return nil, ErrInvalidSig
|
||||||
|
}
|
||||||
|
if sig.R.Sign() == 0 || sig.S.Sign() == 0 || sig.R.Cmp(N) >= 0 || sig.S.Cmp(N) >= 0 {
|
||||||
|
return nil, ErrInvalidSig
|
||||||
|
}
|
||||||
|
x := new(big.Int).Set(sig.R)
|
||||||
|
if sig.V&2 != 0 {
|
||||||
|
x.Add(x, N)
|
||||||
|
}
|
||||||
|
if x.Cmp(P) >= 0 {
|
||||||
|
return nil, ErrNoRecover
|
||||||
|
}
|
||||||
|
alpha := new(big.Int).Mul(x, x)
|
||||||
|
alpha.Mul(alpha, x)
|
||||||
|
alpha.Add(alpha, big.NewInt(7))
|
||||||
|
alpha = mod(alpha, P)
|
||||||
|
beta := sqrtMod(alpha)
|
||||||
|
if beta == nil {
|
||||||
|
return nil, ErrNoRecover
|
||||||
|
}
|
||||||
|
var y *big.Int
|
||||||
|
if byte(beta.Bit(0)) == sig.V&1 {
|
||||||
|
y = beta
|
||||||
|
} else {
|
||||||
|
y = new(big.Int).Sub(P, beta)
|
||||||
|
}
|
||||||
|
rp := &Point{X: x, Y: y}
|
||||||
|
|
||||||
|
e := mod(new(big.Int).SetBytes(hash), N)
|
||||||
|
rinv := new(big.Int).ModInverse(sig.R, N)
|
||||||
|
u1 := mod(new(big.Int).Neg(e), N)
|
||||||
|
u1.Mul(u1, rinv)
|
||||||
|
u1.Mod(u1, N)
|
||||||
|
u2 := mod(new(big.Int).Mul(sig.S, rinv), N)
|
||||||
|
q := Add(ScalarBaseMult(u1), ScalarMult(u2, rp))
|
||||||
|
if q == nil || !IsOnCurve(q) {
|
||||||
|
return nil, ErrNoRecover
|
||||||
|
}
|
||||||
|
return q, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sqrtMod returns a square root of a modulo P (P = 3 mod 4), or nil.
|
||||||
|
func sqrtMod(a *big.Int) *big.Int {
|
||||||
|
exp := new(big.Int).Add(P, big.NewInt(1))
|
||||||
|
exp.Rsh(exp, 2)
|
||||||
|
r := new(big.Int).Exp(a, exp, P)
|
||||||
|
if mod(new(big.Int).Mul(r, r), P).Cmp(mod(a, P)) != 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
package secp256k1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"math/big"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mustDecodeHex(t *testing.T, s string) []byte {
|
||||||
|
t.Helper()
|
||||||
|
b, err := hex.DecodeString(s)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decode hex: %v", err)
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBasePointOnCurve confirms the generator satisfies y^2 = x^3 + 7.
|
||||||
|
func TestBasePointOnCurve(t *testing.T) {
|
||||||
|
if !IsOnCurve(&Point{X: Gx, Y: Gy}) {
|
||||||
|
t.Fatalf("generator is not on the curve")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPointDoublingMatchesScalarMult verifies group consistency: 2*G computed
|
||||||
|
// via point doubling equals k*G for k=2.
|
||||||
|
func TestPointDoublingMatchesScalarMult(t *testing.T) {
|
||||||
|
g := &Point{X: Gx, Y: Gy}
|
||||||
|
viaDouble := Add(g, g)
|
||||||
|
viaScalar := ScalarBaseMult(big.NewInt(2))
|
||||||
|
if viaDouble.X.Cmp(viaScalar.X) != 0 || viaDouble.Y.Cmp(viaScalar.Y) != 0 {
|
||||||
|
t.Fatalf("2G mismatch: double=%v scalar=%v", viaDouble, viaScalar)
|
||||||
|
}
|
||||||
|
if !IsOnCurve(viaScalar) {
|
||||||
|
t.Fatalf("2G not on curve")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestScalarMultDistributes checks (a+b)G == aG + bG.
|
||||||
|
func TestScalarMultDistributes(t *testing.T) {
|
||||||
|
a := big.NewInt(12345)
|
||||||
|
b := big.NewInt(67890)
|
||||||
|
sum := new(big.Int).Add(a, b)
|
||||||
|
left := ScalarBaseMult(sum)
|
||||||
|
right := Add(ScalarBaseMult(a), ScalarBaseMult(b))
|
||||||
|
if left.X.Cmp(right.X) != 0 || left.Y.Cmp(right.Y) != 0 {
|
||||||
|
t.Fatalf("(a+b)G != aG + bG")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPublicKeyKnownValues locks the uncompressed public key for the t3 key.
|
||||||
|
func TestPublicKeyKnownValues(t *testing.T) {
|
||||||
|
priv := mustDecodeHex(t, "a96135266ba58bb5b3a450b8fe7a9d59ea3e6cb0cdb2300e8138d2d2d48e61a8")
|
||||||
|
point, err := PublicKey(priv)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("PublicKey: %v", err)
|
||||||
|
}
|
||||||
|
raw := make([]byte, 64)
|
||||||
|
point.X.FillBytes(raw[0:32])
|
||||||
|
point.Y.FillBytes(raw[32:64])
|
||||||
|
want := "076e3db946fcb53d575c4e742a9f8ff8c4b57ded53776b2a80d13022e1647adaa1641f3e2210f5f6a3bdd73d073ac029ba79d576093d8a92dd148e448a8e63cb"
|
||||||
|
if got := hex.EncodeToString(raw); got != want {
|
||||||
|
t.Fatalf("pubkey = %s, want %s", got, want)
|
||||||
|
}
|
||||||
|
if !IsOnCurve(point) {
|
||||||
|
t.Fatalf("derived public key not on curve")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSignRecoverRoundTrip signs and recovers for several keys and hashes.
|
||||||
|
func TestSignRecoverRoundTrip(t *testing.T) {
|
||||||
|
privs := []string{
|
||||||
|
"a96135266ba58bb5b3a450b8fe7a9d59ea3e6cb0cdb2300e8138d2d2d48e61a8",
|
||||||
|
"3252a4cb9b3865c94dbb7d6f50107ca704c1c2b9e3d1eb81753e221c7db8fa25",
|
||||||
|
"993638df18a38ef2be9f8c915eb9063647d015edc8fb70e3bcb32469bee39c21",
|
||||||
|
}
|
||||||
|
hashes := []string{
|
||||||
|
"e93945efde601eed484553338e58218f4e81c51261dfda72db2d874f75378fe8",
|
||||||
|
"0000000000000000000000000000000000000000000000000000000000000001",
|
||||||
|
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||||
|
}
|
||||||
|
for _, p := range privs {
|
||||||
|
priv := mustDecodeHex(t, p)
|
||||||
|
wantPoint, _ := PublicKey(priv)
|
||||||
|
for _, h := range hashes {
|
||||||
|
hash := mustDecodeHex(t, h)
|
||||||
|
sig, err := Sign(priv, hash)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Sign: %v", err)
|
||||||
|
}
|
||||||
|
// low-s enforcement
|
||||||
|
if sig.S.Cmp(halfN) > 0 {
|
||||||
|
t.Fatalf("signature s is not low-s")
|
||||||
|
}
|
||||||
|
recovered, err := RecoverPublicKey(hash, sig)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("RecoverPublicKey: %v", err)
|
||||||
|
}
|
||||||
|
if recovered.X.Cmp(wantPoint.X) != 0 || recovered.Y.Cmp(wantPoint.Y) != 0 {
|
||||||
|
t.Fatalf("recovered key differs for priv=%s hash=%s", p, h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSignIsDeterministic confirms RFC6979-style reproducibility.
|
||||||
|
func TestSignIsDeterministic(t *testing.T) {
|
||||||
|
priv := mustDecodeHex(t, "a96135266ba58bb5b3a450b8fe7a9d59ea3e6cb0cdb2300e8138d2d2d48e61a8")
|
||||||
|
hash := mustDecodeHex(t, "e93945efde601eed484553338e58218f4e81c51261dfda72db2d874f75378fe8")
|
||||||
|
a, err := Sign(priv, hash)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Sign: %v", err)
|
||||||
|
}
|
||||||
|
b, err := Sign(priv, hash)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Sign: %v", err)
|
||||||
|
}
|
||||||
|
if hex.EncodeToString(a.Bytes()) != hex.EncodeToString(b.Bytes()) {
|
||||||
|
t.Fatalf("signatures differ for identical inputs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPublicKeyRejectsInvalid(t *testing.T) {
|
||||||
|
if _, err := PublicKey(make([]byte, 31)); err == nil {
|
||||||
|
t.Fatalf("expected error for short key")
|
||||||
|
}
|
||||||
|
if _, err := PublicKey(make([]byte, 32)); err == nil {
|
||||||
|
t.Fatalf("expected error for zero key")
|
||||||
|
}
|
||||||
|
}
|
||||||
+121
@@ -0,0 +1,121 @@
|
|||||||
|
package tron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"code.luxferre.top/luxferre/kisstron/base58"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ToBase58CheckAddress converts an address given in any accepted format
|
||||||
|
// (base58check, 41-prefixed hex, or 0x-prefixed hex) to its base58check
|
||||||
|
// representation, verifying the checksum. The argument may be a string or a
|
||||||
|
// byte slice.
|
||||||
|
func ToBase58CheckAddress(raw any) (string, error) {
|
||||||
|
switch v := raw.(type) {
|
||||||
|
case string:
|
||||||
|
return strToBase58Check(v)
|
||||||
|
case []byte:
|
||||||
|
return bytesToBase58Check(v)
|
||||||
|
}
|
||||||
|
return "", ErrBadAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
func strToBase58Check(s string) (string, error) {
|
||||||
|
if len(s) == 0 {
|
||||||
|
return "", ErrBadAddress
|
||||||
|
}
|
||||||
|
if s[0] == 'T' && len(s) == 34 {
|
||||||
|
if !base58.IsValid(s) {
|
||||||
|
return "", ErrBadAddress
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
if len(s) == 42 {
|
||||||
|
if strings.HasPrefix(s, "0x") {
|
||||||
|
b, err := hex.DecodeString(s[2:])
|
||||||
|
if err != nil || len(b) != 20 {
|
||||||
|
return "", ErrBadAddress
|
||||||
|
}
|
||||||
|
return base58.CheckEncode(append([]byte{0x41}, b...)), nil
|
||||||
|
}
|
||||||
|
b, err := hex.DecodeString(s)
|
||||||
|
if err != nil || len(b) != 21 {
|
||||||
|
return "", ErrBadAddress
|
||||||
|
}
|
||||||
|
return base58.CheckEncode(b), nil
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(s, "0x") && len(s) == 44 {
|
||||||
|
b, err := hex.DecodeString(s[2:])
|
||||||
|
if err != nil || len(b) != 21 {
|
||||||
|
return "", ErrBadAddress
|
||||||
|
}
|
||||||
|
return base58.CheckEncode(b), nil
|
||||||
|
}
|
||||||
|
return "", ErrBadAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
func bytesToBase58Check(b []byte) (string, error) {
|
||||||
|
switch {
|
||||||
|
case len(b) == 21 && b[0] == 0x41:
|
||||||
|
return base58.CheckEncode(b), nil
|
||||||
|
case len(b) == 20:
|
||||||
|
return base58.CheckEncode(append([]byte{0x41}, b...)), nil
|
||||||
|
default:
|
||||||
|
return strToBase58Check(string(b))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToHexAddress converts an address to its 41-prefixed hex representation.
|
||||||
|
func ToHexAddress(raw any) (string, error) {
|
||||||
|
addr, err := ToBase58CheckAddress(raw)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
b, err := base58.CheckDecode(addr)
|
||||||
|
if err != nil {
|
||||||
|
return "", ErrBadAddress
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToRawAddress returns the 21-byte raw form of an address.
|
||||||
|
func ToRawAddress(raw any) ([]byte, error) {
|
||||||
|
addr, err := ToBase58CheckAddress(raw)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return base58.CheckDecode(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToTvmAddress returns the 20-byte TVM (Ethereum-style) form of an address.
|
||||||
|
func ToTvmAddress(raw any) ([]byte, error) {
|
||||||
|
b, err := ToRawAddress(raw)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return b[1:], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsBase58CheckAddress reports whether value is a valid base58check address.
|
||||||
|
func IsBase58CheckAddress(value string) bool {
|
||||||
|
if len(value) == 0 || value[0] != 'T' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
b, err := base58.CheckDecode(value)
|
||||||
|
return err == nil && len(b) == 21
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsHexAddress reports whether value is a valid 41-prefixed hex address.
|
||||||
|
func IsHexAddress(value string) bool {
|
||||||
|
if !strings.HasPrefix(value, "41") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
b, err := hex.DecodeString(value)
|
||||||
|
return err == nil && len(b) == 21
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAddress reports whether value is any accepted TRON address format.
|
||||||
|
func IsAddress(value string) bool {
|
||||||
|
return IsBase58CheckAddress(value) || IsHexAddress(value)
|
||||||
|
}
|
||||||
+151
@@ -0,0 +1,151 @@
|
|||||||
|
package tron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client is the subset of tronpy's Tron client used by kisstron.
|
||||||
|
type Client struct {
|
||||||
|
provider *HTTPProvider
|
||||||
|
network string
|
||||||
|
feeLimit int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient builds a client for the given network and node endpoint. A zero or
|
||||||
|
// empty endpoint selects the network default.
|
||||||
|
func NewClient(network, endpoint string, feeLimit int64, apiKey string) *Client {
|
||||||
|
return &Client{
|
||||||
|
provider: NewHTTPProvider(endpoint, 20*time.Second, apiKey),
|
||||||
|
network: network,
|
||||||
|
feeLimit: feeLimit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provider exposes the underlying HTTP provider (used for diagnostics).
|
||||||
|
func (c *Client) Provider() *HTTPProvider { return c.provider }
|
||||||
|
|
||||||
|
func (c *Client) GetAccount(addr string) (map[string]any, error) {
|
||||||
|
b58, err := ToBase58CheckAddress(addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ret, err := c.provider.makeRequest("wallet/getaccount", map[string]any{"address": b58, "visible": true})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(ret) == 0 {
|
||||||
|
return nil, ErrAddressNotFound
|
||||||
|
}
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetAccountResource(addr string) (map[string]any, error) {
|
||||||
|
b58, err := ToBase58CheckAddress(addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ret, err := c.provider.makeRequest("wallet/getaccountresource", map[string]any{"address": b58, "visible": true})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(ret) == 0 {
|
||||||
|
return nil, ErrAddressNotFound
|
||||||
|
}
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) getLatestSolidBlockID() (string, error) {
|
||||||
|
info, err := c.provider.makeRequest("wallet/getnodeinfo", nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
block, _ := info["solidityBlock"].(string)
|
||||||
|
if block == "" {
|
||||||
|
return "", ErrAPI
|
||||||
|
}
|
||||||
|
return block[strings.LastIndex(block, ",ID:")+4:], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) broadcast(tx *Transaction) (map[string]any, error) {
|
||||||
|
payload, err := c.provider.makeRequest("wallet/broadcasttransaction", tx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := handleAPIError(payload); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, ok := payload["txid"]; !ok {
|
||||||
|
payload["txid"] = tx.TxID
|
||||||
|
}
|
||||||
|
return payload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) getTransactionInfo(txid string) (map[string]any, error) {
|
||||||
|
if len(txid) != 64 {
|
||||||
|
return nil, ErrBadHash
|
||||||
|
}
|
||||||
|
ret, err := c.provider.makeRequest("wallet/gettransactioninfobyid", map[string]any{"value": txid, "visible": true})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := handleAPIError(ret); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(ret) == 0 {
|
||||||
|
return nil, ErrTransactionNotFound
|
||||||
|
}
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetContract(addr string) (*Contract, error) {
|
||||||
|
b58, err := ToBase58CheckAddress(addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
info, err := c.provider.makeRequest("wallet/getcontract", map[string]any{"value": b58, "visible": true})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := handleAPIError(info); err != nil {
|
||||||
|
return nil, wrapErr(ErrContractNotFound, err.Error())
|
||||||
|
}
|
||||||
|
return &Contract{Address: b58, client: c}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// triggerConstantContract performs a read-only contract call and returns the
|
||||||
|
// first constant result (hex, without 0x).
|
||||||
|
func (c *Client) triggerConstantContract(owner, contractAddr, functionSelector, parameter string) (string, error) {
|
||||||
|
ownerB58, err := ToBase58CheckAddress(owner)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
contractB58, err := ToBase58CheckAddress(contractAddr)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
ret, err := c.provider.makeRequest("wallet/triggerconstantcontract", map[string]any{
|
||||||
|
"owner_address": ownerB58,
|
||||||
|
"contract_address": contractB58,
|
||||||
|
"function_selector": functionSelector,
|
||||||
|
"parameter": parameter,
|
||||||
|
"visible": true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if err := handleAPIError(ret); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if res, ok := ret["result"].(map[string]any); ok {
|
||||||
|
if _, bad := res["message"]; bad {
|
||||||
|
return "", ErrTvm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
results, ok := ret["constant_result"].([]any)
|
||||||
|
if !ok || len(results) == 0 {
|
||||||
|
return "", ErrTvm
|
||||||
|
}
|
||||||
|
first, _ := results[0].(string)
|
||||||
|
return first, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package tron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"math/big"
|
||||||
|
|
||||||
|
"code.luxferre.top/luxferre/kisstron/keccak"
|
||||||
|
)
|
||||||
|
|
||||||
|
// zeroOwner is the owner address used for read-only TRC20 calls, matching
|
||||||
|
// tronpy's Contract default (0x41 followed by 20 zero bytes).
|
||||||
|
const zeroOwner = "410000000000000000000000000000000000000000"
|
||||||
|
|
||||||
|
// selector returns the 4-byte ABI function selector (as hex) for a signature.
|
||||||
|
func selector(signature string) string {
|
||||||
|
h := keccak.Sum256([]byte(signature))
|
||||||
|
return hex.EncodeToString(h[:4])
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
selectorDecimals = selector("decimals()")
|
||||||
|
selectorBalanceOf = selector("balanceOf(address)")
|
||||||
|
selectorTransfer = selector("transfer(address,uint256)")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Contract is a minimal handle to a TRC20 contract. Only the three TRC20 calls
|
||||||
|
// used by kisstron (decimals, balanceOf, transfer) are supported, so no ABI is
|
||||||
|
// fetched or parsed.
|
||||||
|
type Contract struct {
|
||||||
|
Address string
|
||||||
|
client *Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decimals returns the token's decimal precision.
|
||||||
|
func (c *Contract) Decimals() (int, error) {
|
||||||
|
res, err := c.client.triggerConstantContract(zeroOwner, c.Address, "decimals()", "")
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
v, err := decodeUint256(res)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return int(v.Int64()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BalanceOf returns the raw token balance of addr.
|
||||||
|
func (c *Contract) BalanceOf(addr string) (*big.Int, error) {
|
||||||
|
param, err := encodeAddressParam(addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res, err := c.client.triggerConstantContract(zeroOwner, c.Address, "balanceOf(address)", param)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return decodeUint256(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transfer builds a signed-ready TRC20 transfer transaction builder.
|
||||||
|
func (c *Contract) Transfer(from, to string, amount *big.Int) (*TransactionBuilder, error) {
|
||||||
|
fromAddr, err := encodeAddressParam(to)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fromHex, err := ToHexAddress(from)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
contractHex, err := ToHexAddress(c.Address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
data := selectorTransfer + fromAddr + encodeUint256(amount)
|
||||||
|
return c.client.newTransactionBuilder("TriggerSmartContract", TriggerValue{
|
||||||
|
OwnerAddress: fromHex,
|
||||||
|
ContractAddress: contractHex,
|
||||||
|
Data: data,
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeAddressParam left-pads a TVM address to a 32-byte ABI word.
|
||||||
|
func encodeAddressParam(addr string) (string, error) {
|
||||||
|
tvm, err := ToTvmAddress(addr)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
word := make([]byte, 32)
|
||||||
|
copy(word[32-len(tvm):], tvm)
|
||||||
|
return hex.EncodeToString(word), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeUint256 encodes a non-negative integer as a 32-byte ABI word.
|
||||||
|
func encodeUint256(v *big.Int) string {
|
||||||
|
word := make([]byte, 32)
|
||||||
|
if v != nil {
|
||||||
|
v.FillBytes(word)
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(word)
|
||||||
|
}
|
||||||
|
|
||||||
|
// decodeUint256 decodes a 32-byte ABI word (hex) into a big integer.
|
||||||
|
func decodeUint256(hexStr string) (*big.Int, error) {
|
||||||
|
b, err := hex.DecodeString(hexStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ErrTvm
|
||||||
|
}
|
||||||
|
return new(big.Int).SetBytes(b), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
// Package tron implements the subset of TRON blockchain interaction required by
|
||||||
|
// the kisstron CLI wallet: key/address handling, HTTP node access, transaction
|
||||||
|
// building/signing/broadcasting and minimal TRC20 calls.
|
||||||
|
package tron
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
// Sentinel errors mirror tronpy.exceptions so API failures can be mapped the
|
||||||
|
// same way the original Python implementation does.
|
||||||
|
var (
|
||||||
|
ErrBadAddress = errors.New("invalid address")
|
||||||
|
ErrAddressNotFound = errors.New("account not found on-chain")
|
||||||
|
ErrBadKey = errors.New("invalid private key")
|
||||||
|
ErrBadSignature = errors.New("bad signature")
|
||||||
|
ErrBadHash = errors.New("wrong transaction hash length")
|
||||||
|
ErrTapos = errors.New("tapos error")
|
||||||
|
ErrTransaction = errors.New("transaction error")
|
||||||
|
ErrTvm = errors.New("tvm error")
|
||||||
|
ErrValidation = errors.New("validation error")
|
||||||
|
ErrAPI = errors.New("api error")
|
||||||
|
ErrNotFound = errors.New("not found")
|
||||||
|
ErrTransactionNotFound = errors.New("transaction not found")
|
||||||
|
ErrContractNotFound = errors.New("contract address not found")
|
||||||
|
)
|
||||||
|
|
||||||
|
// apiError carries a node message while unwrapping to a sentinel error, so its
|
||||||
|
// Error() string matches Python's `str(e)` (the bare message) and errors.Is
|
||||||
|
// still identifies the failure class.
|
||||||
|
type apiError struct {
|
||||||
|
sentinel error
|
||||||
|
msg string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *apiError) Error() string { return e.msg }
|
||||||
|
func (e *apiError) Unwrap() error { return e.sentinel }
|
||||||
|
|
||||||
|
// wrapErr attaches a message to a sentinel error.
|
||||||
|
func wrapErr(sentinel error, msg string) error {
|
||||||
|
if msg == "" {
|
||||||
|
return sentinel
|
||||||
|
}
|
||||||
|
return &apiError{sentinel: sentinel, msg: msg}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package tron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
|
||||||
|
"code.luxferre.top/luxferre/kisstron/base58"
|
||||||
|
"code.luxferre.top/luxferre/kisstron/keccak"
|
||||||
|
"code.luxferre.top/luxferre/kisstron/secp256k1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PrivateKey is a validated 32-byte secp256k1 private key.
|
||||||
|
type PrivateKey struct {
|
||||||
|
raw []byte
|
||||||
|
Pub *PublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublicKey is an uncompressed secp256k1 public key (64 bytes, x || y).
|
||||||
|
type PublicKey struct {
|
||||||
|
raw []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPrivateKey validates and wraps a 32-byte private key.
|
||||||
|
func NewPrivateKey(b []byte) (*PrivateKey, error) {
|
||||||
|
if _, err := secp256k1.PublicKey(b); err != nil {
|
||||||
|
return nil, ErrBadKey
|
||||||
|
}
|
||||||
|
return newPrivateKeyUnchecked(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPrivateKeyUnchecked(b []byte) *PrivateKey {
|
||||||
|
p, _ := secp256k1.PublicKey(b)
|
||||||
|
raw := make([]byte, 64)
|
||||||
|
p.X.FillBytes(raw[0:32])
|
||||||
|
p.Y.FillBytes(raw[32:64])
|
||||||
|
return &PrivateKey{raw: append([]byte{}, b...), Pub: &PublicKey{raw: raw}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromHex parses a hex-encoded private key.
|
||||||
|
func PrivateKeyFromHex(s string) (*PrivateKey, error) {
|
||||||
|
b, err := hex.DecodeString(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ErrBadKey
|
||||||
|
}
|
||||||
|
return NewPrivateKey(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes returns the 32-byte private key.
|
||||||
|
func (k *PrivateKey) Bytes() []byte {
|
||||||
|
return append([]byte{}, k.raw...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hex returns the private key as a hex string.
|
||||||
|
func (k *PrivateKey) Hex() string {
|
||||||
|
return hex.EncodeToString(k.raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes returns the uncompressed public key (64 bytes).
|
||||||
|
func (p *PublicKey) Bytes() []byte {
|
||||||
|
return append([]byte{}, p.raw...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hex returns the uncompressed public key as a hex string.
|
||||||
|
func (p *PublicKey) Hex() string {
|
||||||
|
return hex.EncodeToString(p.raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToBase58CheckAddress returns the TRON base58check address of the key.
|
||||||
|
func (p *PublicKey) ToBase58CheckAddress() string {
|
||||||
|
h := keccak.Sum256(p.raw)
|
||||||
|
payload := append([]byte{0x41}, h[12:]...)
|
||||||
|
return base58.CheckEncode(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToHexAddress returns the 41-prefixed hex address of the key.
|
||||||
|
func (p *PublicKey) ToHexAddress() string {
|
||||||
|
h := keccak.Sum256(p.raw)
|
||||||
|
return "41" + hex.EncodeToString(h[12:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign signs a 32-byte message hash (e.g. a transaction id) with the key.
|
||||||
|
func (k *PrivateKey) Sign(hash []byte) ([]byte, error) {
|
||||||
|
sig, err := secp256k1.Sign(k.raw, hash)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return sig.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateAddressInfo collects all display fields for a private key.
|
||||||
|
func GenerateAddressInfo(k *PrivateKey) map[string]string {
|
||||||
|
return map[string]string{
|
||||||
|
"base58check_address": k.Pub.ToBase58CheckAddress(),
|
||||||
|
"hex_address": k.Pub.ToHexAddress(),
|
||||||
|
"private_key": k.Hex(),
|
||||||
|
"public_key": k.Pub.Hex(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
package tron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Golden vectors copied verbatim from the repository fixtures (t2, t3).
|
||||||
|
func TestMnemonicDerivationGoldenVectors(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
mnemonic string
|
||||||
|
priv string
|
||||||
|
pub string
|
||||||
|
addr string
|
||||||
|
hexAddr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "t3",
|
||||||
|
mnemonic: "cloth print knife public box polar impose kingdom phone favorite rose lounge",
|
||||||
|
priv: "a96135266ba58bb5b3a450b8fe7a9d59ea3e6cb0cdb2300e8138d2d2d48e61a8",
|
||||||
|
pub: "076e3db946fcb53d575c4e742a9f8ff8c4b57ded53776b2a80d13022e1647adaa1641f3e2210f5f6a3bdd73d073ac029ba79d576093d8a92dd148e448a8e63cb",
|
||||||
|
addr: "TG9vH8c2jBWq2wSnksrq5mVyq2wjhG4A1n",
|
||||||
|
hexAddr: "4143d75ee7cdcd7f2fcd4fc3b4eb574b6bdc2a3fcb",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "t2",
|
||||||
|
mnemonic: "bullet tissue frozen garment immune valley anxiety nurse vicious caution divert latin",
|
||||||
|
priv: "3252a4cb9b3865c94dbb7d6f50107ca704c1c2b9e3d1eb81753e221c7db8fa25",
|
||||||
|
pub: "ede91cec53b797aa5951b707703a2b20150b97f642abc214289c935ee97ce9959d52acc45ccc5b7907f9a9bdb7f2bc2071789c91ea477e4a2485c92c8e18fb17",
|
||||||
|
addr: "TUSQ1u3HKUsTpfsi5uRXScUzES9RN1w8kk",
|
||||||
|
hexAddr: "41ca96b08266d7b36857d8cf2712d449ea07bf0275",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
key, err := DeriveFromMnemonic(tc.mnemonic, "", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DeriveFromMnemonic: %v", err)
|
||||||
|
}
|
||||||
|
if key.Hex() != tc.priv {
|
||||||
|
t.Fatalf("private key = %s, want %s", key.Hex(), tc.priv)
|
||||||
|
}
|
||||||
|
if key.Pub.Hex() != tc.pub {
|
||||||
|
t.Fatalf("public key = %s, want %s", key.Pub.Hex(), tc.pub)
|
||||||
|
}
|
||||||
|
if got := key.Pub.ToBase58CheckAddress(); got != tc.addr {
|
||||||
|
t.Fatalf("address = %s, want %s", got, tc.addr)
|
||||||
|
}
|
||||||
|
if got := key.Pub.ToHexAddress(); got != tc.hexAddr {
|
||||||
|
t.Fatalf("hex address = %s, want %s", got, tc.hexAddr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAddressFromPrivateKey checks the pk3 fixture (t3 private key).
|
||||||
|
func TestAddressFromPrivateKey(t *testing.T) {
|
||||||
|
pk := "a96135266ba58bb5b3a450b8fe7a9d59ea3e6cb0cdb2300e8138d2d2d48e61a8"
|
||||||
|
key, err := PrivateKeyFromHex(pk)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("PrivateKeyFromHex: %v", err)
|
||||||
|
}
|
||||||
|
if got := key.Pub.ToBase58CheckAddress(); got != "TG9vH8c2jBWq2wSnksrq5mVyq2wjhG4A1n" {
|
||||||
|
t.Fatalf("address = %s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddressConversions(t *testing.T) {
|
||||||
|
b58 := "TG9vH8c2jBWq2wSnksrq5mVyq2wjhG4A1n"
|
||||||
|
hexAddr := "4143d75ee7cdcd7f2fcd4fc3b4eb574b6bdc2a3fcb"
|
||||||
|
|
||||||
|
got, err := ToBase58CheckAddress(hexAddr)
|
||||||
|
if err != nil || got != b58 {
|
||||||
|
t.Fatalf("ToBase58CheckAddress(%s) = %s, %v", hexAddr, got, err)
|
||||||
|
}
|
||||||
|
gotHex, err := ToHexAddress(b58)
|
||||||
|
if err != nil || gotHex != hexAddr {
|
||||||
|
t.Fatalf("ToHexAddress(%s) = %s, %v", b58, gotHex, err)
|
||||||
|
}
|
||||||
|
raw, err := ToRawAddress(b58)
|
||||||
|
if err != nil || len(raw) != 21 || raw[0] != 0x41 {
|
||||||
|
t.Fatalf("ToRawAddress(%s) = %x, %v", b58, raw, err)
|
||||||
|
}
|
||||||
|
if !IsAddress(b58) || !IsAddress(hexAddr) {
|
||||||
|
t.Fatalf("IsAddress should accept both formats")
|
||||||
|
}
|
||||||
|
if IsAddress("TNotARealAddress000000000000000000") {
|
||||||
|
t.Fatalf("IsAddress accepted an invalid address")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSignRecoverRoundTrip(t *testing.T) {
|
||||||
|
pk, _ := hex.DecodeString("a96135266ba58bb5b3a450b8fe7a9d59ea3e6cb0cdb2300e8138d2d2d48e61a8")
|
||||||
|
key, err := NewPrivateKey(pk)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewPrivateKey: %v", err)
|
||||||
|
}
|
||||||
|
hash := make([]byte, 32)
|
||||||
|
for i := range hash {
|
||||||
|
hash[i] = byte(i)
|
||||||
|
}
|
||||||
|
sigBytes, err := key.Sign(hash)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Sign: %v", err)
|
||||||
|
}
|
||||||
|
if len(sigBytes) != 65 {
|
||||||
|
t.Fatalf("signature length = %d, want 65", len(sigBytes))
|
||||||
|
}
|
||||||
|
rec, err := RecoverAddress(hash, sigBytes)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("RecoverAddress: %v", err)
|
||||||
|
}
|
||||||
|
if rec != key.Pub.ToBase58CheckAddress() {
|
||||||
|
t.Fatalf("recovered %s, want %s", rec, key.Pub.ToBase58CheckAddress())
|
||||||
|
}
|
||||||
|
}
|
||||||
+244
@@ -0,0 +1,244 @@
|
|||||||
|
package tron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This file implements the minimal protobuf encoding required to compute TRON
|
||||||
|
// transaction ids (sha256 of the serialized Transaction.raw message) and to
|
||||||
|
// mirror tronpy's JSON transaction representation. Only the fields the wallet
|
||||||
|
// populates are handled; proto3 default (zero) values are omitted.
|
||||||
|
|
||||||
|
// --- protobuf wire primitives ---
|
||||||
|
|
||||||
|
const (
|
||||||
|
wireVarint = 0
|
||||||
|
wireBytes = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
type pbWriter struct {
|
||||||
|
buf []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *pbWriter) rawVarint(v uint64) {
|
||||||
|
for v >= 0x80 {
|
||||||
|
w.buf = append(w.buf, byte(v)|0x80)
|
||||||
|
v >>= 7
|
||||||
|
}
|
||||||
|
w.buf = append(w.buf, byte(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *pbWriter) tag(field, wire int) {
|
||||||
|
w.rawVarint(uint64(field)<<3 | uint64(wire))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *pbWriter) varintField(field int, v uint64) {
|
||||||
|
if v == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.tag(field, wireVarint)
|
||||||
|
w.rawVarint(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *pbWriter) int64Field(field int, v int64) {
|
||||||
|
w.varintField(field, uint64(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *pbWriter) bytesField(field int, b []byte) {
|
||||||
|
if len(b) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.tag(field, wireBytes)
|
||||||
|
w.rawVarint(uint64(len(b)))
|
||||||
|
w.buf = append(w.buf, b...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *pbWriter) stringField(field int, s string) {
|
||||||
|
w.bytesField(field, []byte(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *pbWriter) messageField(field int, fn func(*pbWriter)) {
|
||||||
|
sub := &pbWriter{}
|
||||||
|
fn(sub)
|
||||||
|
w.bytesField(field, sub.buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- contract value types (JSON shape matches tronpy dicts) ---
|
||||||
|
|
||||||
|
// TransferValue is the value payload of a TransferContract.
|
||||||
|
type TransferValue struct {
|
||||||
|
OwnerAddress string `json:"owner_address"`
|
||||||
|
ToAddress string `json:"to_address"`
|
||||||
|
Amount int64 `json:"amount"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TriggerValue is the value payload of a TriggerSmartContract.
|
||||||
|
type TriggerValue struct {
|
||||||
|
OwnerAddress string `json:"owner_address"`
|
||||||
|
ContractAddress string `json:"contract_address"`
|
||||||
|
Data string `json:"data"`
|
||||||
|
CallTokenValue int64 `json:"call_token_value"`
|
||||||
|
CallValue int64 `json:"call_value"`
|
||||||
|
TokenID int64 `json:"token_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FreezeValue is the value payload of a FreezeBalanceV2Contract.
|
||||||
|
type FreezeValue struct {
|
||||||
|
OwnerAddress string `json:"owner_address"`
|
||||||
|
FrozenBalance int64 `json:"frozen_balance"`
|
||||||
|
Resource string `json:"resource"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnfreezeValue is the value payload of an UnfreezeBalanceV2Contract.
|
||||||
|
type UnfreezeValue struct {
|
||||||
|
OwnerAddress string `json:"owner_address"`
|
||||||
|
UnfreezeBalance int64 `json:"unfreeze_balance"`
|
||||||
|
Resource string `json:"resource"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParameterJSON mirrors tronpy's {value, type_url} contract parameter.
|
||||||
|
type ParameterJSON struct {
|
||||||
|
Value any `json:"value"`
|
||||||
|
TypeURL string `json:"type_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContractJSON mirrors tronpy's contract entry inside raw_data.
|
||||||
|
type ContractJSON struct {
|
||||||
|
Parameter ParameterJSON `json:"parameter"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RawData mirrors tronpy's raw_data dict; addresses are hex strings.
|
||||||
|
type RawData struct {
|
||||||
|
Contract []ContractJSON `json:"contract"`
|
||||||
|
Timestamp int64 `json:"timestamp"`
|
||||||
|
Expiration int64 `json:"expiration"`
|
||||||
|
RefBlockBytes string `json:"ref_block_bytes"`
|
||||||
|
RefBlockHash string `json:"ref_block_hash"`
|
||||||
|
FeeLimit int64 `json:"fee_limit,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- encoding ---
|
||||||
|
|
||||||
|
func contractTypeCode(t string) uint64 {
|
||||||
|
switch t {
|
||||||
|
case "TransferContract":
|
||||||
|
return 1
|
||||||
|
case "TriggerSmartContract":
|
||||||
|
return 31
|
||||||
|
case "FreezeBalanceV2Contract":
|
||||||
|
return 54
|
||||||
|
case "UnfreezeBalanceV2Contract":
|
||||||
|
return 55
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceCode(r string) uint64 {
|
||||||
|
if r == "ENERGY" {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ContractJSON) serializeValue() ([]byte, error) {
|
||||||
|
w := &pbWriter{}
|
||||||
|
switch v := c.Parameter.Value.(type) {
|
||||||
|
case TransferValue:
|
||||||
|
owner, err := ToRawAddress(v.OwnerAddress)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
to, err := ToRawAddress(v.ToAddress)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
w.bytesField(1, owner)
|
||||||
|
w.bytesField(2, to)
|
||||||
|
w.int64Field(3, v.Amount)
|
||||||
|
case TriggerValue:
|
||||||
|
owner, err := ToRawAddress(v.OwnerAddress)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
contractAddr, err := ToRawAddress(v.ContractAddress)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
data, err := hex.DecodeString(v.Data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
w.bytesField(1, owner)
|
||||||
|
w.bytesField(2, contractAddr)
|
||||||
|
w.int64Field(3, v.CallValue)
|
||||||
|
w.bytesField(4, data)
|
||||||
|
w.int64Field(5, v.CallTokenValue)
|
||||||
|
w.int64Field(6, v.TokenID)
|
||||||
|
case FreezeValue:
|
||||||
|
owner, err := ToRawAddress(v.OwnerAddress)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
w.bytesField(1, owner)
|
||||||
|
w.int64Field(2, v.FrozenBalance)
|
||||||
|
w.varintField(3, resourceCode(v.Resource))
|
||||||
|
case UnfreezeValue:
|
||||||
|
owner, err := ToRawAddress(v.OwnerAddress)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
w.bytesField(1, owner)
|
||||||
|
w.int64Field(2, v.UnfreezeBalance)
|
||||||
|
w.varintField(3, resourceCode(v.Resource))
|
||||||
|
default:
|
||||||
|
return nil, ErrValidation
|
||||||
|
}
|
||||||
|
return w.buf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// serialize encodes the raw data into the protobuf Transaction.raw message.
|
||||||
|
func (rd *RawData) serialize() ([]byte, error) {
|
||||||
|
w := &pbWriter{}
|
||||||
|
rbb, err := hex.DecodeString(rd.RefBlockBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
w.bytesField(1, rbb)
|
||||||
|
rbh, err := hex.DecodeString(rd.RefBlockHash)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
w.bytesField(4, rbh)
|
||||||
|
w.int64Field(8, rd.Expiration)
|
||||||
|
for i := range rd.Contract {
|
||||||
|
c := &rd.Contract[i]
|
||||||
|
val, err := c.serializeValue()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
typeURL := c.Parameter.TypeURL
|
||||||
|
code := contractTypeCode(c.Type)
|
||||||
|
w.messageField(11, func(sub *pbWriter) {
|
||||||
|
sub.varintField(1, code)
|
||||||
|
sub.messageField(2, func(anyW *pbWriter) {
|
||||||
|
anyW.stringField(1, typeURL)
|
||||||
|
anyW.bytesField(2, val)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
w.int64Field(14, rd.Timestamp)
|
||||||
|
w.int64Field(18, rd.FeeLimit)
|
||||||
|
return w.buf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TxID computes the transaction id (hex) from the raw data.
|
||||||
|
func (rd *RawData) TxID() (string, error) {
|
||||||
|
b, err := rd.serialize()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
h := sha256.Sum256(b)
|
||||||
|
return hex.EncodeToString(h[:]), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package tron
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// TestTxIDMatchesPython locks the hand-rolled protobuf encoder to the reference
|
||||||
|
// values produced by tronpy's proto.calculate_txid_from_raw_data. These vectors
|
||||||
|
// were computed with the vendored tronpy 0.6.2 + protobuf runtime.
|
||||||
|
func TestTxIDMatchesPython(t *testing.T) {
|
||||||
|
const baseTS = int64(1600000000000)
|
||||||
|
|
||||||
|
transfer := &RawData{
|
||||||
|
Contract: []ContractJSON{{
|
||||||
|
Parameter: ParameterJSON{
|
||||||
|
Value: TransferValue{
|
||||||
|
OwnerAddress: "41ca96b08266d7b36857d8cf2712d449ea07bf0275",
|
||||||
|
ToAddress: "4143d75ee7cdcd7f2fcd4fc3b4eb574b6bdc2a3fcb",
|
||||||
|
Amount: 1000000,
|
||||||
|
},
|
||||||
|
TypeURL: "type.googleapis.com/protocol.TransferContract",
|
||||||
|
},
|
||||||
|
Type: "TransferContract",
|
||||||
|
}},
|
||||||
|
Timestamp: baseTS,
|
||||||
|
Expiration: baseTS + 60000,
|
||||||
|
RefBlockBytes: "1234",
|
||||||
|
RefBlockHash: "1234567890abcdef",
|
||||||
|
}
|
||||||
|
|
||||||
|
trigger := &RawData{
|
||||||
|
Contract: []ContractJSON{{
|
||||||
|
Parameter: ParameterJSON{
|
||||||
|
Value: TriggerValue{
|
||||||
|
OwnerAddress: "4143d75ee7cdcd7f2fcd4fc3b4eb574b6bdc2a3fcb",
|
||||||
|
ContractAddress: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
|
||||||
|
Data: "a9059cbb000000000000000000000000ca96b08266d7b36857d8cf2712d449ea07bf020000000000000000000000000000000000000000000000000000000000000001",
|
||||||
|
},
|
||||||
|
TypeURL: "type.googleapis.com/protocol.TriggerSmartContract",
|
||||||
|
},
|
||||||
|
Type: "TriggerSmartContract",
|
||||||
|
}},
|
||||||
|
Timestamp: baseTS,
|
||||||
|
Expiration: baseTS + 60000,
|
||||||
|
RefBlockBytes: "1234",
|
||||||
|
RefBlockHash: "1234567890abcdef",
|
||||||
|
FeeLimit: 100000000,
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
rd *RawData
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"transfer", transfer, "e93945efde601eed484553338e58218f4e81c51261dfda72db2d874f75378fe8"},
|
||||||
|
{"trigger", trigger, "0c1dec1fa73590ddf4a3975b78791e72d18998ae42751af199384587c2ef12d0"},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
got, err := tc.rd.TxID()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("TxID: %v", err)
|
||||||
|
}
|
||||||
|
if got != tc.want {
|
||||||
|
t.Fatalf("txid = %s, want %s", got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
package tron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// defaultAPIKeys are the public TronGrid API keys used by tronpy when the
|
||||||
|
// endpoint is a TronGrid host and no explicit key is supplied.
|
||||||
|
var defaultAPIKeys = []string{
|
||||||
|
"f92221d5-7056-4366-b96f-65d3662ec2d9",
|
||||||
|
"1e0a625f-cfa5-43ee-ba41-a09db1aae55f",
|
||||||
|
"f399168e-2259-481c-90fc-6b3d984c5463",
|
||||||
|
"da63253b-aa9c-46e7-a4e8-22d259a8026d",
|
||||||
|
"88c10958-af7b-4d5a-8eef-6e84bf5fb809",
|
||||||
|
"169bb4b3-cbe8-449a-984e-80e9adacac55",
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPProvider is a minimal JSON-over-HTTP client for a TRON fullnode. It
|
||||||
|
// mirrors tronpy.providers.HTTPProvider: POST {<method>} with a JSON body and
|
||||||
|
// an optional Tron-Pro-Api-Key header for TronGrid endpoints.
|
||||||
|
type HTTPProvider struct {
|
||||||
|
endpointURI string
|
||||||
|
timeout time.Duration
|
||||||
|
useAPIKey bool
|
||||||
|
apiKeys []string
|
||||||
|
client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHTTPProvider creates a provider for the given endpoint URI. When the
|
||||||
|
// endpoint is a TronGrid host an api key is used (the supplied one, or the
|
||||||
|
// public defaults). An empty endpoint falls back to the TRONPY_HTTP_PROVIDER_URI
|
||||||
|
// environment variable and then to api.trongrid.io.
|
||||||
|
func NewHTTPProvider(endpointURI string, timeout time.Duration, apiKey string) *HTTPProvider {
|
||||||
|
if endpointURI == "" {
|
||||||
|
endpointURI = os.Getenv("TRONPY_HTTP_PROVIDER_URI")
|
||||||
|
}
|
||||||
|
if endpointURI == "" {
|
||||||
|
endpointURI = "https://api.trongrid.io/"
|
||||||
|
}
|
||||||
|
p := &HTTPProvider{
|
||||||
|
endpointURI: strings.TrimRight(endpointURI, "/"),
|
||||||
|
timeout: timeout,
|
||||||
|
client: &http.Client{Timeout: timeout},
|
||||||
|
}
|
||||||
|
if strings.Contains(p.endpointURI, "trongrid") {
|
||||||
|
p.useAPIKey = true
|
||||||
|
if apiKey != "" {
|
||||||
|
p.apiKeys = []string{apiKey}
|
||||||
|
} else {
|
||||||
|
p.apiKeys = append([]string{}, defaultAPIKeys...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// Endpoint returns the configured endpoint URI.
|
||||||
|
func (p *HTTPProvider) Endpoint() string { return p.endpointURI }
|
||||||
|
|
||||||
|
// makeRequest issues a POST to {endpoint}/{method} with the JSON params.
|
||||||
|
func (p *HTTPProvider) makeRequest(method string, params any) (map[string]any, error) {
|
||||||
|
if params == nil {
|
||||||
|
params = map[string]any{}
|
||||||
|
}
|
||||||
|
body, err := json.Marshal(params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for attempt := 0; ; attempt++ {
|
||||||
|
req, err := http.NewRequest(http.MethodPost, p.endpointURI+"/"+method, bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("User-Agent", "kisstron")
|
||||||
|
if p.useAPIKey && len(p.apiKeys) > 0 {
|
||||||
|
req.Header.Set("Tron-Pro-Api-Key", p.apiKeys[rand.Intn(len(p.apiKeys))])
|
||||||
|
}
|
||||||
|
resp, err := p.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
data, readErr := io.ReadAll(resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
if readErr != nil {
|
||||||
|
return nil, readErr
|
||||||
|
}
|
||||||
|
if resp.StatusCode == http.StatusForbidden && strings.Contains(string(data), "Exceed the user daily usage") {
|
||||||
|
if !p.rotateAPIKey() || attempt >= len(defaultAPIKeys) {
|
||||||
|
return nil, errors.New("rate limit! please add more API keys")
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
return nil, errors.New("http error: " + resp.Status)
|
||||||
|
}
|
||||||
|
dec := json.NewDecoder(bytes.NewReader(data))
|
||||||
|
dec.UseNumber()
|
||||||
|
var out map[string]any
|
||||||
|
if err := dec.Decode(&out); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HTTPProvider) rotateAPIKey() bool {
|
||||||
|
if len(p.apiKeys) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
p.apiKeys = p.apiKeys[1:]
|
||||||
|
return len(p.apiKeys) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleAPIError maps a node error payload to the same typed errors as
|
||||||
|
// tronpy. It returns nil when the payload indicates success.
|
||||||
|
func handleAPIError(payload map[string]any) error {
|
||||||
|
if res, ok := payload["result"].(bool); ok && res {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if e, ok := payload["Error"].(string); ok {
|
||||||
|
return errors.New(e)
|
||||||
|
}
|
||||||
|
if code, ok := payload["code"].(string); ok {
|
||||||
|
msg := code
|
||||||
|
if m, ok := payload["message"].(string); ok {
|
||||||
|
if decoded, err := hex.DecodeString(m); err == nil {
|
||||||
|
msg = string(decoded)
|
||||||
|
} else {
|
||||||
|
msg = m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch code {
|
||||||
|
case "SIGERROR":
|
||||||
|
return wrapErr(ErrBadSignature, msg)
|
||||||
|
case "TAPOS_ERROR":
|
||||||
|
return wrapErr(ErrTapos, msg)
|
||||||
|
case "TRANSACTION_EXPIRATION_ERROR", "TOO_BIG_TRANSACTION_ERROR":
|
||||||
|
return wrapErr(ErrTransaction, msg)
|
||||||
|
case "CONTRACT_VALIDATE_ERROR":
|
||||||
|
return wrapErr(ErrValidation, msg)
|
||||||
|
default:
|
||||||
|
return wrapErr(ErrAPI, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if res, ok := payload["result"].(map[string]any); ok {
|
||||||
|
return handleAPIError(res)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package tron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestSignatureMatchesPython locks the hand-rolled RFC6979/low-s recoverable
|
||||||
|
// signing to values produced by coincurve/libsecp256k1 via tronpy's
|
||||||
|
// PrivateKey.sign_msg_hash. The hashes are the txids computed elsewhere in the
|
||||||
|
// suite.
|
||||||
|
func TestSignatureMatchesPython(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
priv string
|
||||||
|
hash string
|
||||||
|
want string
|
||||||
|
addr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"t3_transfer_txid",
|
||||||
|
"a96135266ba58bb5b3a450b8fe7a9d59ea3e6cb0cdb2300e8138d2d2d48e61a8",
|
||||||
|
"e93945efde601eed484553338e58218f4e81c51261dfda72db2d874f75378fe8",
|
||||||
|
"a48ffafc8bdb57bdb64216b0facdff77cd2bb7090e1e1b7dcd6e829823c7cd207422b7bd910a063b96f562e32dc566b3b6a1c935d7c63c01642dda14b0b4966d00",
|
||||||
|
"TG9vH8c2jBWq2wSnksrq5mVyq2wjhG4A1n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t3_sparse_hash",
|
||||||
|
"a96135266ba58bb5b3a450b8fe7a9d59ea3e6cb0cdb2300e8138d2d2d48e61a8",
|
||||||
|
"0000000000000000000000000000000000000000000000000000000000000001",
|
||||||
|
"2df95cd58fc299c155dd51e13799c42c00786b1d18a1423f9b098f8354f4785e670d6a75c0fea96162a990689a9ee0089e08bd9e663dfe27104902a0bdf6a04f01",
|
||||||
|
"TG9vH8c2jBWq2wSnksrq5mVyq2wjhG4A1n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t2_trigger_txid",
|
||||||
|
"3252a4cb9b3865c94dbb7d6f50107ca704c1c2b9e3d1eb81753e221c7db8fa25",
|
||||||
|
"0c1dec1fa73590ddf4a3975b78791e72d18998ae42751af199384587c2ef12d0",
|
||||||
|
"3136e1e010697c23c3aa7ba78b3eee06a3fbb75d32e37c0730f2b2a733ed5da6191f93d706b0c6c18d480d0f27cfd208bf64ce157ee83f6ff6082fdd230d0cab01",
|
||||||
|
"TUSQ1u3HKUsTpfsi5uRXScUzES9RN1w8kk",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
key, err := PrivateKeyFromHex(tc.priv)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("PrivateKeyFromHex: %v", err)
|
||||||
|
}
|
||||||
|
h, err := hex.DecodeString(tc.hash)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decode hash: %v", err)
|
||||||
|
}
|
||||||
|
sig, err := key.Sign(h)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Sign: %v", err)
|
||||||
|
}
|
||||||
|
if got := hex.EncodeToString(sig); got != tc.want {
|
||||||
|
t.Fatalf("signature = %s, want %s", got, tc.want)
|
||||||
|
}
|
||||||
|
rec, err := RecoverAddress(h, sig)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("RecoverAddress: %v", err)
|
||||||
|
}
|
||||||
|
if rec != tc.addr {
|
||||||
|
t.Fatalf("recovered = %s, want %s", rec, tc.addr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package tron
|
||||||
|
|
||||||
|
// Token is a hardcoded TRC20 token definition.
|
||||||
|
type Token struct {
|
||||||
|
Key string
|
||||||
|
Contract string
|
||||||
|
Ticker string
|
||||||
|
ShowBalance bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hardcoded network and scale constants matching the original kisstron.py.
|
||||||
|
const (
|
||||||
|
// DefaultNodeAddr is the public TRON node used for mainnet without an API key.
|
||||||
|
DefaultNodeAddr = "http://3.225.171.164:8090"
|
||||||
|
// FeeLimitMainnet is the default mainnet fee limit in SUN (100 TRX).
|
||||||
|
FeeLimitMainnet = 100000000
|
||||||
|
// FeeLimitTestnet is the default testnet fee limit in SUN (1000 TRX).
|
||||||
|
FeeLimitTestnet = 1000000000
|
||||||
|
// TrxScale is the number of SUN in one TRX.
|
||||||
|
TrxScale = 1000000.0
|
||||||
|
)
|
||||||
|
|
||||||
|
// tokensByNetwork preserves the insertion order of the original dict so wallet
|
||||||
|
// info output matches byte-for-byte.
|
||||||
|
var tokensByNetwork = map[string][]Token{
|
||||||
|
"mainnet": {
|
||||||
|
{Key: "usdt", Contract: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", Ticker: "USDT", ShowBalance: true},
|
||||||
|
{Key: "usdc", Contract: "TEkxiTehnzSmSe2XqrBj4w32RUN966rdz8", Ticker: "USDC", ShowBalance: true},
|
||||||
|
{Key: "tusd", Contract: "TUpMhErZL2fhh4sVNULAbNKLokS4GjC1F4", Ticker: "TUSD", ShowBalance: true},
|
||||||
|
},
|
||||||
|
"nile": {
|
||||||
|
{Key: "usdt", Contract: "TXLAQ63Xg1NAzckPwKHvzw7CSEmLMEqcdj", Ticker: "USDT", ShowBalance: true},
|
||||||
|
{Key: "usdc", Contract: "TEMVynQpntMqkPxP6wXTW2K7e4sM3cRmWz", Ticker: "USDC", ShowBalance: true},
|
||||||
|
},
|
||||||
|
"shasta": {
|
||||||
|
{Key: "usdt", Contract: "TG3XXyExBkPp9nzdajDZsozEu4BkaSJozs", Ticker: "USDT", ShowBalance: true},
|
||||||
|
{Key: "usdc", Contract: "TSdZwNqpHofzP6BsBKGQUWdBeJphLmF6id", Ticker: "USDC", ShowBalance: true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultEndpoints = map[string]string{
|
||||||
|
"mainnet": "",
|
||||||
|
"nile": "https://api.nileex.io",
|
||||||
|
"shasta": "https://api.shasta.trongrid.io",
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokensForNetwork returns the tokens for a network in a stable order.
|
||||||
|
func TokensForNetwork(network string) []Token {
|
||||||
|
return tokensByNetwork[network]
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenByKey looks up a token by its ticker key for a network.
|
||||||
|
func TokenByKey(network, key string) (Token, bool) {
|
||||||
|
for _, t := range tokensByNetwork[network] {
|
||||||
|
if t.Key == key {
|
||||||
|
return t, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Token{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultEndpoint returns the default node endpoint for a network.
|
||||||
|
func DefaultEndpoint(network string) string {
|
||||||
|
return defaultEndpoints[network]
|
||||||
|
}
|
||||||
+206
@@ -0,0 +1,206 @@
|
|||||||
|
package tron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Transaction is a signed or unsigned TRON transaction. Its field order matches
|
||||||
|
// tronpy's Transaction.to_json() dict order so the marshalled JSON is
|
||||||
|
// byte-compatible with the Python offline output.
|
||||||
|
type Transaction struct {
|
||||||
|
TxID string `json:"txID"`
|
||||||
|
RawData *RawData `json:"raw_data"`
|
||||||
|
Signature []string `json:"signature"`
|
||||||
|
Permission any `json:"permission"`
|
||||||
|
client *Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func currentMillis() int64 {
|
||||||
|
return time.Now().UnixNano() / int64(time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransactionBuilder assembles a transaction before signing.
|
||||||
|
type TransactionBuilder struct {
|
||||||
|
client *Client
|
||||||
|
rawData *RawData
|
||||||
|
}
|
||||||
|
|
||||||
|
// newTransactionBuilder creates a builder for a single contract.
|
||||||
|
func (c *Client) newTransactionBuilder(typ string, value any) *TransactionBuilder {
|
||||||
|
now := currentMillis()
|
||||||
|
rd := &RawData{
|
||||||
|
Contract: []ContractJSON{{
|
||||||
|
Parameter: ParameterJSON{
|
||||||
|
Value: value,
|
||||||
|
TypeURL: "type.googleapis.com/protocol." + typ,
|
||||||
|
},
|
||||||
|
Type: typ,
|
||||||
|
}},
|
||||||
|
Timestamp: now,
|
||||||
|
Expiration: now + 60000,
|
||||||
|
}
|
||||||
|
if typ == "TriggerSmartContract" || typ == "CreateSmartContract" {
|
||||||
|
rd.FeeLimit = c.feeLimit
|
||||||
|
}
|
||||||
|
return &TransactionBuilder{client: c, rawData: rd}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transfer builds a TRX transfer builder. Amount is in SUN.
|
||||||
|
func (c *Client) Transfer(from, to string, amount int64) (*TransactionBuilder, error) {
|
||||||
|
fromHex, err := ToHexAddress(from)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
toHex, err := ToHexAddress(to)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return c.newTransactionBuilder("TransferContract", TransferValue{
|
||||||
|
OwnerAddress: fromHex,
|
||||||
|
ToAddress: toHex,
|
||||||
|
Amount: amount,
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FreezeBalance builds a FreezeBalanceV2 builder for the given resource.
|
||||||
|
func (c *Client) FreezeBalance(owner string, amount int64, resource string) (*TransactionBuilder, error) {
|
||||||
|
ownerHex, err := ToHexAddress(owner)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return c.newTransactionBuilder("FreezeBalanceV2Contract", FreezeValue{
|
||||||
|
OwnerAddress: ownerHex,
|
||||||
|
FrozenBalance: amount,
|
||||||
|
Resource: resource,
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnfreezeBalance builds an UnfreezeBalanceV2 builder for the given resource.
|
||||||
|
func (c *Client) UnfreezeBalance(owner string, amount int64, resource string) (*TransactionBuilder, error) {
|
||||||
|
ownerHex, err := ToHexAddress(owner)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return c.newTransactionBuilder("UnfreezeBalanceV2Contract", UnfreezeValue{
|
||||||
|
OwnerAddress: ownerHex,
|
||||||
|
UnfreezeBalance: amount,
|
||||||
|
Resource: resource,
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build fills the reference block fields from the chain and computes the txid.
|
||||||
|
func (b *TransactionBuilder) Build() (*Transaction, error) {
|
||||||
|
refID, err := b.client.getLatestSolidBlockID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(refID) < 32 {
|
||||||
|
return nil, ErrBadHash
|
||||||
|
}
|
||||||
|
b.rawData.RefBlockBytes = refID[12:16]
|
||||||
|
b.rawData.RefBlockHash = refID[16:32]
|
||||||
|
txid, err := b.rawData.TxID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &Transaction{
|
||||||
|
TxID: txid,
|
||||||
|
RawData: b.rawData,
|
||||||
|
Signature: []string{},
|
||||||
|
client: b.client,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign signs the transaction with the given private key.
|
||||||
|
func (tx *Transaction) Sign(key *PrivateKey) error {
|
||||||
|
if tx.TxID == "" {
|
||||||
|
return ErrTransaction
|
||||||
|
}
|
||||||
|
hash, err := hex.DecodeString(tx.TxID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sig, err := key.Sign(hash)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tx.Signature = append(tx.Signature, hex.EncodeToString(sig))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Broadcast sends the signed transaction and returns the raw response.
|
||||||
|
func (tx *Transaction) Broadcast() (map[string]any, error) {
|
||||||
|
if tx.client == nil {
|
||||||
|
return nil, ErrTransaction
|
||||||
|
}
|
||||||
|
return tx.client.broadcast(tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait polls for the transaction receipt, matching TransactionRet.wait().
|
||||||
|
func (tx *Transaction) Wait(timeout, interval time.Duration) (map[string]any, error) {
|
||||||
|
if tx.client == nil {
|
||||||
|
return nil, ErrTransaction
|
||||||
|
}
|
||||||
|
deadline := time.Now().Add(timeout)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
info, err := tx.client.getTransactionInfo(tx.TxID)
|
||||||
|
if err == nil {
|
||||||
|
return info, nil
|
||||||
|
}
|
||||||
|
if !errors.Is(err, ErrTransactionNotFound) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
time.Sleep(interval)
|
||||||
|
}
|
||||||
|
return nil, ErrTransactionNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON returns the compact transaction JSON used for node requests and offline
|
||||||
|
// mode output, matching json.dumps(txn.to_json(), separators=(",", ":")).
|
||||||
|
func (tx *Transaction) JSON() string {
|
||||||
|
b, _ := json.Marshal(tx)
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// toJSON returns the transaction as a generic map for node requests.
|
||||||
|
func (tx *Transaction) toJSON() map[string]any {
|
||||||
|
var m map[string]any
|
||||||
|
json.Unmarshal([]byte(tx.JSON()), &m)
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransactionFromJSON parses a transaction previously exported by JSON().
|
||||||
|
func TransactionFromJSON(s string) (*Transaction, error) {
|
||||||
|
var decoded struct {
|
||||||
|
TxID string `json:"txID"`
|
||||||
|
RawData *RawData `json:"raw_data"`
|
||||||
|
Signature []string `json:"signature"`
|
||||||
|
Permission any `json:"permission"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(s), &decoded); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if decoded.RawData == nil {
|
||||||
|
return nil, ErrTransaction
|
||||||
|
}
|
||||||
|
return &Transaction{
|
||||||
|
TxID: decoded.TxID,
|
||||||
|
RawData: decoded.RawData,
|
||||||
|
Signature: decoded.Signature,
|
||||||
|
Permission: decoded.Permission,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseTransaction parses exported transaction JSON and attaches the client so
|
||||||
|
// the transaction can be broadcast (mirrors Transaction.from_json + _client).
|
||||||
|
func (c *Client) ParseTransaction(raw string) (*Transaction, error) {
|
||||||
|
tx, err := TransactionFromJSON(raw)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tx.client = c
|
||||||
|
return tx, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package tron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ValueString coerces a decoded JSON value to a string. It handles the
|
||||||
|
// json.Number type produced when decoding with UseNumber so large integers are
|
||||||
|
// preserved exactly.
|
||||||
|
func ValueString(v any) string {
|
||||||
|
switch x := v.(type) {
|
||||||
|
case nil:
|
||||||
|
return ""
|
||||||
|
case string:
|
||||||
|
return x
|
||||||
|
case json.Number:
|
||||||
|
return x.String()
|
||||||
|
case float64:
|
||||||
|
return strconv.FormatFloat(x, 'f', -1, 64)
|
||||||
|
case bool:
|
||||||
|
if x {
|
||||||
|
return "true"
|
||||||
|
}
|
||||||
|
return "false"
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%v", x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValueInt64 coerces a decoded JSON value to an int64. Missing or non-numeric
|
||||||
|
// values yield 0, matching Python's treatment of absent dict keys.
|
||||||
|
func ValueInt64(v any) int64 {
|
||||||
|
switch x := v.(type) {
|
||||||
|
case json.Number:
|
||||||
|
if i, err := x.Int64(); err == nil {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
f, _ := x.Float64()
|
||||||
|
return int64(f)
|
||||||
|
case float64:
|
||||||
|
return int64(x)
|
||||||
|
case int64:
|
||||||
|
return x
|
||||||
|
case int:
|
||||||
|
return int64(x)
|
||||||
|
case string:
|
||||||
|
i, _ := strconv.ParseInt(x, 10, 64)
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package tron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/big"
|
||||||
|
|
||||||
|
"code.luxferre.top/luxferre/kisstron/bip32"
|
||||||
|
"code.luxferre.top/luxferre/kisstron/bip39"
|
||||||
|
"code.luxferre.top/luxferre/kisstron/secp256k1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ValidWordCount reports whether a mnemonic word count is supported.
|
||||||
|
func ValidWordCount(n int) bool {
|
||||||
|
return bip39.ValidWordCount(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateMnemonic reports whether a mnemonic is well-formed and checksum-correct.
|
||||||
|
func ValidateMnemonic(mnemonic string) bool {
|
||||||
|
return bip39.Validate(mnemonic)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeriveFromMnemonic restores a private key from a BIP-39 mnemonic and an
|
||||||
|
// optional passphrase using the given BIP-32 derivation path. An empty path
|
||||||
|
// selects the default TRON path (m/44'/195'/0'/0/0).
|
||||||
|
func DeriveFromMnemonic(mnemonic, passphrase, path string) (*PrivateKey, error) {
|
||||||
|
if path == "" {
|
||||||
|
path = bip32.DefaultPath
|
||||||
|
}
|
||||||
|
seed := bip39.Seed(mnemonic, passphrase)
|
||||||
|
key, err := bip32.DerivePath(seed, path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return newPrivateKeyUnchecked(key), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateWithMnemonic creates a brand new wallet and its mnemonic. The
|
||||||
|
// returned key and mnemonic belong together and must both be stored.
|
||||||
|
func GenerateWithMnemonic(passphrase string, numWords int, path string) (*PrivateKey, string, error) {
|
||||||
|
mnemonic, err := bip39.NewMnemonic(numWords)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
key, err := DeriveFromMnemonic(mnemonic, passphrase, path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
return key, mnemonic, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecoverAddress recovers the signer's base58check address from a message hash
|
||||||
|
// and a 65-byte recoverable signature.
|
||||||
|
func RecoverAddress(hash, sig []byte) (string, error) {
|
||||||
|
if len(sig) != 65 {
|
||||||
|
return "", ErrBadSignature
|
||||||
|
}
|
||||||
|
r := new(big.Int).SetBytes(sig[0:32])
|
||||||
|
ss := new(big.Int).SetBytes(sig[32:64])
|
||||||
|
v := sig[64]
|
||||||
|
if v != 0 && v != 1 {
|
||||||
|
return "", ErrBadSignature
|
||||||
|
}
|
||||||
|
point, err := secp256k1.RecoverPublicKey(hash, &secp256k1.Signature{R: r, S: ss, V: v})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
raw := make([]byte, 64)
|
||||||
|
point.X.FillBytes(raw[0:32])
|
||||||
|
point.Y.FillBytes(raw[32:64])
|
||||||
|
return (&PublicKey{raw: raw}).ToBase58CheckAddress(), nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user