def _setup_transaction_detail_view(vc : ObjCInstance) -> None: entry = utils.nspy_get_byname(vc, 'tx_entry') tx, tx_hash, status_str, label, v_str, balance_str, date, ts, conf, status, value, fiat_amount, fiat_balance, fiat_amount_str, fiat_balance_str, ccy, img, *dummy2 = entry parent = gui.ElectrumGui.gui wallet = parent.wallet base_unit = parent.base_unit() format_amount = parent.format_amount if not wallet: utils.NSLog("TxDetail: Wallet not open.. aborting early (tx_hash=%s)",tx_hash) return if tx is None: tx = wallet.transactions.get(tx_hash, None) if tx is not None and tx.raw: tx = Transaction(tx.raw, sign_schnorr=parent.prefs_use_schnorr) tx.deserialize() if tx is None: utils.NSLog("*** ERROR: Cannot find tx for hash: %s",tx_hash) return tx_hash, status_, label_, can_broadcast, amount, fee, height, conf, timestamp, exp_n = wallet.get_tx_info(tx) size = tx.estimated_size() can_sign = not tx.is_complete() and wallet and wallet.can_sign(tx) #and (wallet.can_sign(tx) # or bool(self.main_window.tx_external_keypairs)) wasNew = False if not vc.viewIfLoaded: NSBundle.mainBundle.loadNibNamed_owner_options_("TxDetail",vc,None) wasNew = True if vc.maxTVHeight < 1.0: vc.maxTVHeight = vc.inputsTVHeightCS.constant # grab all the views # Transaction ID: txTit = vc.txTit txHash = vc.txHash copyBut = vc.cpyBut qrBut = vc.qrBut # Description: descTit = vc.descTit descTf = vc.descTf # Status: statusTit = vc.statusTit statusIV = vc.statusIV statusLbl = vc.statusLbl # Date: dateTit = vc.dateTit dateLbl = vc.dateLbl # Amount received/sent: amtTit = vc.amtTit amtLbl = vc.amtLbl # Size: sizeTit = vc.sizeTit sizeLbl = vc.sizeLbl # Fee: feeTit = vc.feeTit feeLbl = vc.feeLbl # Locktime: lockTit = vc.lockTit lockLbl = vc.lockLbl # ⓢ Schnorr Signed label schnorrLbl = vc.schnorrLbl # Inputs inputsTV = vc.inputsTV # Outputs outputsTV = vc.outputsTV # Setup data for all the stuff txTit.text = _("Transaction ID:").translate({ord(':') : None}) tx_hash_str = tx_hash if tx_hash is not None and tx_hash != "None" and tx_hash != "Unknown" and tx_hash != _("Unknown") else _('Unknown') rbbs = [] vc.bottomView.setHidden_(True) vc.bottomBut.handleControlEvent_withBlock_(UIControlEventPrimaryActionTriggered, None) # clear previous events if can_sign: vc.noBlkXplo = True vc.bottomView.setHidden_(False) def fun() -> None: vc.onSign() vc.bottomBut.handleControlEvent_withBlock_(UIControlEventPrimaryActionTriggered, fun) vc.bottomBut.setTitle_forState_(_('Sign'), UIControlStateNormal) if not img: img = StatusImages[-1] if can_broadcast: vc.noBlkXplo = True vc.bottomView.setHidden_(False) def fun() -> None: vc.onBroadcast() vc.bottomBut.handleControlEvent_withBlock_(UIControlEventPrimaryActionTriggered, None) # clear previous events vc.bottomBut.handleControlEvent_withBlock_(UIControlEventPrimaryActionTriggered, fun) vc.bottomBut.setTitle_forState_(_('Broadcast'), UIControlStateNormal) if not img: img = StatusImages[-2] if tx_hash_str == _("Unknown") or tx_hash is None: #unsigned tx copyBut.setHidden_(True) qrBut.setHidden_(True) txHash.setHidden_(True) txHash.userInteractionEnabled = False vc.noTxHashView.setHidden_(False) vc.noTxHashLbl.text = _("You need to sign this transaction in order for it to get a transaction ID.") if can_sign else _("This transaction is not signed and thus lacks a transaction ID.") vc.notsigned = True rbbs.append(UIBarButtonItem.alloc().initWithImage_style_target_action_(UIImage.imageNamed_("barbut_actions"), UIBarButtonItemStyleBordered, vc, SEL(b'onShareSave:')).autorelease()) else: copyBut.setHidden_(False) qrBut.setHidden_(False) txHash.setHidden_(False) vc.noTxHashView.setHidden_(True) vc.notsigned = False txHash.linkText = tx_hash_str txHash.userInteractionEnabled = True def onTxLinkTap(ll : objc_id) -> None: vc.onTxLink_(ObjCInstance(ll).gr) txHash.linkTarget = Block(onTxLinkTap) rbbs.append(UIBarButtonItem.alloc().initWithImage_style_target_action_(UIImage.imageNamed_("barbut_actions"), UIBarButtonItemStyleBordered, vc, SEL(b'onTxLink:')).autorelease()) if amount is None: # unrelated to this wallet.. hide the description textfield.. also affects messaging below.. see viewDidLayoutSubviews vc.unrelated = True else: vc.unrelated = False vc.navigationItem.rightBarButtonItems = rbbs descTit.text = _("Description") descTf.text = label descTf.placeholder = _("Tap to add a description") descTf.clearButtonMode = UITextFieldViewModeWhileEditing utils.uitf_redo_attrs(descTf) statusTit.setText_withKerning_(_("Status:").translate({ord(':') : None}), utils._kern) if not img: #try and auto-determine the appropriate image if it has some confirmations and img is still null try: c = min(int(conf), 6) if c >= 0: img = StatusImages[c+3] except: pass if not img: img = UIImage.imageNamed_("empty.png") ff = str(status_) #status_str vc.canRefresh = False try: if int(conf) > 0: ff = "%s %s"%(str(conf), _('confirmations')) vc.canRefresh = conf >= 0 # if we got here means refresh has meaning.. it's not an external tx or if it is, it now is on the network, so enable refreshing except: pass statusLbl.text = _(ff) if vc.canRefresh and conf >= 1: img = StatusImages[min(len(StatusImages)-1,3+min(6,conf))] statusIV.image = img if timestamp or exp_n: if timestamp: dateTit.setText_withKerning_(_("Date"), utils._kern) #dateLbl.text = str(date) dateLbl.attributedText = utils.makeFancyDateAttrString(str(date)) elif exp_n: dateTit.setText_withKerning_(_("Expected conf."), utils._kern) dateLbl.text = '%d blocks'%(exp_n) if exp_n > 0 else _('unknown (low fee)') vc.noBlkXplo = False dateTit.alpha = 1.0 dateLbl.alpha = 1.0 else: # wtf? what to do here? dateTit.setText_withKerning_(_("Date"), utils._kern) dateLbl.text = "" dateTit.alpha = 0.5 dateLbl.alpha = 0.5 myAmtStr = '' if vc.unrelated: amtTit.setText_withKerning_(_("Amount"), utils._kern) amtLbl.text = _("Transaction unrelated to your wallet") elif amount > 0: amtTit.setText_withKerning_(_("Amount received:").translate({ord(':') : None}), utils._kern) myAmtStr = ('%s %s%s'%(format_amount(amount),base_unit, (" " + fiat_amount_str + " " + ccy + "") if fiat_amount_str else '', )) else: amtTit.setText_withKerning_( _("Amount sent:").translate({ord(':') : None}), utils._kern ) myAmtStr = ('%s %s%s'%(format_amount(-amount),base_unit, (" " + fiat_amount_str.replace('-','') + " " + ccy + "") if fiat_amount_str else '', )) if myAmtStr: l = myAmtStr.split() am = l[0] unt = ' ' + l[1] if len(l) else '' rest = ' ' + ' '.join(l[2:]) if len(l) > 2 else '' ats = NSMutableAttributedString.alloc().initWithString_attributes_(am, {NSFontAttributeName : UIFont.systemFontOfSize_weight_(16.0, UIFontWeightBold)}).autorelease() if unt: ats.appendAttributedString_(NSAttributedString.alloc().initWithString_attributes_(unt, {NSFontAttributeName : UIFont.systemFontOfSize_weight_(16.0, UIFontWeightBold)}).autorelease()) if rest: ats.appendAttributedString_(NSAttributedString.alloc().initWithString_attributes_(rest, {NSFontAttributeName : UIFont.systemFontOfSize_weight_(14.0, UIFontWeightRegular)}).autorelease()) amtLbl.attributedText = ats sizeTit.setText_withKerning_( _("Size:").translate({ord(':') : None}), utils._kern ) if size: sizeLbl.text = ('%d bytes' % (size)) else: sizeLbl.text = _("Unknown") feeTit.setText_withKerning_( _("Fee"), utils._kern ) fee_str = '%s' % (format_amount(fee) + ' ' + base_unit if fee is not None else _('unknown')) if fee is not None: fee_str += ' ( %s ) '% parent.format_fee_rate(fee/size*1000) feeLbl.text = fee_str lockTit.setText_withKerning_(_("Locktime"), utils._kern) if tx.locktime > 0: lockLbl.text = str(tx.locktime) lockTit.setHidden_(False) lockLbl.setHidden_(False) else: lockTit.setHidden_(True) lockLbl.setHidden_(True) n_inp, n_outp = len(tx.inputs()), len(tx.outputs()) # auto-adjust height of table views vc.inputsTVHeightCS.constant = min(_TxInputsOutputsHeaderHeight + _TxInputsOutputsCellHeight*n_inp, vc.maxTVHeight) vc.outputsTVHeightCS.constant = min(_TxInputsOutputsHeaderHeight + _TxInputsOutputsCellHeight*n_outp, vc.maxTVHeight) # refreshes the tableview with data if wasNew: if ts is None: ts = time.time() tvc = CreateTxInputsOutputsTVC(vc, tx, inputsTV, outputsTV, float(ts)) else: inputsTV.reloadData() outputsTV.reloadData() if any(tx.is_schnorr_signed(i) for i in range(n_inp)): schnorrLbl.text = SCHNORR_SIGIL + " " + _('Schnorr Signed') schnorrLbl.setHidden_(False) else: schnorrLbl.setHidden_(True)
def sign_transaction(self, tx: Transaction, password, *, use_cache=False): self.print_error('sign_transaction(): tx: ' + str(tx)) #debugSatochip client = self.get_client() # outputs txOutputs = ''.join(tx.serialize_output(o) for o in tx.outputs()) hashOutputs = bh2u(Hash(bfh(txOutputs))) txOutputs = var_int(len(tx.outputs())) + txOutputs self.print_error('sign_transaction(): hashOutputs= ', hashOutputs) #debugSatochip self.print_error('sign_transaction(): outputs= ', txOutputs) #debugSatochip # Fetch inputs of the transaction to sign derivations = self.get_tx_derivations(tx) for i, txin in enumerate(tx.inputs()): self.print_error('sign_transaction(): input =', i) #debugSatochip self.print_error('sign_transaction(): input[type]:', txin['type']) #debugSatochip if txin['type'] == 'coinbase': self.give_error( "Coinbase not supported") # should never happen if txin['type'] in ['p2sh']: p2shTransaction = True pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin) for j, x_pubkey in enumerate(x_pubkeys): self.print_error('sign_transaction(): forforloop: j=', j) #debugSatochip if tx.is_txin_complete(txin): break if x_pubkey in derivations: signingPos = j s = derivations.get(x_pubkey) address_path = "%s/%d/%d" % (self.get_derivation()[2:], s[0], s[1]) # get corresponing extended key (depth, bytepath) = bip32path2bytes(address_path) (key, chaincode ) = client.cc.card_bip32_get_extendedkey(bytepath) # parse tx pre_tx_hex = tx.serialize_preimage(i) pre_tx = bytes.fromhex( pre_tx_hex) # hex representation => converted to bytes pre_hash = Hash(bfh(pre_tx_hex)) pre_hash_hex = pre_hash.hex() self.print_error('sign_transaction(): pre_tx_hex=', pre_tx_hex) #debugSatochip self.print_error('sign_transaction(): pre_hash=', pre_hash_hex) #debugSatochip #(response, sw1, sw2) = client.cc.card_parse_transaction(pre_tx, True) # use 'True' since BCH use BIP143 as in Segwit... #print_error('[satochip] sign_transaction(): response= '+str(response)) #debugSatochip #(tx_hash, needs_2fa) = client.parser.parse_parse_transaction(response) (response, sw1, sw2, tx_hash, needs_2fa) = client.cc.card_parse_transaction( pre_tx, True ) # use 'True' since BCH use BIP143 as in Segwit... tx_hash_hex = bytearray(tx_hash).hex() if pre_hash_hex != tx_hash_hex: raise RuntimeError( f"[Satochip_KeyStore] Tx preimage mismatch: {pre_hash_hex} vs {tx_hash_hex}" ) # sign tx keynbr = 0xFF #for extended key if needs_2fa: # format & encrypt msg import json coin_type = 145 #see https://github.com/satoshilabs/slips/blob/master/slip-0044.md test_net = networks.net.TESTNET msg = { 'action': "sign_tx", 'tx': pre_tx_hex, 'ct': coin_type, 'sw': True, 'tn': test_net, 'txo': txOutputs, 'ty': txin['type'] } msg = json.dumps(msg) (id_2FA, msg_out) = client.cc.card_crypt_transaction_2FA( msg, True) d = {} d['msg_encrypt'] = msg_out d['id_2FA'] = id_2FA # self.print_error("encrypted message: "+msg_out) self.print_error("id_2FA:", id_2FA) #do challenge-response with 2FA device... client.handler.show_message( '2FA request sent! Approve or reject request on your second device.' ) Satochip2FA.do_challenge_response(d) # decrypt and parse reply to extract challenge response try: reply_encrypt = None # init it in case of exc below reply_encrypt = d['reply_encrypt'] except Exception as e: # Note: give_error here will raise again.. :/ self.give_error( "No response received from 2FA.\nPlease ensure that the Satochip-2FA plugin is enabled in Tools>Optional Features", True) break if reply_encrypt is None: #todo: abort tx break reply_decrypt = client.cc.card_crypt_transaction_2FA( reply_encrypt, False) self.print_error("challenge:response=", reply_decrypt) reply_decrypt = reply_decrypt.split(":") rep_pre_hash_hex = reply_decrypt[0][0:64] if rep_pre_hash_hex != pre_hash_hex: #todo: abort tx or retry? self.print_error("Abort transaction: tx mismatch:", rep_pre_hash_hex, "!=", pre_hash_hex) break chalresponse = reply_decrypt[1] if chalresponse == "00" * 20: #todo: abort tx? self.print_error( "Abort transaction: rejected by 2FA!") break chalresponse = list(bytes.fromhex(chalresponse)) else: chalresponse = None (tx_sig, sw1, sw2) = client.cc.card_sign_transaction( keynbr, tx_hash, chalresponse) #self.print_error('sign_transaction(): sig=', bytes(tx_sig).hex()) #debugSatochip #todo: check sw1sw2 for error (0x9c0b if wrong challenge-response) # enforce low-S signature (BIP 62) tx_sig = bytearray(tx_sig) r, s = get_r_and_s_from_der_sig(tx_sig) if s > CURVE_ORDER // 2: s = CURVE_ORDER - s tx_sig = der_sig_from_r_and_s(r, s) #update tx with signature tx_sig = tx_sig.hex() + '41' #tx.add_signature_to_txin(i,j,tx_sig) txin['signatures'][j] = tx_sig break else: self.give_error("No matching x_key for sign_transaction" ) # should never happen self.print_error("is_complete", tx.is_complete()) tx.raw = tx.serialize() return