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 * Sending TRX/USDT/USDC on all supported networks
* Freezing (for energy) and unfreezing TRX 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) * 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) * No filesystem interaction (that's a feature, not a bug: see the FAQ section)
* Full scriptability: send/freeze/unfreeze transactions require manual approval * Full scriptability: send/freeze/unfreeze transactions require manual approval
but it actually can be turned off with a special commandline parameter for the 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. 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 == == FAQ ==
- Can I trust it? - Can I trust this wallet?
The author trusts it. In fact, kisstron was created because of lack of decent 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 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 private key never leaves your machine. It uses libsecp256k1 via coincurve
library binding for transaction signing. library binding for transaction signing.
Ability to implement offline mode (transactions are created and signed on one You can also use offline mode to split signing and sending transactions, as
kisstron instance without Internet connectivity, then broadcast from another) shown in this document.
in the future versions is being studied.
- Why doesn't kisstron support TRC-10, multisig, witnesses, history etc? - Why doesn't kisstron support TRC-10, multisig, witnesses, history etc?
+92 -23
View File
@@ -11,6 +11,7 @@
# * ability to select testnets (nile, shasta) if necessary # * ability to select testnets (nile, shasta) if necessary
# * ability to connect to custom mainnet nodes if necessary # * ability to connect to custom mainnet nodes if necessary
# * switching to TronGrid API by providing your own API key # * 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. # 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. # For mnemonic phrase generation, only English is currently supported.
@@ -19,11 +20,14 @@
# #
# Created by Luxferre in 2024, released into public domain # Created by Luxferre in 2024, released into public domain
import sys import sys, json
from tronpy import Tron from tronpy import Tron
from tronpy.tron import Transaction
from tronpy.keys import PrivateKey from tronpy.keys import PrivateKey
from tronpy.providers import HTTPProvider 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 import mnemonic, eth_utils
# some global parameters (hardcoded) # some global parameters (hardcoded)
@@ -258,7 +262,8 @@ def infobypk(network: str, pk: str):
# send TRX transaction (and wait for its completion) # send TRX transaction (and wait for its completion)
# input: network, private key, target address, amount (in TRX) # input: network, private key, target address, amount (in TRX)
# output: transaction receipt object # 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': {}} res = {'success': False, 'message': '', 'result': {}}
if amt < 1: if amt < 1:
res['message'] = 'Nothing to transfer!' res['message'] = 'Nothing to transfer!'
@@ -273,11 +278,15 @@ def sendtrx(network: str, pk: str, toaddr: str, amt: float):
# build and sign the transaction # build and sign the transaction
txn = client.trx.transfer(fromaddr, toaddr, txn = client.trx.transfer(fromaddr, toaddr,
int(amt * TRX_SCALE)).build().sign(rpk) int(amt * TRX_SCALE)).build().sign(rpk)
# send the transaction 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: try:
receipt = txn.broadcast().wait() receipt = txn.broadcast().wait()
except ValidationError: except ValidationError as e:
res['message'] = 'Insufficient balance or other validation error!' res['message'] = str(e)
return res return res
res['success'] = True res['success'] = True
res['message'] = 'Sent %f TRX to %s\nTransaction ID %s' % (amt, res['message'] = 'Sent %f TRX to %s\nTransaction ID %s' % (amt,
@@ -288,7 +297,8 @@ def sendtrx(network: str, pk: str, toaddr: str, amt: float):
# send TRC20 token transaction (and wait for its completion) # send TRC20 token transaction (and wait for its completion)
# input: network, private key, token ticker, target address, amount (in tokens) # input: network, private key, token ticker, target address, amount (in tokens)
# output: transaction receipt object # 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': {}} res = {'success': False, 'message': '', 'result': {}}
ticker = ticker.lower() ticker = ticker.lower()
if ticker not in KT_TRC20_TOKENS[network]: if ticker not in KT_TRC20_TOKENS[network]:
@@ -312,11 +322,15 @@ def sendtrc20(network: str, pk: str, ticker: str, toaddr: str, amt: float):
# build and sign the transaction # build and sign the transaction
txn = cntr.functions.transfer(toaddr, txn = cntr.functions.transfer(toaddr,
int(amt * scaler)).with_owner(fromaddr).build().sign(rpk) int(amt * scaler)).with_owner(fromaddr).build().sign(rpk)
# send the transaction 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: try:
receipt = txn.broadcast().wait() receipt = txn.broadcast().wait()
except ValidationError: except ValidationError as e:
res['message'] = 'Insufficient balance or other validation error!' res['message'] = str(e)
return res return res
res['result'] = receipt res['result'] = receipt
if receipt['receipt']['result'] == 'OUT_OF_ENERGY': if receipt['receipt']['result'] == 'OUT_OF_ENERGY':
@@ -331,7 +345,7 @@ def sendtrc20(network: str, pk: str, ticker: str, toaddr: str, amt: float):
# freeze TRX for energy (and wait for its completion) # freeze TRX for energy (and wait for its completion)
# input: network, private key, amount # input: network, private key, amount
# output: transaction receipt object # 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': {}} res = {'success': False, 'message': '', 'result': {}}
if amt < 1: if amt < 1:
res['message'] = 'Nothing to freeze!' res['message'] = 'Nothing to freeze!'
@@ -346,11 +360,15 @@ def freezetrx(network: str, pk: str, amt: float):
# build and sign the transaction # build and sign the transaction
txn = client.trx.freeze_balance(myaddr, int(amt * TRX_SCALE), txn = client.trx.freeze_balance(myaddr, int(amt * TRX_SCALE),
"ENERGY").build().sign(rpk) "ENERGY").build().sign(rpk)
# send the transaction 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: try:
receipt = txn.broadcast().wait() receipt = txn.broadcast().wait()
except ValidationError: except ValidationError as e:
res['message'] = 'Insufficient balance or other validation error!' res['message'] = str(e)
return res return res
res['success'] = True res['success'] = True
res['message'] = 'Frozen %f TRX at %s\nTransaction ID %s' % (amt, res['message'] = 'Frozen %f TRX at %s\nTransaction ID %s' % (amt,
@@ -361,7 +379,7 @@ def freezetrx(network: str, pk: str, amt: float):
# unfreeze TRX for energy (and wait for its completion) # unfreeze TRX for energy (and wait for its completion)
# input: network, private key, amount # input: network, private key, amount
# output: transaction receipt object # 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': {}} res = {'success': False, 'message': '', 'result': {}}
if amt < 1: if amt < 1:
res['message'] = 'Nothing to unfreeze!' res['message'] = 'Nothing to unfreeze!'
@@ -377,11 +395,15 @@ def unfreezetrx(network: str, pk: str, amt: float):
txn = client.trx.unfreeze_balance(owner = myaddr, txn = client.trx.unfreeze_balance(owner = myaddr,
unfreeze_balance = int(amt * TRX_SCALE), unfreeze_balance = int(amt * TRX_SCALE),
resource = "ENERGY").build().sign(rpk) resource = "ENERGY").build().sign(rpk)
# send the transaction 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: try:
receipt = txn.broadcast().wait() receipt = txn.broadcast().wait()
except ValidationError: except ValidationError as e:
res['message'] = 'Insufficient balance or other validation error!' res['message'] = str(e)
return res return res
res['success'] = True res['success'] = True
res['message'] = 'Unfrozen %f TRX at %s\nTransaction ID %s' % (amt, res['message'] = 'Unfrozen %f TRX at %s\nTransaction ID %s' % (amt,
@@ -389,10 +411,42 @@ def unfreezetrx(network: str, pk: str, amt: float):
res['result'] = receipt res['result'] = receipt
return res 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 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'] = 'Transaction sent\nTransaction ID %s' % receipt['id']
return res
if __name__ == '__main__': # main app start if __name__ == '__main__': # main app start
from argparse import ArgumentParser 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 = 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('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('-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('-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('-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('-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('-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('-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('-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('-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") trans_confirm = True # ask for transaction confirmation ("yes")
NODE_ADDR = args.tron_node # custom Tron node HTTP address NODE_ADDR = args.tron_node # custom Tron node HTTP address
TG_API_KEY = args.trongrid_api_key # optional TronGrid API key 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: if args.no_confirm == True:
trans_confirm = False trans_confirm = False
@@ -432,6 +490,9 @@ if __name__ == '__main__': # main app start
mnemo = input('Your mnemonic phrase: ') mnemo = input('Your mnemonic phrase: ')
opres = importwallet(tronnet, mnemo, passphrase, dpath) opres = importwallet(tronnet, mnemo, passphrase, dpath)
elif op == 'info': # info on own or foreign address 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 if pk == None: # public info, address must be set
opres = infobyaddr(tronnet, targetaddr) opres = infobyaddr(tronnet, targetaddr)
else: # own info, private key must be set else: # own info, private key must be set
@@ -440,17 +501,25 @@ if __name__ == '__main__': # main app start
if trans_confirm == True: if trans_confirm == True:
trconfirm('Enter "yes" to confirm sending funds: ') trconfirm('Enter "yes" to confirm sending funds: ')
if coin == 'trx': # send TRX 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 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 elif op == 'fre' or op == 'freeze': # freeze TRX for energy
if trans_confirm == True: if trans_confirm == True:
trconfirm('Enter "yes" to confirm freezing funds: ') 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 elif op == 'unf' or op == 'unfreeze': # unfreeze TRX
if trans_confirm == True: if trans_confirm == True:
trconfirm('Enter "yes" to confirm unfreezing funds: ') 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 else: # unknown operation
print('Unknown operation!') print('Unknown operation!')
print('Run the script with -h parameter to see available options') print('Run the script with -h parameter to see available options')