commit db24b899521b06959ff4292b591cd35d10f2c85b
parent ce7ad5189c4d29344daa69779ebf5c2aec87152f
Author: Luxferre <lux@ferre>
Date: Thu, 15 Feb 2024 10:44:32 +0200
Offline mode implemented
Diffstat:
M | README | | | 24 | ++++++++++++++++++++---- |
M | kisstron.py | | | 169 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------ |
2 files changed, 139 insertions(+), 54 deletions(-)
diff --git a/README b/README
@@ -16,6 +16,7 @@ similar third-party projects.
* 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)
+* Offline mode for send/freeze/unfreeze transactions
* 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
@@ -145,9 +146,25 @@ 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.
+-- Offline operations --
+
+The send trx, send usdt, send usdc, fre trx and unf trx subcommands support the
+-o flag that, after the "Success:" line, outputs a JSON of a signed transaction.
+You can copy or otherwise transfer this output to a different machine running
+kisstron and broadcast it to the network with bro trx subcommand:
+
+./kisstron.py bro trx [netopts]
+
+You can then paste the transaction JSON into the input field, press Enter and,
+unless -nc flag is passed too, confirm sending the transaction by entering yes.
+The transaction will be broadcast and processed as usual.
+
+Keep in mind that Tron transactions expire very quickly, so you have a limited
+time window between signing and publishing them.
+
== FAQ ==
-- Can I trust it?
+- Can I trust this wallet?
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
@@ -179,9 +196,8 @@ 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.
+You can also use offline mode to split signing and sending transactions, as
+shown in this document.
- Why doesn't kisstron support TRC-10, multisig, witnesses, history etc?
diff --git a/kisstron.py b/kisstron.py
@@ -11,6 +11,7 @@
# * 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
+# * offline mode with -o switch and bro[adcast] operation
#
# 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.
@@ -19,11 +20,14 @@
#
# Created by Luxferre in 2024, released into public domain
-import sys
+import sys, json
from tronpy import Tron
+from tronpy.tron import Transaction
from tronpy.keys import PrivateKey
from tronpy.providers import HTTPProvider
-from tronpy.exceptions import BadAddress, AddressNotFound, BadKey, ValidationError
+from tronpy.exceptions import (BadAddress, AddressNotFound,
+ BadKey, ValidationError, TransactionError)
+from json.decoder import JSONDecodeError
import mnemonic, eth_utils
# some global parameters (hardcoded)
@@ -258,7 +262,8 @@ def infobypk(network: str, pk: str):
# 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):
+def sendtrx(network: str, pk: str, toaddr: str, amt: float,
+ offline: bool = False):
res = {'success': False, 'message': '', 'result': {}}
if amt < 1:
res['message'] = 'Nothing to transfer!'
@@ -273,22 +278,27 @@ def sendtrx(network: str, pk: str, toaddr: str, amt: float):
# 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
+ if offline == True: # generate transaction JSON
+ res['success'] = True
+ res['result'] = txn
+ res['message'] = json.dumps(txn.to_json(), separators=(',', ':'))
+ else: # send the transaction
+ try:
+ receipt = txn.broadcast().wait()
+ except ValidationError as e:
+ res['message'] = str(e)
+ 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):
+def sendtrc20(network: str, pk: str, ticker: str, toaddr: str,
+ amt: float, offline: bool = False):
res = {'success': False, 'message': '', 'result': {}}
ticker = ticker.lower()
if ticker not in KT_TRC20_TOKENS[network]:
@@ -312,26 +322,30 @@ def sendtrc20(network: str, pk: str, ticker: str, toaddr: str, amt: float):
# 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'])
+ if offline == True: # generate transaction JSON
+ res['success'] = True
+ res['result'] = txn
+ res['message'] = json.dumps(txn.to_json(), separators=(',', ':'))
+ else: # send the transaction
+ try:
+ receipt = txn.broadcast().wait()
+ except ValidationError as e:
+ res['message'] = str(e)
+ 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):
+def freezetrx(network: str, pk: str, amt: float, offline: bool = False):
res = {'success': False, 'message': '', 'result': {}}
if amt < 1:
res['message'] = 'Nothing to freeze!'
@@ -346,22 +360,26 @@ def freezetrx(network: str, pk: str, amt: float):
# 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
+ if offline == True: # generate transaction JSON
+ res['success'] = True
+ res['result'] = txn
+ res['message'] = json.dumps(txn.to_json(), separators=(',', ':'))
+ else: # send the transaction
+ try:
+ receipt = txn.broadcast().wait()
+ except ValidationError as e:
+ res['message'] = str(e)
+ 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):
+def unfreezetrx(network: str, pk: str, amt: float, offline: bool = False):
res = {'success': False, 'message': '', 'result': {}}
if amt < 1:
res['message'] = 'Nothing to unfreeze!'
@@ -377,22 +395,58 @@ def unfreezetrx(network: str, pk: str, amt: float):
txn = client.trx.unfreeze_balance(owner = myaddr,
unfreeze_balance = int(amt * TRX_SCALE),
resource = "ENERGY").build().sign(rpk)
+ if offline == True: # generate transaction JSON
+ res['success'] = True
+ res['result'] = txn
+ res['message'] = json.dumps(txn.to_json(), separators=(',', ':'))
+ else: # send the transaction
+ try:
+ receipt = txn.broadcast().wait()
+ except ValidationError as e:
+ res['message'] = str(e)
+ return res
+ res['success'] = True
+ res['message'] = 'Unfrozen %f TRX at %s\nTransaction ID %s' % (amt,
+ myaddr, receipt['id'])
+ res['result'] = receipt
+ return res
+
+# Broadcast transaction JSON saved in kisstron offline mode
+# input: network, transaction JSON string
+# output: transaction receipt object
+def broadcastjson(network, tjson):
+ res = {'success': False, 'message': '', 'result': {}}
+ # import the transaction from JSON
+ try:
+ txn = Transaction.from_json(json.loads(tjson))
+ except (TypeError, ValidationError, TransactionError,
+ JSONDecodeError, KeyError):
+ res['message'] = 'Malformed transaction JSON!'
+ return res
+ # instantiate a client for the transaction
+ txn._client = tronclient(network)
# send the transaction
try:
receipt = txn.broadcast().wait()
- except ValidationError:
- res['message'] = 'Insufficient balance or other validation error!'
+ except ValidationError as e:
+ res['message'] = str(e)
+ return res
+ except TransactionError as e:
+ res['message'] = str(e)
return res
- res['success'] = True
- res['message'] = 'Unfrozen %f TRX at %s\nTransaction ID %s' % (amt,
- myaddr, receipt['id'])
res['result'] = receipt
+ if ('receipt' in receipt and 'result' in receipt['receipt']
+ and receipt['receipt']['result'] == 'OUT_OF_ENERGY'):
+ res['message'] = 'Not enough energy for transaction!'
+ return res
+ res['success'] = True
+ res['message'] = 'Transaction sent\nTransaction ID %s' % receipt['id']
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('op', help='Operation: new, import, info, send, fre[eze], unf[reeze], bro[adcast]')
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)
@@ -400,6 +454,7 @@ if __name__ == '__main__': # main app start
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('-o', '--offline', action='store_true', help='(op: send, fre, unf) Build and sign the transaction in offline mode and output its JSON')
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)")
@@ -420,6 +475,9 @@ if __name__ == '__main__': # main app start
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
+ offline_mode = False # offline mode
+ if args.offline == True:
+ offline_mode = True
if args.no_confirm == True:
trans_confirm = False
@@ -432,6 +490,9 @@ if __name__ == '__main__': # main app start
mnemo = input('Your mnemonic phrase: ')
opres = importwallet(tronnet, mnemo, passphrase, dpath)
elif op == 'info': # info on own or foreign address
+ if offline_mode == True:
+ print('Info operation is not available in offline mode!')
+ sys.exit(1)
if pk == None: # public info, address must be set
opres = infobyaddr(tronnet, targetaddr)
else: # own info, private key must be set
@@ -440,17 +501,25 @@ if __name__ == '__main__': # main app start
if trans_confirm == True:
trconfirm('Enter "yes" to confirm sending funds: ')
if coin == 'trx': # send TRX
- opres = sendtrx(tronnet, pk, targetaddr, amount)
+ opres = sendtrx(tronnet, pk, targetaddr, amount, offline_mode)
else: # send one of the predefined TRC20 tokens
- opres = sendtrc20(tronnet, pk, coin, targetaddr, amount)
+ opres = sendtrc20(tronnet, pk, coin, targetaddr, amount, offline_mode)
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)
+ opres = freezetrx(tronnet, pk, amount, offline_mode)
elif op == 'unf' or op == 'unfreeze': # unfreeze TRX
if trans_confirm == True:
trconfirm('Enter "yes" to confirm unfreezing funds: ')
- opres = unfreezetrx(tronnet, pk, amount)
+ opres = unfreezetrx(tronnet, pk, amount, offline_mode)
+ elif op == 'bro' or op == 'broadcast': # broadcast signed JSON transaction
+ if offline_mode == True:
+ print('Broadcast operation is not available in offline mode!')
+ sys.exit(1)
+ transjson = input('Input transaction JSON: ')
+ if trans_confirm == True:
+ trconfirm('Enter "yes" to confirm sending the transaction: ')
+ opres = broadcastjson(tronnet, transjson)
else: # unknown operation
print('Unknown operation!')
print('Run the script with -h parameter to see available options')