Skip to content

Commit

Permalink
Implement offline transaction signing in CLI. (#337)
Browse files Browse the repository at this point in the history
  • Loading branch information
christian-oudard authored May 31, 2022
1 parent c0ff4c1 commit 017f44c
Show file tree
Hide file tree
Showing 2 changed files with 63 additions and 23 deletions.
57 changes: 42 additions & 15 deletions cli/mobilecoin/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,9 @@ def import_(self, backup, name=None, block=None, key_derivation_version=2):
with open(backup) as f:
data = json.load(f)

if data['object'] == 'account_secrets':
if data.get('method') == 'import_view_only_account':
account = self.client.import_view_only_account(data['params'])
else:
params = {}
for field in [
'mnemonic', # Key derivation version 2+.
Expand All @@ -353,12 +355,11 @@ def import_(self, backup, name=None, block=None, key_derivation_version=2):
'fog_report_id',
'fog_authority_spki',
]:
params['fog_keys'][field] = data['account_key'][field]
value = data['account_key'].get(field)
if value is not None:
params['fog_keys'][field] = value
account = self.client.import_account(**params)

elif data['object'] == 'view_only_account_import_package':
account = self.client.import_view_only_account(data)

else:
# Try to use the legacy import system, treating the string as hexadecimal root entropy.
root_entropy = None
Expand Down Expand Up @@ -419,7 +420,7 @@ def export(self, account_id, show=False, view=False):
print('{:<2} {}'.format(i, word))
print()
else:
filename = 'mobilecoin_seed_mnemonic_{}.json'.format(account_id[:6])
filename = 'mobilecoin_secret_mnemonic_{}.json'.format(account_id[:6])
try:
_save_export(account, secrets, filename)
except OSError as e:
Expand Down Expand Up @@ -549,7 +550,12 @@ def block_key(t):
def send(self, account_id, amount, to_address, build_only=False, fee=None):
account = self._load_account_prefix(account_id)
account_id = account['account_id']
balance = self.client.get_balance_for_account(account_id)

view_only = (account['object'] == 'view_only_account')
if view_only:
balance = self.client.get_balance_for_view_only_account(account_id)
else:
balance = self.client.get_balance_for_account(account_id)
unspent = pmob2mob(balance['unspent_pmob'])

network_status = self.client.get_network_status()
Expand All @@ -570,20 +576,22 @@ def send(self, account_id, amount, to_address, build_only=False, fee=None):
amount = Decimal(amount)
total_amount = amount + fee

if build_only:
if view_only:
verb = 'Building unsigned transaction for'
elif build_only:
verb = 'Building transaction for'
else:
verb = 'Sending'

print('\n'.join([
'{} {} from account {} {}',
'{} {}',
'from account {}',
'to address {}',
'Fee is {}, for a total amount of {}.',
]).format(
verb,
_format_mob(amount),
account_id[:6],
account['name'],
_format_account_header(account),
to_address,
_format_mob(fee),
_format_mob(total_amount),
Expand All @@ -596,6 +604,19 @@ def send(self, account_id, amount, to_address, build_only=False, fee=None):
]).format(_format_mob(unspent)))
return

if view_only:
response = self.client.build_unsigned_transaction(account_id, amount, to_address, fee=fee)
path = Path('unsigned_tx_proposal_{}_{}.json'.format(
account_id[:6],
balance['local_block_height'],
))
if path.exists():
print(f'The file {path} already exists. Please rename the existing file and retry.')
else:
_save_json_file(path, response)
print(f'Wrote {path}.')
return

if build_only:
tx_proposal = self.client.build_transaction(account_id, amount, to_address, fee=fee)
path = Path('tx_proposal.json')
Expand Down Expand Up @@ -631,6 +652,10 @@ def submit(self, proposal, account_id=None, receipt=False):
with Path(proposal).open() as f:
tx_proposal = json.load(f)

# Check whether this is an already built response from the offline transaction signer.
if tx_proposal.get('method') == 'submit_transaction':
tx_proposal = tx_proposal['params']['tx_proposal']

# Check that the tombstone block is within range.
tombstone_block = int(tx_proposal['tx']['prefix']['tombstone_block'])
network_status = self.client.get_network_status()
Expand Down Expand Up @@ -843,6 +868,7 @@ def sync(self, account_id_or_sync_response):
self._finish_sync(sync_response)
else:
account_id = account_id_or_sync_response
self._start_sync(account_id)

def _start_sync(self, account_id):
account = self._load_account_prefix(account_id)
Expand All @@ -860,11 +886,12 @@ def _start_sync(self, account_id):

def _finish_sync(self, sync_response):
with open(sync_response) as f:
data = json.load(f)['params']
data = json.load(f)

r = self.client.sync_view_only_account(**data)
account = self.client.get_view_only_account(data['account_id'])
balance = self.client.get_balance_for_view_only_account(data['account_id'])
r = self.client.sync_view_only_account(data['params'])
account_id = data['params']['account_id']
account = self.client.get_view_only_account(account_id)
balance = self.client.get_balance_for_view_only_account(account_id)

print()
print('Synced {} transaction outputs.'.format(len(data['completed_txos'])))
Expand Down
29 changes: 21 additions & 8 deletions cli/mobilecoin/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,10 @@ def import_account_from_legacy_root_entropy(self, legacy_root_entropy, name=None
})
return r['account']

def import_view_only_account(self, package):
def import_view_only_account(self, params):
r = self._req({
"method": "import_view_only_account",
"params": {"package": package},
"params": params,
})
return r['view_only_account']

Expand Down Expand Up @@ -309,6 +309,23 @@ def build_transaction(self, account_id, amount, to_address, tombstone_block=None
})
return r['tx_proposal']

def build_unsigned_transaction(self, account_id, amount, to_address, tombstone_block=None, fee=None):
amount = str(mob2pmob(amount))
params = {
"account_id": account_id,
"recipient_public_address": to_address,
"value_pmob": amount,
}
if tombstone_block is not None:
params['tombstone_block'] = str(int(tombstone_block))
if fee is not None:
params['fee'] = str(mob2pmob(fee))
r = self._req({
"method": "build_unsigned_transaction",
"params": params,
})
return r

def submit_transaction(self, tx_proposal, account_id=None):
r = self._req({
"method": "submit_transaction",
Expand Down Expand Up @@ -424,14 +441,10 @@ def create_view_only_account_sync_request(self, account_id):
})
return r

def sync_view_only_account(self, account_id, completed_txos, subaddresses):
def sync_view_only_account(self, params):
r = self._req({
"method": "sync_view_only_account",
"params": {
"account_id": account_id,
"completed_txos": completed_txos,
"subaddresses": subaddresses,
},
"params": params,
})
return r

Expand Down

0 comments on commit 017f44c

Please sign in to comment.