go port
This commit is contained in:
+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