diff --git a/cli/mobilecoin/cli.py b/cli/mobilecoin/cli.py index f0621c750..18c1aa922 100644 --- a/cli/mobilecoin/cli.py +++ b/cli/mobilecoin/cli.py @@ -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+. @@ -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 @@ -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: @@ -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() @@ -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), @@ -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') @@ -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() @@ -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) @@ -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']))) diff --git a/cli/mobilecoin/client.py b/cli/mobilecoin/client.py index d36f70cb7..4e8a85fbe 100755 --- a/cli/mobilecoin/client.py +++ b/cli/mobilecoin/client.py @@ -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'] @@ -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", @@ -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