App-NoodlePay

 view release on metacpan or  search on metacpan

bin/noodlepay.pl  view on Meta::CPAN

#!/usr/bin/perl
# -*-cperl-*-
#
# noodlepay.pl - Convenient way to securely send Bitcoin from cold storage
# Copyright (c) Ashish Gulhati <noodlepay at hash.neo.tc>
#
# $Id: bin/noodlepay.pl v1.007 Tue Oct 16 22:06:51 PDT 2018 $

use warnings;

use Wx qw (:everything);
use Wx::Event qw (EVT_BUTTON);
use LWP::UserAgent;
use HTTP::Request;
use GD::Barcode::QRcode;
use Math::Prime::Util qw(fromdigits todigitstring);
use vars qw( $VERSION $AUTOLOAD );

my $electrum = 'electrum';
#my $electrum = 'python ~/src/Electrum-2.8.2/electrum';

# Initialize l8n
my ($lang, %lang) = initl8n();

# Initialize UI
my ($app, $frame, $topSizer, $boxSizer, $statusBar, %button) = initui();

# Actions for all the buttons
my %action;

$action{SendSign} = sub {   # Send bitcoin / Sign transaction
  if ($ARGV[0] and $ARGV[0] eq '--offline') {
    system ('v4l2-ctl --set-fmt-overlay=width=400,top=0,left=0 --overlay=1');
    open (ZBAR, "zbarcam --nodisplay --prescale=640x480 /dev/video0 |");
    my $x = <ZBAR>; chomp $x; $x =~ s/^QR-Code://;
    system ('killall -9 zbarcam');
    close ZBAR;
    system ('v4l2-ctl --overlay=0');
    my $tx = "{\n\"complete\": false,\n\"final\": true,\n\"hex\": \"$x\"\n}";
    my $txdetails = `$electrum deserialize '$tx'`;    # Check transaction details
    my $chgaddresses = `$electrum listaddresses --change`;
    my @chgaddresses = map { /\"(\S+)\"/; $1; } split /,\s*/, $chgaddresses;
    $txdetails =~ /\"inputs\": (.*)\"outputs:\"/s; my $inputs = $1;
    $txdetails =~ /\"outputs\": (.*)/s; my $outputs = $1;
    my @outputs = map { /\"address\": \"(\S+?)\".*\"value\": (\d+)/s; ($1 => $2) } split /\}\,/s, $outputs;
    my @inputs = map { /\"address\": \"(\S+?)\".*\"value\": (\d+)/s; ($1 => $2) } split /\}\,/s, $inputs;
    my $chgtotal = 0;
    for (2..$#outputs) {
      $chgtotal += $outputs[$_], next if $_ % 2; my $o = $outputs[$_];
      $err = 1, last unless grep { /^$o$/ } @chgaddresses;
    }
#    my $feeamt = $total - $chgtotal;
    unless ($err) {
      my $dialog = Wx::MessageDialog->new( $frame, "Sign transaction of $outputs[1] Satoshi to $outputs[0]?", "Confirm", wxOK|wxCANCEL);
      return if $dialog->ShowModal == wxID_CANCEL;
      my $signedtx = `$electrum signtransaction '$tx'`;
      $signedtx =~ /"hex": "(\S+)"/s;
      $dialog = qrdialog(fromdigits($1,16), 'Scan the QR code on Noodle Pi', 'Broadcast Transaction');
      $dialog->ShowModal;
    }
  }
  else {
    my $dialog = Wx::TextEntryDialog->new( $frame, "Enter amount (in Satoshi)", "Send Bitcoin", "", wxOK|wxCANCEL, [70,280] );
    system ('xvkbd -geometry 480x250+0+520 -keypad&');
    my $ret = $dialog->ShowModal;
    system ('killall -9 xvkbd');
    return if $ret == wxID_CANCEL;
    my $amount = $dialog->GetValue(); return unless $amount =~ /^\d+$/; $amount = sprintf("%f",$amount / 100000000);
    system ('v4l2-ctl --set-fmt-overlay=width=400,top=0,left=0 --overlay=1');
    open (ZBAR, "zbarcam --nodisplay --prescale=640x480 /dev/video0 |");
    my $sendto = <ZBAR>;
    system ('killall -9 zbarcam');
    close ZBAR;
    system ('v4l2-ctl --overlay=0');
    chomp $sendto; $sendto =~ s/^QR-Code://; $sendto =~ s/^bitcoin://;
    my $ProgressDialog = Wx::ProgressDialog->new("Send Money", "Creating transaction", 3, $frame,
						 wxPD_AUTO_HIDE | wxPD_APP_MODAL );
    $ProgressDialog->Update(0,"Checking balance...");
    my $balance = `$electrum getbalance`;
    if (defined $balance and $balance) {
      $balance =~ /"confirmed": "(\S+)"/s; $balance = $1 * 100000000;
      # TODO: Return error if wallet balance lower than send amount
      $ProgressDialog->Update(1,"Creating transaction...");
      my $tx = `$electrum payto $sendto $amount -f 0 -u`;
      $ProgressDialog->Update(2,"Looking up fees...");
      $tx =~ /"hex": "(\S+)"/s; my $txsize = length($1)/2 + 65;
      my $ua = new LWP::UserAgent; $ua->agent('Mozilla/5.0');
      my $req = HTTP::Request->new(GET => 'https://bitcoinfees.earn.com/api/v1/fees/recommended');
      my $res = $ua->request($req);
      my $fees = $res->content; $fees =~ s/\{\s*(.*)\s*\}/$1/; $fees =~ s/\"//g;
      my %fees = split /[:,]\s*/, $fees;
      my $fastest_fee = $fees{fastestFee} * $txsize;
      my $halfhour_fee = $fees{halfHourFee} * $txsize;
      my $hour_fee = $fees{hourFee} * $txsize;
      $ProgressDialog->Update(3);
      $dialog = Wx::TextEntryDialog->new( $frame, "Enter fee amount (in Satoshi). Recommended fees:\n\n" .
					  "- Fastest (10-20 mins): $fastest_fee\n- Within half an hour: $halfhour_fee\n" .
					  "- Within an hour: $hour_fee\n", "Send Bitcoin", $fastest_fee, wxOK|wxCANCEL, [50, 180]);
      system ('xvkbd -geometry 480x250+0+520 -keypad&');
      $ret = $dialog->ShowModal;
      system ('killall -9 xvkbd');
      return if $ret == wxID_CANCEL;
      my $feeamt = $dialog->GetValue(); return unless $feeamt =~ /^\d+$/; $feeamt = sprintf("%f",$feeamt / 100000000);
      my $signedtx;
      if ($ARGV[0] and $ARGV[0] eq '--online') {
	$ProgressDialog = Wx::ProgressDialog->new("Send Money", "Signing transaction", 1, $frame,
						  wxPD_AUTO_HIDE | wxPD_APP_MODAL );
	$ProgressDialog->Update(0,"Signing transaction...");
	$signedtx = `$electrum payto $sendto $amount -f $feeamt`;
        $signedtx =~ s/\"final\": false/\"final\": true/;
	$ProgressDialog->Update(1);
      }
      else {
	$tx = `$electrum payto $sendto $amount -f $feeamt -u`;
	$tx =~ /"hex": "(\S+)"/s;
	my $dialog = qrdialog(fromdigits($1,16), 'Scan the QR code on Noodle Air', 'Sign Transaction');
	$dialog->ShowModal;
	system ('v4l2-ctl --set-fmt-overlay=width=400,top=0,left=0 --overlay=1');
	open (ZBAR, "zbarcam --nodisplay --prescale=640x480 /dev/video0 |");
	my $signed = <ZBAR>; chomp $signed; $signed =~ s/^QR-Code://; $signed = todigitstring($signed,16);
	system ('killall -9 zbarcam');
	close ZBAR;
	system ('v4l2-ctl --overlay=0');
	$signedtx = "{\n\"complete\": true,\n\"final\": true,\n\"hex\": \"$signed\"\n}";
      }
      $dialog = Wx::MessageDialog->new( $frame, "Broadcast transaction?", "Confirm", wxOK|wxCANCEL);
      return if $dialog->ShowModal == wxID_CANCEL;
      my $id = `$electrum broadcast '$signedtx'`;
      $dialog = Wx::MessageDialog->new( $frame, "Transaction ID is $id", "Payment Sent", wxOK);
      $dialog->ShowModal;
    }
  }
  show();
};

$action{Exit} = sub {       # Exit
  $statusBar->SetStatusText("Exit", 0);
  my $dialog = Wx::MessageDialog->new( $frame, "Do you really want to exit?", "Confirm", wxOK|wxCANCEL);
  return if $dialog->ShowModal == wxID_CANCEL;
  $app->ExitMainLoop;
};

EVT_BUTTON($frame, $button{$_}, $action{$_}) for (qw(SendSign Exit));

# Start the UI

show();
$statusBar->SetStatusText(_('Noodle Pay Ready'), 0);
$frame->Show; $frame->Centre(wxBOTH); $frame->Refresh;
$app->MainLoop;

# Helper functions

sub qrdialog {
  my ($text, $msgtxt, $title) = @_;
  my $len = length($text); my ($msg, $qr, $dialog, $version);
  if ($len < 7090 ) {
    my $qrfh; my $level;
    my @version = qw( 0 41 77 127 187 255 322 370 461 552 652 772 883 1022 1101 1250 1408 1548 1725 1903 2061 2232 
		      2409 2620 2812 3057 3283 3517 3669 3909 4158 4417 4686 4965 5253 5529 5836 6153 6479 6743 7089 );
    for (1..40) { next if $len > $version[$_]; $version = $_; last; }
    my $qrxpm = GD::Barcode::QRcode->new($text, { Version => $version, Ecc => L, ModuleSize => 4 })->barcode;
    $qrxpm =~ /^(.*)$/m; my $qrsize = length($1);
    $qr = Wx::Bitmap->newFromXPM([ split /\n/, "$qrsize $qrsize 2 1\n0 c #FFFFFF\n1 c #000000\n$qrxpm" ]);
  }
  if (defined $qr) {
    my $fontheight = $frame->GetCharHeight;
    $msg = "\n" x ($qr->GetHeight / $fontheight) . "\n\n$msgtxt";
    $dialog = Wx::TextEntryDialog->new($frame, $msg , $title, $text);
    my $dialog_height = $dialog->GetSize->GetHeight;
    my $dialogwidth = $dialog->GetSize->GetWidth;
    my $qrheight = $qr->GetHeight; my $width = $qrheight + 20 > $dialogwidth ? $qrheight + 20 : $dialogwidth;
    my $adjust = 10 unless $^O eq 'MSWin32' or $^O eq 'darwin';
    my $qrpos = ($width-$qrheight)/2 + ($width == 300 ? $adjust : 0);
    $dialog->SetSize([$width,$dialog_height]);
    $dialog->SetMinSize([$width,$dialog_height]);
    $dialog->SetMaxSize([$width,$dialog_height]);
      Wx::StaticBitmap->new( $dialog, -1, $qr, [$qrpos,10] );
  }
  return $dialog;
}

sub show {                  # Update the main frame



( run in 2.537 seconds using v1.01-cache-2.11-cpan-7fcb06a456a )