initial upload
This commit is contained in:
@@ -0,0 +1,196 @@
|
|||||||
|
kisstron: KISS-friendly Tron CLI wallet
|
||||||
|
---------------------------------------
|
||||||
|
This is a simple, no-nonsense Tron cryptocurrency wallet written in Python 3
|
||||||
|
on top of the open-source tronpy library. The kisstron project aims to enable
|
||||||
|
CLI users to easily use Tron cryptocurrency, including stablecoins like USDT
|
||||||
|
or USDC, without all the hassle of "native" (Java-based) Tron's wallet-cli or
|
||||||
|
similar third-party projects.
|
||||||
|
|
||||||
|
== Features ==
|
||||||
|
|
||||||
|
* Support for mainnet Tron as well as Nile and Shasta testnets
|
||||||
|
* Small codebase size that can be easily audited
|
||||||
|
* Full BIP-39 support for mnemonic wallet generation and import (English only)
|
||||||
|
* Account info display: TRX/USDT/USDC balances, frozen assets, remaining
|
||||||
|
energy and bandwidth
|
||||||
|
* Sending TRX/USDT/USDC on all supported networks
|
||||||
|
* Freezing (for energy) and unfreezing TRX on all supported networks
|
||||||
|
* Ability to connect to custom nodes or TronGrid API (with your own API key)
|
||||||
|
* No filesystem interaction (that's a feature, not a bug: see the FAQ section)
|
||||||
|
* Full scriptability: send/freeze/unfreeze transactions require manual approval
|
||||||
|
but it actually can be turned off with a special commandline parameter for the
|
||||||
|
kisstron wallet to be used in various automation tasks
|
||||||
|
* The kisstron.py file can be used as a library itself for different projects
|
||||||
|
|
||||||
|
== Dependency setup ==
|
||||||
|
|
||||||
|
First, install Python 3, pip and the required dependencies (from this dir):
|
||||||
|
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
In some distributions, you may need to run pip3 instead of pip. Also, your
|
||||||
|
distribution might not contain the required development packages. For instance,
|
||||||
|
in Alpine Linux, you might have to run the following command first (as root):
|
||||||
|
|
||||||
|
apk add python3-dev libtool autoconf automake musl-dev libffi-dev
|
||||||
|
|
||||||
|
Optionally, make the kisstron.py file executable:
|
||||||
|
|
||||||
|
chmod +x kisstron.py
|
||||||
|
|
||||||
|
After everything is set up, you should be able to run the help screen:
|
||||||
|
|
||||||
|
./kisstron.py -h
|
||||||
|
|
||||||
|
== Usage ==
|
||||||
|
|
||||||
|
Now, let's cover typical kisstron wallet usage scenarios.
|
||||||
|
|
||||||
|
-- Generate a new wallet --
|
||||||
|
|
||||||
|
./kisstron.py new trx [-pp "passphrase"] [-wc word_count] [-dp derivation_path]
|
||||||
|
|
||||||
|
This will generate a new Tron wallet and display all its information. You must
|
||||||
|
then store this information in a secure place.
|
||||||
|
If -pp option is specified, your custom passphrase will be used to encrypt the
|
||||||
|
mnemonic phrase. If -wc option is specified, you can set to generate 15, 18, 21
|
||||||
|
or 24 words of mnemonic instead of 12. If -dp option is specified, you can set
|
||||||
|
a non-standard BIP-39 derivation path. It is generally NOT recommended to ever
|
||||||
|
use this option unless you are an expert.
|
||||||
|
|
||||||
|
-- Import an existing wallet from a mnemonic --
|
||||||
|
|
||||||
|
./kisstron.py import trx [-pp "passphrase"] [-dp derivation_path]
|
||||||
|
|
||||||
|
This will ask you to enter your BIP-39 mnemonic and import your Tron wallet by
|
||||||
|
recovering and displaying all information about it. You must then store this
|
||||||
|
information in a secure place.
|
||||||
|
If -pp option is specified, your custom passphrase will be used to decrypt the
|
||||||
|
mnemonic phrase. The -dp option is the same as for the "new trx" subcommand, so
|
||||||
|
the same precautions apply.
|
||||||
|
Note that entering a wrong passphrase won't generate an error: kisstron will
|
||||||
|
just restore a different wallet from the same mnemonic, so be careful.
|
||||||
|
|
||||||
|
-- Network options --
|
||||||
|
|
||||||
|
For any of the further operations in this section, the following options can
|
||||||
|
be added and are quite important:
|
||||||
|
|
||||||
|
* -net selects the network: mainnet (default), nile, shasta
|
||||||
|
* -node allows to specify custom Tron node URL (must be HTTP URL, not RPC)
|
||||||
|
* -tgkey allows to specify custom TronGrid API key (overrides -node)
|
||||||
|
|
||||||
|
These options will be marked as [netopts] in the further reference.
|
||||||
|
|
||||||
|
Note that -tgkey is only relevant for mainnet and has no effect on testnets.
|
||||||
|
|
||||||
|
-- Displaying account information --
|
||||||
|
|
||||||
|
Display account information based on any Tron address:
|
||||||
|
|
||||||
|
./kisstron.py info trx -addr tron_address [netopts]
|
||||||
|
|
||||||
|
Display your account information based on your private key (in hex):
|
||||||
|
|
||||||
|
./kisstron.py info trx -pk private_key [netopts]
|
||||||
|
|
||||||
|
Either of these commands will display the following information:
|
||||||
|
|
||||||
|
* Tron address (in Base58check format)
|
||||||
|
* Balances in TRX, USDT and USDC
|
||||||
|
* Frozen TRX assets (if any)
|
||||||
|
* Remaining bandwidth amount
|
||||||
|
* Remaining energy amount
|
||||||
|
|
||||||
|
If you supply both -pk and -addr to "info trx" subcommand, -pk will take
|
||||||
|
precedence.
|
||||||
|
|
||||||
|
-- Sending funds --
|
||||||
|
|
||||||
|
Send TRX:
|
||||||
|
|
||||||
|
./kisstron.py send trx -addr dest_addr -amt amount -pk private_key [netopts]
|
||||||
|
|
||||||
|
Send USDT:
|
||||||
|
|
||||||
|
./kisstron.py send usdt -addr dest_addr -amt amount -pk private_key [netopts]
|
||||||
|
|
||||||
|
Send USDC:
|
||||||
|
|
||||||
|
./kisstron.py send usdc -addr dest_addr -amt amount -pk private_key [netopts]
|
||||||
|
|
||||||
|
Any of these commands will ask you for confirmation by entering "yes" and
|
||||||
|
pressing Enter to send the transaction. If you don't want to do this or use
|
||||||
|
the send subcommands in some kind of automation, append -nc flag to them.
|
||||||
|
The amount is entered in floating point TRX/USDT/USDC values respectively,
|
||||||
|
not in minimal units.
|
||||||
|
When transfering USDT/USDC, you may get error messages related to lack of
|
||||||
|
energy to conduct the transaction. Please refer to the next section of TRX
|
||||||
|
freezing to get energy.
|
||||||
|
|
||||||
|
-- Freezing and unfreezing TRX funds --
|
||||||
|
|
||||||
|
Freeze TRX for energy:
|
||||||
|
|
||||||
|
./kisstron.py fre trx -amt amount -pk private_key [netopts]
|
||||||
|
|
||||||
|
Unfreeze TRX:
|
||||||
|
|
||||||
|
./kisstron.py unf trx -amt amount -pk private_key [netopts]
|
||||||
|
|
||||||
|
Any of these commands will ask you for confirmation by entering "yes" and
|
||||||
|
pressing Enter to send the transaction. If you don't want to do this or use
|
||||||
|
the subcommands in some kind of automation, append -nc flag to them.
|
||||||
|
Again, the amount of TRX is entered as a floating point value.
|
||||||
|
|
||||||
|
Keep in mind that it takes up to 72 hours to unfreeze your TRX assets.
|
||||||
|
|
||||||
|
== FAQ ==
|
||||||
|
|
||||||
|
- Can I trust it?
|
||||||
|
|
||||||
|
The author trusts it. In fact, kisstron was created because of lack of decent
|
||||||
|
FOSS Tron wallets for Linux/BSD desktop operating systems. However, it cannot
|
||||||
|
be considered production-ready as it hasn't undergone rigorous testing and
|
||||||
|
independent audit. The kisstron codebase is small and well-commented though,
|
||||||
|
so you can trust it exactly to the same extent as you trust the underlying
|
||||||
|
tronpy library and its dependencies.
|
||||||
|
You can study tronpy source code here: https://github.com/andelf/tronpy
|
||||||
|
|
||||||
|
- Why are TRC-20 tokens hardcoded?
|
||||||
|
|
||||||
|
To simplify the logic and avoid any user mistakes when manually pasting token
|
||||||
|
contract addresses. If you need to add custom token support, you can always
|
||||||
|
modify the KT_TRC20_TOKENS dictionary in the source code directly.
|
||||||
|
|
||||||
|
- Why doesn't kisstron do anything to keep private keys and passphrases secure?
|
||||||
|
|
||||||
|
Because it only cares about interacting with Tron network and creating or
|
||||||
|
restoring your wallets. For key/password management, there are dedicated tools
|
||||||
|
like pass (https://www.passwordstore.org/) that do their job extremely well.
|
||||||
|
For example, instead of -pk private_key, you could write -pk $(pass kt/pkey) or
|
||||||
|
something like that if you stored your wallet key under kt/pkey path in pass.
|
||||||
|
This way, your keys can stay secure without adding any filesystem interaction
|
||||||
|
to kisstron itself, reducing the overall attack surface.
|
||||||
|
|
||||||
|
- Does kisstron use any remote API to sign transactions?
|
||||||
|
|
||||||
|
No. The underlying tronpy library signs all transactions offline, so the
|
||||||
|
private key never leaves your machine. It uses libsecp256k1 via coincurve
|
||||||
|
library binding for transaction signing.
|
||||||
|
|
||||||
|
Ability to implement offline mode (transactions are created and signed on one
|
||||||
|
kisstron instance without Internet connectivity, then broadcast from another)
|
||||||
|
in the future versions is being studied.
|
||||||
|
|
||||||
|
- Why doesn't kisstron support TRC-10, multisig, witnesses, history etc?
|
||||||
|
|
||||||
|
Support for some of these things may come in the future. But kisstron was
|
||||||
|
created as a minimum viable wallet to operate on TRX and select stablecoins. It
|
||||||
|
adheres to the KISS principle ("keep it simple, stupid"), hence the name.
|
||||||
|
|
||||||
|
== Credits ==
|
||||||
|
|
||||||
|
Created by Luxferre in 2024, released into public domain with no warranties.
|
||||||
|
|
||||||
|
Made in Ukraine.
|
||||||
Executable
+459
@@ -0,0 +1,459 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# kisstron: a simple to use TRON CLI wallet based on tronpy
|
||||||
|
#
|
||||||
|
# Supports:
|
||||||
|
# * BIP39 mnemonic wallet generation/restoration
|
||||||
|
# * converting private keys to BIP39 mnemonics and vice versa
|
||||||
|
# * basic TRX parameters display (balance, energy, bandwidth)
|
||||||
|
# * basic TRX operations (transfer, freeze for energy, unfreeze)
|
||||||
|
# * basic TRC20 token operations (balance display, transfer) for select tokens
|
||||||
|
# (contract addresses are hardcoded, see KT_TRC20_TOKENS dictionary)
|
||||||
|
# * ability to select testnets (nile, shasta) if necessary
|
||||||
|
# * ability to connect to custom mainnet nodes if necessary
|
||||||
|
# * switching to TronGrid API by providing your own API key
|
||||||
|
#
|
||||||
|
# As of now, the main goal is to be able to operate on TRX and stablecoins.
|
||||||
|
# For mnemonic phrase generation, only English is currently supported.
|
||||||
|
#
|
||||||
|
# See README for more information, run with -h flag for usage instructions
|
||||||
|
#
|
||||||
|
# Created by Luxferre in 2024, released into public domain
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from tronpy import Tron
|
||||||
|
from tronpy.keys import PrivateKey
|
||||||
|
from tronpy.providers import HTTPProvider
|
||||||
|
from tronpy.exceptions import BadAddress, AddressNotFound, BadKey, ValidationError
|
||||||
|
import mnemonic, eth_utils
|
||||||
|
|
||||||
|
# some global parameters (hardcoded)
|
||||||
|
KT_TIMEOUT = 20.0 # network operation timeout
|
||||||
|
KT_FEE_LIMIT = 10000000 # transaction fee limit for mainnet (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)
|
||||||
|
TG_API_KEY = None # TronGrid API Key (unused by default)
|
||||||
|
NODE_ADDR = 'http://18.196.99.16:8090' # public Tron node (Germany)
|
||||||
|
|
||||||
|
# TRC20 token definitions (also hardcoded)
|
||||||
|
# Make sure the mainnet tokens match the contract addresses
|
||||||
|
# specified on these pages:
|
||||||
|
# USDT: https://tron.network/usdt
|
||||||
|
# USDC: https://tron.network/usdc
|
||||||
|
|
||||||
|
KT_TRC20_TOKENS = {
|
||||||
|
'mainnet': {
|
||||||
|
'usdt': {
|
||||||
|
'contract': 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t',
|
||||||
|
'ticker': 'USDT',
|
||||||
|
'show_balance': True
|
||||||
|
},
|
||||||
|
'usdc': {
|
||||||
|
'contract': 'TEkxiTehnzSmSe2XqrBj4w32RUN966rdz8',
|
||||||
|
'ticker': 'USDC',
|
||||||
|
'show_balance': True
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'nile': {
|
||||||
|
'usdt': {
|
||||||
|
'contract': 'TXLAQ63Xg1NAzckPwKHvzw7CSEmLMEqcdj',
|
||||||
|
'ticker': 'USDT',
|
||||||
|
'show_balance': True
|
||||||
|
},
|
||||||
|
'usdc': {
|
||||||
|
'contract': 'TEMVynQpntMqkPxP6wXTW2K7e4sM3cRmWz',
|
||||||
|
'ticker': 'USDC',
|
||||||
|
'show_balance': True
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'shasta': {
|
||||||
|
'usdt': {
|
||||||
|
'contract': 'TG3XXyExBkPp9nzdajDZsozEu4BkaSJozs',
|
||||||
|
'ticker': 'USDT',
|
||||||
|
'show_balance': True
|
||||||
|
},
|
||||||
|
'usdc': {
|
||||||
|
'contract': 'TSdZwNqpHofzP6BsBKGQUWdBeJphLmF6id',
|
||||||
|
'ticker': 'USDC',
|
||||||
|
'show_balance': True
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# helper functions
|
||||||
|
|
||||||
|
# print operation result according to the below convention
|
||||||
|
def presult(resdict: dict):
|
||||||
|
if 'success' not in resdict:
|
||||||
|
resdict['success'] = False
|
||||||
|
if 'message' not in resdict:
|
||||||
|
resdict['message'] = ''
|
||||||
|
if resdict['success'] == True:
|
||||||
|
print ('Success:\n%s\n' % resdict['message'])
|
||||||
|
else:
|
||||||
|
print ('Error:\n%s\n' % resdict['message'])
|
||||||
|
|
||||||
|
# confirm transactions
|
||||||
|
def trconfirm(promptstr: str):
|
||||||
|
choice = input(promptstr).lower()
|
||||||
|
if choice != 'yes':
|
||||||
|
print('Transaction not confirmed, aborting!')
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# instantiate tronpy client
|
||||||
|
def tronclient(network='mainnet'):
|
||||||
|
network = network.lower() # lowercase network name
|
||||||
|
if network == 'mainnet': # use a public node or TronGrid API for mainnet
|
||||||
|
if TG_API_KEY is not None: # TG_API_KEY must be set
|
||||||
|
client = Tron(network=network, conf={'fee_limit': KT_FEE_LIMIT},
|
||||||
|
provider=HTTPProvider(api_key=TG_API_KEY, timeout=KT_TIMEOUT))
|
||||||
|
else: # NODE_ADDR must be set
|
||||||
|
client = Tron(network=network, conf={'fee_limit': KT_FEE_LIMIT},
|
||||||
|
provider=HTTPProvider(NODE_ADDR, timeout=KT_TIMEOUT))
|
||||||
|
else: # any default API is fine for a testnet
|
||||||
|
client = Tron(network=network,
|
||||||
|
conf={'fee_limit': KT_TEST_FEE_LIMIT, 'timeout': KT_TIMEOUT})
|
||||||
|
return client
|
||||||
|
|
||||||
|
# get TRC20 token balance of an address (in float)
|
||||||
|
def trc20balance(client: Tron, contractaddr: str, targetaddr: str):
|
||||||
|
cntr = client.get_contract(contractaddr)
|
||||||
|
precision = cntr.functions.decimals()
|
||||||
|
return cntr.functions['balanceOf'](targetaddr) / (10 ** precision)
|
||||||
|
|
||||||
|
# operation definitions
|
||||||
|
# all operations return a dictionary with the following fields:
|
||||||
|
# success: True/False
|
||||||
|
# message: string
|
||||||
|
# (optional) result: dictionary
|
||||||
|
|
||||||
|
# create a new wallet
|
||||||
|
# input: network, passphrase, word count (12, 15, 18, 21, 24), derivation path
|
||||||
|
# output: private key, address, mnemonic
|
||||||
|
def newwallet(network: str, passphrase: str, wordcount: int, dpath: str):
|
||||||
|
res = {'success': False, 'message': '', 'result': {}}
|
||||||
|
if wordcount not in [12, 15, 18, 21, 24]:
|
||||||
|
res['message'] = 'Invalid word count for mnemonic generation'
|
||||||
|
return res
|
||||||
|
client = tronclient(network)
|
||||||
|
wallet, mnemo = client.generate_address_with_mnemonic(passphrase = passphrase,
|
||||||
|
num_words = wordcount, language = 'english', account_path = dpath)
|
||||||
|
wallet['mnemonic'] = mnemo
|
||||||
|
res['success'] = True
|
||||||
|
res['result'] = wallet
|
||||||
|
res['message'] = 'Wallet created, save this info in a secure place!\n\n'
|
||||||
|
res['message'] += 'Address: %s\n' % wallet['base58check_address']
|
||||||
|
res['message'] += 'Address (hex): %s\n' % wallet['hex_address']
|
||||||
|
res['message'] += 'Private key (hex): %s\n' % wallet['private_key']
|
||||||
|
res['message'] += 'Public key (hex): %s\n' % wallet['public_key']
|
||||||
|
res['message'] += 'Mnemonic: %s' % wallet['mnemonic']
|
||||||
|
return res
|
||||||
|
|
||||||
|
# import a wallet
|
||||||
|
# input: network, mnemonic, passphrase, derivation path
|
||||||
|
# output: private key, address
|
||||||
|
def importwallet(network: str, mnemo: str, passphrase: str, dpath: str):
|
||||||
|
res = {'success': False, 'message': '', 'result': {}}
|
||||||
|
client = tronclient(network)
|
||||||
|
try:
|
||||||
|
wallet = client.generate_address_from_mnemonic(mnemonic = mnemo,
|
||||||
|
passphrase = passphrase, account_path = dpath)
|
||||||
|
except (mnemonic.mnemonic.ConfigurationError,
|
||||||
|
eth_utils.exceptions.ValidationError):
|
||||||
|
res['message'] = 'Invalid mnemonic phrase!'
|
||||||
|
return res
|
||||||
|
wallet['mnemonic'] = mnemo
|
||||||
|
res['success'] = True
|
||||||
|
res['result'] = wallet
|
||||||
|
res['message'] = 'Wallet imported, save this info in a secure place!\n\n'
|
||||||
|
res['message'] += 'Address: %s\n' % wallet['base58check_address']
|
||||||
|
res['message'] += 'Address (hex): %s\n' % wallet['hex_address']
|
||||||
|
res['message'] += 'Private key (hex): %s\n' % wallet['private_key']
|
||||||
|
res['message'] += 'Public key (hex): %s\n' % wallet['public_key']
|
||||||
|
res['message'] += 'Mnemonic: %s' % wallet['mnemonic']
|
||||||
|
return res
|
||||||
|
|
||||||
|
# get account information by Tron address
|
||||||
|
# input: network, address
|
||||||
|
# output: info(dictionary)
|
||||||
|
def infobyaddr(network: str, address: str):
|
||||||
|
res = {'success': False, 'message': '', 'result': {}}
|
||||||
|
client = tronclient(network)
|
||||||
|
try:
|
||||||
|
rawinfo = client.get_account(address)
|
||||||
|
except BadAddress:
|
||||||
|
res['message'] = 'Invalid address! Set the correct one with -addr flag'
|
||||||
|
return res
|
||||||
|
except AddressNotFound:
|
||||||
|
res['message'] = 'Address not found on the blockchain! Set the correct one with -addr flag'
|
||||||
|
return res
|
||||||
|
# get bandwidth and energy information
|
||||||
|
rawresinfo = client.get_account_resource(address)
|
||||||
|
# some values used in bandwidth calculation
|
||||||
|
freenetlimit = 0
|
||||||
|
freenetused = 0
|
||||||
|
netlimit = 0
|
||||||
|
netused = 0
|
||||||
|
if 'freeNetLimit' in rawresinfo:
|
||||||
|
freenetlimit = rawresinfo['freeNetLimit']
|
||||||
|
if 'freeNetUsed' in rawresinfo:
|
||||||
|
freenetlimit = rawresinfo['freeNetUsed']
|
||||||
|
if 'NetLimit' in rawresinfo:
|
||||||
|
netlimit = rawresinfo['NetLimit']
|
||||||
|
if 'NetUsed' in rawresinfo:
|
||||||
|
netused = rawresinfo['NetUsed']
|
||||||
|
bandwidth = freenetlimit - freenetused + netlimit - netused
|
||||||
|
# some values used in energy calculation
|
||||||
|
energylimit = 0
|
||||||
|
energyused = 0
|
||||||
|
if 'EnergyLimit' in rawresinfo:
|
||||||
|
energylimit = rawresinfo['EnergyLimit']
|
||||||
|
if 'EnergyUsed' in rawresinfo:
|
||||||
|
energyused = rawresinfo['EnergyUsed']
|
||||||
|
# info dictionary received, parse it
|
||||||
|
info = {
|
||||||
|
'address': rawinfo['address'], # wallet address
|
||||||
|
'trxbalance': rawinfo['balance'], # TRX wallet balance (in Sun)
|
||||||
|
'frozen': [], # hold frozen TRX info here
|
||||||
|
'trc20balance': {}, # hold TRC20 token balances here
|
||||||
|
'bandwidth': bandwidth, # bandwidth
|
||||||
|
'energy': energylimit - energyused # energy
|
||||||
|
}
|
||||||
|
# iterate over TRX freeze records
|
||||||
|
for el in rawinfo['frozenV2']:
|
||||||
|
if 'type' in el and 'amount' in el and el['amount'] > 0:
|
||||||
|
info['frozen'].append({'type': el['type'], 'amount': el['amount']})
|
||||||
|
# iterate over hardcoded TRC20 tokens
|
||||||
|
for tkn in KT_TRC20_TOKENS[network]:
|
||||||
|
tknobj = KT_TRC20_TOKENS[network][tkn]
|
||||||
|
if tknobj['show_balance'] == True:
|
||||||
|
info['trc20balance'][tknobj['ticker']] = trc20balance(client,
|
||||||
|
tknobj['contract'], address)
|
||||||
|
res['success'] = True
|
||||||
|
res['result'] = info
|
||||||
|
res['message'] = 'Address: %s\nBalance:\n' % info['address']
|
||||||
|
res['message'] += '%f TRX\n' % (info['trxbalance'] / TRX_SCALE)
|
||||||
|
for ticker in info['trc20balance']:
|
||||||
|
res['message'] += '%f %s\n' % (info['trc20balance'][ticker], ticker)
|
||||||
|
res['message'] += 'Frozen assets:\n'
|
||||||
|
for el in info['frozen']:
|
||||||
|
res['message'] += '%s\t%f TRX\n' % (el['type'], el['amount'] / TRX_SCALE)
|
||||||
|
res['message'] += 'Remaining bandwidth: %s\n' % str(info['bandwidth'])
|
||||||
|
res['message'] += 'Remaining energy: %s' % str(info['energy'])
|
||||||
|
return res
|
||||||
|
|
||||||
|
# get own account information by private key
|
||||||
|
# input: network, private key
|
||||||
|
# output: info(dictionary)
|
||||||
|
def infobypk(network: str, pk: str):
|
||||||
|
res = {'success': False, 'message': '', 'result': {}}
|
||||||
|
try:
|
||||||
|
rpk = PrivateKey(bytes.fromhex(pk))
|
||||||
|
except (BadKey, ValueError):
|
||||||
|
res['message'] = 'Invalid private key!'
|
||||||
|
return res
|
||||||
|
client = tronclient(network)
|
||||||
|
addr = client.generate_address(rpk)['base58check_address']
|
||||||
|
return infobyaddr(network, addr)
|
||||||
|
|
||||||
|
# send TRX transaction (and wait for its completion)
|
||||||
|
# input: network, private key, target address, amount (in TRX)
|
||||||
|
# output: transaction receipt object
|
||||||
|
def sendtrx(network: str, pk: str, toaddr: str, amt: float):
|
||||||
|
res = {'success': False, 'message': '', 'result': {}}
|
||||||
|
if amt < 1:
|
||||||
|
res['message'] = 'Nothing to transfer!'
|
||||||
|
return res
|
||||||
|
try:
|
||||||
|
rpk = PrivateKey(bytes.fromhex(pk))
|
||||||
|
except (BadKey, ValueError):
|
||||||
|
res['message'] = 'Invalid private key!'
|
||||||
|
return res
|
||||||
|
client = tronclient(network)
|
||||||
|
fromaddr = client.generate_address(rpk)['base58check_address']
|
||||||
|
# build and sign the transaction
|
||||||
|
txn = client.trx.transfer(fromaddr, toaddr,
|
||||||
|
int(amt * TRX_SCALE)).build().sign(rpk)
|
||||||
|
# send the transaction
|
||||||
|
try:
|
||||||
|
receipt = txn.broadcast().wait()
|
||||||
|
except ValidationError:
|
||||||
|
res['message'] = 'Insufficient balance or other validation error!'
|
||||||
|
return res
|
||||||
|
res['success'] = True
|
||||||
|
res['message'] = 'Sent %f TRX to %s\nTransaction ID %s' % (amt,
|
||||||
|
toaddr, receipt['id'])
|
||||||
|
res['result'] = receipt
|
||||||
|
return res
|
||||||
|
|
||||||
|
# send TRC20 token transaction (and wait for its completion)
|
||||||
|
# input: network, private key, token ticker, target address, amount (in tokens)
|
||||||
|
# output: transaction receipt object
|
||||||
|
def sendtrc20(network: str, pk: str, ticker: str, toaddr: str, amt: float):
|
||||||
|
res = {'success': False, 'message': '', 'result': {}}
|
||||||
|
ticker = ticker.lower()
|
||||||
|
if ticker not in KT_TRC20_TOKENS[network]:
|
||||||
|
res['message'] = 'Token not supported by kisstron for %s!' % network
|
||||||
|
return res
|
||||||
|
if amt < 1:
|
||||||
|
res['message'] = 'Nothing to transfer!'
|
||||||
|
return res
|
||||||
|
try:
|
||||||
|
rpk = PrivateKey(bytes.fromhex(pk))
|
||||||
|
except (BadKey, ValueError):
|
||||||
|
res['message'] = 'Invalid private key!'
|
||||||
|
return res
|
||||||
|
client = tronclient(network)
|
||||||
|
fromaddr = client.generate_address(rpk)['base58check_address']
|
||||||
|
# instantiate the contract
|
||||||
|
contractaddr = KT_TRC20_TOKENS[network][ticker]['contract']
|
||||||
|
cntr = client.get_contract(contractaddr)
|
||||||
|
precision = cntr.functions.decimals()
|
||||||
|
scaler = 10 ** precision
|
||||||
|
# build and sign the transaction
|
||||||
|
txn = cntr.functions.transfer(toaddr,
|
||||||
|
int(amt * scaler)).with_owner(fromaddr).build().sign(rpk)
|
||||||
|
# send the transaction
|
||||||
|
try:
|
||||||
|
receipt = txn.broadcast().wait()
|
||||||
|
except ValidationError:
|
||||||
|
res['message'] = 'Insufficient balance or other validation error!'
|
||||||
|
return res
|
||||||
|
res['result'] = receipt
|
||||||
|
if receipt['receipt']['result'] == 'OUT_OF_ENERGY':
|
||||||
|
res['message'] = 'Not enough energy for transaction!'
|
||||||
|
return res
|
||||||
|
res['success'] = True
|
||||||
|
res['message'] = 'Sent %f %s to %s\nTransaction ID %s' % (amt,
|
||||||
|
KT_TRC20_TOKENS[network][ticker]['ticker'],
|
||||||
|
toaddr, receipt['id'])
|
||||||
|
return res
|
||||||
|
|
||||||
|
# freeze TRX for energy (and wait for its completion)
|
||||||
|
# input: network, private key, amount
|
||||||
|
# output: transaction receipt object
|
||||||
|
def freezetrx(network: str, pk: str, amt: float):
|
||||||
|
res = {'success': False, 'message': '', 'result': {}}
|
||||||
|
if amt < 1:
|
||||||
|
res['message'] = 'Nothing to freeze!'
|
||||||
|
return res
|
||||||
|
try:
|
||||||
|
rpk = PrivateKey(bytes.fromhex(pk))
|
||||||
|
except (BadKey, ValueError):
|
||||||
|
res['message'] = 'Invalid private key!'
|
||||||
|
return res
|
||||||
|
client = tronclient(network)
|
||||||
|
myaddr = client.generate_address(rpk)['base58check_address']
|
||||||
|
# build and sign the transaction
|
||||||
|
txn = client.trx.freeze_balance(myaddr, int(amt * TRX_SCALE),
|
||||||
|
"ENERGY").build().sign(rpk)
|
||||||
|
# send the transaction
|
||||||
|
try:
|
||||||
|
receipt = txn.broadcast().wait()
|
||||||
|
except ValidationError:
|
||||||
|
res['message'] = 'Insufficient balance or other validation error!'
|
||||||
|
return res
|
||||||
|
res['success'] = True
|
||||||
|
res['message'] = 'Frozen %f TRX at %s\nTransaction ID %s' % (amt,
|
||||||
|
myaddr, receipt['id'])
|
||||||
|
res['result'] = receipt
|
||||||
|
return res
|
||||||
|
|
||||||
|
# unfreeze TRX for energy (and wait for its completion)
|
||||||
|
# input: network, private key, amount
|
||||||
|
# output: transaction receipt object
|
||||||
|
def unfreezetrx(network: str, pk: str, amt: float):
|
||||||
|
res = {'success': False, 'message': '', 'result': {}}
|
||||||
|
if amt < 1:
|
||||||
|
res['message'] = 'Nothing to unfreeze!'
|
||||||
|
return res
|
||||||
|
try:
|
||||||
|
rpk = PrivateKey(bytes.fromhex(pk))
|
||||||
|
except (BadKey, ValueError):
|
||||||
|
res['message'] = 'Invalid private key!'
|
||||||
|
return res
|
||||||
|
client = tronclient(network)
|
||||||
|
myaddr = client.generate_address(rpk)['base58check_address']
|
||||||
|
# build and sign the transaction
|
||||||
|
txn = client.trx.unfreeze_balance(owner = myaddr,
|
||||||
|
unfreeze_balance = int(amt * TRX_SCALE),
|
||||||
|
resource = "ENERGY").build().sign(rpk)
|
||||||
|
# send the transaction
|
||||||
|
try:
|
||||||
|
receipt = txn.broadcast().wait()
|
||||||
|
except ValidationError:
|
||||||
|
res['message'] = 'Insufficient balance or other validation error!'
|
||||||
|
return res
|
||||||
|
res['success'] = True
|
||||||
|
res['message'] = 'Unfrozen %f TRX at %s\nTransaction ID %s' % (amt,
|
||||||
|
myaddr, receipt['id'])
|
||||||
|
res['result'] = receipt
|
||||||
|
return res
|
||||||
|
|
||||||
|
if __name__ == '__main__': # main app start
|
||||||
|
from argparse import ArgumentParser
|
||||||
|
parser = ArgumentParser(description='kisstron: a simple CLI wallet for Tron blockchain', epilog='(c) Luxferre 2024 --- No rights reserved <https://unlicense.org>')
|
||||||
|
parser.add_argument('op', help='Operation: new, import, info, send, fre[eze], unf[reeze]')
|
||||||
|
parser.add_argument('coin', help='Coin to operate on: trx (default), usdt, usdc', default='trx')
|
||||||
|
parser.add_argument('-net', '--network', default='mainnet', help='(op: all) Network: mainnet (default), nile, shasta')
|
||||||
|
parser.add_argument('-node', '--tron-node', default=NODE_ADDR, help='(op: all) Custom Tron node HTTP address (default %s)' % NODE_ADDR)
|
||||||
|
parser.add_argument('-tgkey', '--trongrid-api-key', default=None, help='(op: all) TronGrid API key (default None)')
|
||||||
|
parser.add_argument('-addr', '--address', default=None, help='(op: info, send) Tron address to send to or display information about (-pk overrides this to display own info)')
|
||||||
|
parser.add_argument('-amt', '--amount', type=float, default=0, help='(op: send, fre, unf) Amount to send/freeze/unfreeze')
|
||||||
|
parser.add_argument('-pk', '--private-key', default=None, help='(op: info, send, fre, unf) Private key for transactions or own information, in hex')
|
||||||
|
parser.add_argument('-pp', '--passphrase', default='', help='(op: new, import) Mnemonic encryption passphrase (default empty)')
|
||||||
|
parser.add_argument('-wc', '--words-count', type=int, default=12, help='(op: new) Word count in the mnemonic: 12 (default), 15, 18, 21, 24')
|
||||||
|
parser.add_argument('-dp', '--derivation-path', default="m/44'/195'/0'/0/0", help="(op: new, import) BIP-39 derivation path for mnemonic (default is m/44'/195'/0'/0/0)")
|
||||||
|
parser.add_argument('-nc', '--no-confirm', action='store_true', help='(op: send, fre, unf) Do not ask to type "yes" for transaction confirmation (dangerous)')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# fix all relevant parameters (and lowercase where applicable)
|
||||||
|
op = args.op.lower() # operation
|
||||||
|
coin = args.coin.lower() # coin/token
|
||||||
|
tronnet = args.network.lower() # Tron network (mainnet, nile, shasta)
|
||||||
|
targetaddr = args.address # address parameter
|
||||||
|
amount = float(args.amount) # amount to send/freeze/unfreeze
|
||||||
|
pk = args.private_key # private key (in hex)
|
||||||
|
passphrase = args.passphrase # mnemonic encryption passphrase
|
||||||
|
wcount = int(args.words_count) # mnemonic word count (for new operation)
|
||||||
|
dpath = args.derivation_path # mnemonic derivation path (for new/import)
|
||||||
|
trans_confirm = True # ask for transaction confirmation ("yes")
|
||||||
|
NODE_ADDR = args.tron_node # custom Tron node HTTP address
|
||||||
|
TG_API_KEY = args.trongrid_api_key # optional TronGrid API key
|
||||||
|
if args.no_confirm == True:
|
||||||
|
trans_confirm = False
|
||||||
|
|
||||||
|
# check the constraints for every action and call appropriate functions
|
||||||
|
|
||||||
|
opres = {} # operation result placeholder
|
||||||
|
if op == 'new': # new wallet
|
||||||
|
opres = newwallet(tronnet, passphrase, wcount, dpath)
|
||||||
|
elif op == 'import': # import wallet
|
||||||
|
mnemo = input('Your mnemonic phrase: ')
|
||||||
|
opres = importwallet(tronnet, mnemo, passphrase, dpath)
|
||||||
|
elif op == 'info': # info on own or foreign address
|
||||||
|
if pk == None: # public info, address must be set
|
||||||
|
opres = infobyaddr(tronnet, targetaddr)
|
||||||
|
else: # own info, private key must be set
|
||||||
|
opres = infobypk(tronnet, pk)
|
||||||
|
elif op == 'send': # send transaction
|
||||||
|
if trans_confirm == True:
|
||||||
|
trconfirm('Enter "yes" to confirm sending funds: ')
|
||||||
|
if coin == 'trx': # send TRX
|
||||||
|
opres = sendtrx(tronnet, pk, targetaddr, amount)
|
||||||
|
else: # send one of the predefined TRC20 tokens
|
||||||
|
opres = sendtrc20(tronnet, pk, coin, targetaddr, amount)
|
||||||
|
elif op == 'fre' or op == 'freeze': # freeze TRX for energy
|
||||||
|
if trans_confirm == True:
|
||||||
|
trconfirm('Enter "yes" to confirm freezing funds: ')
|
||||||
|
opres = freezetrx(tronnet, pk, amount)
|
||||||
|
elif op == 'unf' or op == 'unfreeze': # unfreeze TRX
|
||||||
|
if trans_confirm == True:
|
||||||
|
trconfirm('Enter "yes" to confirm unfreezing funds: ')
|
||||||
|
opres = unfreezetrx(tronnet, pk, amount)
|
||||||
|
else: # unknown operation
|
||||||
|
print('Unknown operation!')
|
||||||
|
print('Run the script with -h parameter to see available options')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
presult(opres) # print out the result
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
cffi>=1.16.0
|
||||||
|
coincurve>=18.0.0
|
||||||
|
cytoolz>=0.12.3
|
||||||
|
mnemonic>=0.20
|
||||||
|
pycryptodome>=3.18.0
|
||||||
|
pycryptodomex>=3.18.0
|
||||||
|
tronpy>=0.4.0
|
||||||
Reference in New Issue
Block a user