Offline mode implemented

This commit is contained in:
Luxferre
2024-02-15 10:44:32 +02:00
parent ce7ad5189c
commit db24b89952
2 changed files with 139 additions and 54 deletions
+20 -4
View File
@@ -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?
+119 -50
View File
@@ -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['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'] = 'Unfrozen %f TRX at %s\nTransaction ID %s' % (amt,
myaddr, receipt['id'])
res['result'] = receipt
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')