Offline mode implemented
This commit is contained in:
@@ -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
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user