#!/usr/bin/perl

package FCC::transaction;

#######################################
#                                     #
#     FCC Transaction                 #
#                                     #
#    (C) 2017 Domero                  #
#                                     #
#######################################

use strict;
use warnings;
use Exporter;
use vars qw($VERSION @ISA @EXPORT @EXPORT_OK);

$VERSION     = '1.01';
@ISA         = qw(Exporter);
@EXPORT      = qw(maketransaction puttransaction finalizetransaction validatetransaction writetransaction
                  maketransid calcfee transactionfee);
@EXPORT_OK   = qw();

use gfio 1.08;
use Digest::SHA qw(sha256_hex sha512_hex);
use Crypt::Ed25519;
use POSIX;
use gerr;
use JSON qw(decode_json encode_json);
use FCC::global;
use FCC::fcc;
use FCC::wallet 1.02;

1;

###################################################################################################################################

# Ledger-blocks: (all big-endian)

# offset  length  content
#      0       4  difference of position of the previous transaction in the chain compared to this position (equal to length previous block)
#      4       4  difference of position of the next transaction in the chain compared to this position (equal to length block)
#      8      64  transaction id (next fields from 'number' on, secure-hashed (sha256(sha512(data)))
#     72      64  cumulative hash of all blocks
#    136      12  transaction number in chain (0=genesis) 48 bit
#    148       4  version
#    152       1  type
#    153      64  previous id (any transaction type)

# Genesis / In / Coinbase

#    217       8  time, epoch is 00:00:00 UTC, January 1, 1970 (UNIX)
#    225       2  number of out addresses ( = 1 for genesis/coinbase)

# Coinbase

#    227       8  count
#    235     128  signature of FCC-Server 

# In

#    227       2  number of in addresses
#    229     192  signature (public key (64) followed by signature (128))
#    421   64num  list of id's of collected out-transactions to form in-addresses

# =================================================================================================================================

# Out

#    217      68  FCC-wallet address
#    285      16  amount in 64 bit (100000000 = 1 FCC)
#    301       4  fee = 0 for coinbase and genesis, minimum = 1 = 0,01% maximum = 10000 (100%)

# All

# 227/Xin/305/363  1  block identifier 'z'

# CAREFUL: Ledger will end with a pointer to the last block, which will be the beginning of the next block,
#          thus forming a double linked list to search from the beginning or the behind!

###################################################################################################################################

sub maketransid {
  my ($transaction) = @_;
  my $data="";
  foreach my $k (sort keys %{$transaction->{inblock}}) {
    if ($k eq 'inblocks') {
      foreach my $ih (@{$transaction->{inblock}{$k}}) {
        $data.=$ih
      }
    } else {
      $data.=$transaction->{inblock}{$k}
    }
  }
  foreach my $b (@{$transaction->{outblocks}}) {
    foreach my $k (sort keys %$b) {
      $data.=$b->{$k}
    }
  }
  return securehash($data)
}

sub writetransaction {
  my ($transaction) = @_;
  ($transaction->{blocks},$transaction->{tid})=encodetransaction($transaction->{inblock},$transaction->{outblocks});
  addtoledger($transaction->{blocks})
  return $transaction->{tid}
}


# =================================================================================================================================

sub maketransaction {  
  my ($wallet,$changewallet) = @_;
  # changewallet is optional
  if (ref($wallet) ne 'FCC::wallet') { error "FCC::transaction::maketransaction - Wallet-structure expected (use FCC::wallet)" }
  if (defined $changewallet && (ref($changewallet) ne 'FCC::wallet')) {
    error "FCC::transaction::maketransaction - Change-Wallet needs a wallet structure (use FCC::Wallet)"
  }
  if (!validatehash($wallet->{wallet},$wallet->{pubkey})) {
    error "FCC::transaction::maketransaction - Wallet-address and public key does not match"
  }
  if (!defined $changewallet) {
    $changewallet=$wallet
  } elsif (!validatehash($changewallet->{wallet},$changewallet->{pubkey})) {
    error "FCC::transaction::maketransaction - Change-Wallet-address and public key does not match"
  }
  my $donate = 0;
  my $transaction = {
    totamount => 0,
    totfee => 0,
    wallet => $wallet,
    changewallet => $changewallet->{wallet},
    valid => 1,
    error => '',
    inblocks => [],
    outblocks => []
  };
  bless($transaction); return $transaction
}

sub puttransaction {
  my ($transaction,$address,$amount,$fee) = @_;
  if (!$transaction->{valid}) { return $transaction }
  if ($#{$transaction->{outblocks}} >= 255) {
    $transaction->{valid}=0; $transaction->{error}="Too many recipients (max=255) for transaction from $address"; return $transaction
  }
  if (!validwallet($address)) {
    $transaction->{valid}=0; $transaction->{error}="Invalid FCC-address given ($address)"; return $transaction
  }
  if ($amount<100) {
    $transaction->{valid}=0; $transaction->{error}="Amount must be >= 100 ($address)"; return $transaction
  }
  # if fee is given in percentages
  $fee =~ s/\,/\./; if ($fee =~ /\./) { $fee=int $fee*10000 } $fee =~ s/[ \%]//g;
  if (($fee<0) || ($fee>10000)) {
    $transaction->{valid}=0; $transaction->{error}="Fee must be between 1..10000 [0.01% - 100%] ($address)"; return $transaction
  }
  if ($amount<10000) {
    $transaction->{valid}=0; $transaction->{error}="Amount must be >= 10000 ($address)"; return $transaction
  }
  $transaction->{totamount}+=$amount;
  $transaction->{totfee}+=$amount*($fee/10000);
  push @{$transaction->{outblocks}},{
    type => 'out',
    addr => $address,
    amount => $amount,
    fee => $fee
  };
  return $transaction
}

sub finalizetransaction {
  my ($transaction,$fcctime) = @_;
  if (!$transaction->{valid}) { return $transaction }
  if (!$transaction->{totamount}) {
    $transaction->{valid}=0; $transaction->{error}="The transaction is empty, use puttransaction"; return $transaction
  }
  my $wanttospend=$transaction->{totamount} + $transaction->{totfee};
  my $saldo=saldo($transaction->{wallet}{wallet});
  if ($saldo<$wanttospend) {
    $transaction->{valid}=0; $transaction->{error}="Finalizetransaction: Insufficient funds ($saldo < $wanttospend)"; return $transaction
  }
  $transaction->{balance}=$saldo;
  if (!$fcctime) { $fcctime = time + $FCCTIME }
  # looks good :) build a valid transaction
  my $spendblocks=collectspendblocks($transaction->{wallet}{wallet},$wanttospend);
  my $ct=0; my $tosign=''; $transaction->{collected}=0;
  foreach my $pos (@$spendblocks) {
    my $outblock=readblock($pos);
    $transaction->{collected}+=$outblock->{amount};
    push @{$transaction->{inblocks}},$outblock->{tid};
    $tosign.=$outblock->{tid}
  }
  my $change=$transaction->{collected}-$wanttospend;
  if ($change>0) {
    push @{$transaction->{outblocks}},{
      type => 'out',
      addr => $transaction->{changewallet},
      amount => $change,
      fee => 0
    }
  }
  # finish to be signed data (inblock-addresses followed by all outblocks info: wallet, hexamount, hexfee)
  foreach my $ob (@{$transaction->{outblocks}}) {
    $tosign.=$ob->{addr}.dechex($ob->{amount},16).dechex($ob->{fee},4)
  }
  $transaction->{inblock} = {
    type => 'in',
    time => $fcctime,
    pubkey => $transaction->{wallet}{pubkey},
    signature => octhex(Crypt::Ed25519::sign($tosign,hexoct($transaction->{wallet}{pubkey}),hexoct($transaction->{wallet}{privkey}))),
    inblocks => $transaction->{inblocks}
  };
  $transaction->{wallet}=$transaction->{wallet}{wallet};
  return $transaction
}

# =================================================================================================================================

sub validatetransaction {
  # HACK THIS ! :P I wish you luck as a genuine moral sane gray-hat ;) Let's play chess on the real world money-board ;)
  my ($transaction) = @_;
  if (!$transaction->{valid}) { return $transaction }
  if (!$transaction->{totamount}) { 
    $transaction->{valid}=0; $transaction->{error}="Empty transaction, use puttransaction"; return $transaction
  }
  if ($#{$transaction->{outblocks}}<0) {
    $transaction->{valid}=0; $transaction->{error}="No outblocks given in transaction, finalize first!"; return $transaction    
  }
  if (!$transaction->{inblock}) {
    $transaction->{valid}=0; $transaction->{error}="No inblock given in transaction, finalize first!"; return $transaction    
  }
  my $wallet=createwalletaddress($transaction->{inblock}{pubkey});
  if (!validwallet($wallet)) {
    $transaction->{valid}=0; $transaction->{error}="Public key results in an invalid wallet-address"; return $transaction    
  }
  if ($wallet ne $transaction->{wallet}) {
    $transaction->{valid}=0; $transaction->{error}="Public key does not match given wallet-address"; return $transaction    
  }
  my $wanttospend=$transaction->{totamount} + $transaction->{totfee};
  my $checkspend=0;
  for (my $obn=0;$obn<$#{$transaction->{outblocks}};$obn++) {
    my $ob=$transaction->{outblocks}[$obn];
    if (!validwallet($ob->{addr})) {
      $transaction->{valid}=0; $transaction->{error}="Illegal wallet-address found in outblock $obn"; return $transaction
    }
    if ($ob->{amount}<10000) {
      $transaction->{valid}=0; $transaction->{error}="Illegal amount ($ob->{amount} < 10000) found in outblock $obn"; return $transaction
    }
    if (($ob->{fee}<0) || ($ob->{fee}>10000)) {
      $transaction->{valid}=0; $transaction->{error}="Illegal fee ($ob->{fee}) found in outblock $obn"; return $transaction
    }
    $checkspend+=$ob->{amount} + $ob->{amount} * ($ob->{fee} / 10000)
  }
  if ($checkspend != $wanttospend) {
    $transaction->{valid}=0; $transaction->{error}="The amount spent in the outblocks does not match the given amount to spend"; return $transaction
  }
  my $ob=$transaction->{outblocks}[$#{$transaction->{outblocks}}];
  if ($ob->{fee} != 0) {
    $transaction->{valid}=0; $transaction->{error}="Illegal fee ($ob->{fee}) found in change-block"; return $transaction
  }
  my $saldo=saldo($transaction->{wallet});
  if ($saldo<$wanttospend) {
    $transaction->{valid}=0; $transaction->{error}="Insufficient funds ($saldo < $wanttospend)"; return $transaction
  }
  my $spendblocks=collectspendblocks($transaction->{wallet},$wanttospend);
  my $collected=0; my $ct=0; my $tosign='';
  foreach my $pos (@$spendblocks) {
    my $outblock=readblock($pos);
    # I need a dollar, a dollar is all I need
    $collected+=$outblock->{amount};
    if ($outblock->{tid} ne $transaction->{inblock}{inblocks}[$ct]) {
      $transaction->{valid}=0; $transaction->{error}="Inblock $ct does not match the given transaction h4x0rz!"; return $transaction
    }
    $tosign.=$outblock->{tid}; $ct++
  }
  if ($collected != $transaction->{collected}) {
    $transaction->{valid}=0; $transaction->{error}="The amount of collected blocks to spend does not match the given transaction h4x0rz!"; return $transaction
  }
  if ($ob->{amount} + $wanttospend != $collected) {
    $transaction->{valid}=0; $transaction->{error}="The amount of change is incorrect h4x0rz!"; return $transaction
  }
  foreach my $ob (@{$transaction->{outblocks}}) {
    $tosign.=$ob->{addr}.dechex($ob->{amount},16).dechex($ob->{fee},4)
  }
  if (!Crypt::Ed25519::verify($tosign,hexoct($transaction->{inblock}{pubkey}),hexoct($transaction->{inblock}{signature}))) {
    $transaction->{valid}=0; $transaction->{error}="Ed25519 Signature failed! This is not a valid transaction h4x0rz!"; return $transaction
  }
  return $transaction
}

# =================================================================================================================================

sub calcfee {
  my ($amount,$fee) = @_;
  my $itr=int $amount*($fee/10000);
  if ($itr<1) { $itr=1 }
  return $itr
}

sub transactionfee {
  my ($transaction) = @_;
  my $fee=0;
  foreach my $ob (@{$transaction->{outblocks}}) {
    $fee+=calcfee($ob->{amount},$ob->{fee});
  }
  return $fee
}

# EOF FCC::transaction by Chaosje
