Package com.bitsofproof.supernode.wallet

Source Code of com.bitsofproof.supernode.wallet.BaseAccountManager$TransactionSink

/*
* Copyright 2013 bits of proof zrt.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*    http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.bitsofproof.supernode.wallet;

import java.io.PrintStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.bitsofproof.supernode.api.Address;
import com.bitsofproof.supernode.api.Transaction;
import com.bitsofproof.supernode.api.TransactionInput;
import com.bitsofproof.supernode.api.TransactionOutput;
import com.bitsofproof.supernode.common.ByteUtils;
import com.bitsofproof.supernode.common.ECKeyPair;
import com.bitsofproof.supernode.common.Key;
import com.bitsofproof.supernode.common.ScriptFormat;
import com.bitsofproof.supernode.common.ValidationException;
import com.bitsofproof.supernode.common.WireFormat;

public abstract class BaseAccountManager implements AccountManager
{
  private static final Logger log = LoggerFactory.getLogger (BaseAccountManager.class);

  private static final long MINIMUM_FEE = 10000;
  private static final long MAXIMUM_FEE = 1000000;

  private UTXO confirmed = createConfirmedUTXO ();
  private UTXO change = createChangeUTXO ();
  private UTXO receiving = createReceivingUTXO ();
  private UTXO sending = createSendingUTXO ();

  private long created;

  @Override
  public long getCreated ()
  {
    return created;
  }

  public void setCreated (long created)
  {
    this.created = created;
  }

  private final List<AccountListener> accountListener = Collections.synchronizedList (new ArrayList<AccountListener> ());
  private final Map<String, Transaction> transactions = new HashMap<String, Transaction> ();

  protected UTXO createConfirmedUTXO ()
  {
    return new InMemoryUTXO ();
  }

  protected UTXO createChangeUTXO ()
  {
    return new InMemoryUTXO ();
  }

  protected UTXO createSendingUTXO ()
  {
    return new InMemoryUTXO ();
  }

  protected UTXO createReceivingUTXO ()
  {
    return new InMemoryUTXO ();
  }

  protected void reset ()
  {
    confirmed = createConfirmedUTXO ();
    change = createChangeUTXO ();
    receiving = createReceivingUTXO ();
    sending = createSendingUTXO ();
  }

  public boolean isOwnAddress (Address address)
  {
    return getKeyForAddress (address) != null;
  }

  protected static class TransactionSink
  {
    private final Address address;
    private final long value;

    public TransactionSink (Address address, long value)
    {
      super ();
      this.address = address;
      this.value = value;
    }

    public Address getAddress ()
    {
      return address;
    }

    public long getValue ()
    {
      return value;
    }
  }

  public static long estimateFee (Transaction t)
  {
    WireFormat.Writer writer = new WireFormat.Writer ();
    t.toWire (writer);
    return Math.min (MAXIMUM_FEE, Math.max (MINIMUM_FEE, writer.toByteArray ().length / 1000 * MINIMUM_FEE));
  }

  protected Transaction createSpend (List<TransactionOutput> sources, List<TransactionSink> sinks, long fee) throws ValidationException
  {
    if ( fee < 0 || fee > MAXIMUM_FEE )
    {
      throw new ValidationException ("You unlikely want to do that");
    }
    Transaction transaction = new Transaction ();
    transaction.setInputs (new ArrayList<TransactionInput> ());
    transaction.setOutputs (new ArrayList<TransactionOutput> ());

    long sumOut = 0;
    for ( TransactionSink s : sinks )
    {
      TransactionOutput o = new TransactionOutput ();
      o.setValue (s.getValue ());
      sumOut += s.getValue ();
      o.setScript (s.getAddress ().getAddressScript ());

      transaction.getOutputs ().add (o);
    }

    long sumInput = 0;
    for ( TransactionOutput o : sources )
    {
      TransactionInput i = new TransactionInput ();
      i.setSourceHash (o.getTxHash ());
      i.setIx (o.getIx ());
      sumInput += o.getValue ();

      transaction.getInputs ().add (i);
    }
    if ( sumInput != (sumOut + fee) )
    {
      throw new ValidationException ("Sum of sinks (+fee) does not match sum of sources");
    }

    int j = 0;
    for ( TransactionOutput s : sources )
    {
      TransactionInput i = transaction.getInputs ().get (j);
      ScriptFormat.Writer sw = new ScriptFormat.Writer ();
      if ( ScriptFormat.isPayToAddress (s.getScript ()) )
      {
        Address address = s.getOutputAddress ();
        Key key = getKeyForAddress (address);
        if ( key == null )
        {
          throw new ValidationException ("Have no key to spend this output");
        }
        byte[] sig = key.sign (hashTransaction (transaction, j, ScriptFormat.SIGHASH_ALL, s.getScript ()));
        byte[] sigPlusType = new byte[sig.length + 1];
        System.arraycopy (sig, 0, sigPlusType, 0, sig.length);
        sigPlusType[sigPlusType.length - 1] = (byte) (ScriptFormat.SIGHASH_ALL & 0xff);
        sw.writeData (sigPlusType);
        sw.writeData (key.getPublic ());
      }
      else
      {
        spendNonAddressOutput (j, s, sw, transaction);
      }
      i.setScript (sw.toByteArray ());
      ++j;
    }

    transaction.computeHash ();
    return transaction;
  }

  protected void spendNonAddressOutput (int ix, TransactionOutput source, ScriptFormat.Writer writer, Transaction transaction) throws ValidationException
  {
    throw new ValidationException ("Can not spend this output type");
  }

  public static byte[] hashTransaction (Transaction transaction, int inr, int hashType, byte[] script) throws ValidationException
  {
    Transaction copy = null;
    try
    {
      copy = transaction.clone ();
    }
    catch ( CloneNotSupportedException e1 )
    {
      return null;
    }

    // implicit SIGHASH_ALL
    int i = 0;
    for ( TransactionInput in : copy.getInputs () )
    {
      if ( i == inr )
      {
        in.setScript (script);
      }
      else
      {
        in.setScript (new byte[0]);
      }
      ++i;
    }

    if ( (hashType & 0x1f) == ScriptFormat.SIGHASH_NONE )
    {
      copy.getOutputs ().clear ();
      i = 0;
      for ( TransactionInput in : copy.getInputs () )
      {
        if ( i != inr )
        {
          in.setSequence (0);
        }
        ++i;
      }
    }
    else if ( (hashType & 0x1f) == ScriptFormat.SIGHASH_SINGLE )
    {
      int onr = inr;
      if ( onr >= copy.getOutputs ().size () )
      {
        // this is a Satoshi client bug.
        // This case should throw an error but it instead retuns 1 that is not checked and interpreted as below
        return ByteUtils.fromHex ("0100000000000000000000000000000000000000000000000000000000000000");
      }
      for ( i = copy.getOutputs ().size () - 1; i > onr; --i )
      {
        copy.getOutputs ().remove (i);
      }
      for ( i = 0; i < onr; ++i )
      {
        copy.getOutputs ().get (i).setScript (new byte[0]);
        copy.getOutputs ().get (i).setValue (-1L);
      }
      i = 0;
      for ( TransactionInput in : copy.getInputs () )
      {
        if ( i != inr )
        {
          in.setSequence (0);
        }
        ++i;
      }
    }
    if ( (hashType & ScriptFormat.SIGHASH_ANYONECANPAY) != 0 )
    {
      List<TransactionInput> oneIn = new ArrayList<TransactionInput> ();
      oneIn.add (copy.getInputs ().get (inr));
      copy.setInputs (oneIn);
    }

    WireFormat.Writer writer = new WireFormat.Writer ();
    copy.toWire (writer);

    byte[] txwire = writer.toByteArray ();
    byte[] hash = null;
    try
    {
      MessageDigest a = MessageDigest.getInstance ("SHA-256");
      a.update (txwire);
      a.update (new byte[] { (byte) (hashType & 0xff), 0, 0, 0 });
      hash = a.digest (a.digest ());
    }
    catch ( NoSuchAlgorithmException e )
    {
    }
    return hash;
  }

  protected List<TransactionOutput> getSufficientSources (long amount, long fee, String color)
  {
    List<TransactionOutput> candidates = new ArrayList<TransactionOutput> ();
    candidates.addAll (confirmed.getUTXO ());
    // prefer confirmed
    Collections.sort (candidates, new Comparator<TransactionOutput> ()
    {
      // prefer aggregation of UTXO
      @Override
      public int compare (TransactionOutput o1, TransactionOutput o2)
      {
        return o1.getValue () < o2.getValue () ? -1 : o1.getValue () > o2.getValue () ? 1 : 0;
      }
    });
    List<TransactionOutput> changelist = new ArrayList<TransactionOutput> ();
    changelist.addAll (change.getUTXO ());
    // ... then change
    Collections.sort (changelist, new Comparator<TransactionOutput> ()
    {
      // prefer aggregation of UTXO
      @Override
      public int compare (TransactionOutput o1, TransactionOutput o2)
      {
        return o1.getValue () < o2.getValue () ? -1 : o1.getValue () > o2.getValue () ? 1 : 0;
      }
    });
    candidates.addAll (changelist);

    List<TransactionOutput> result = new ArrayList<TransactionOutput> ();
    long sum = 0;
    for ( TransactionOutput o : candidates )
    {
      if ( color == null )
      {
        if ( o.getColor () == null )
        {
          sum += o.getValue ();
          result.add (o);
          if ( sum >= (amount + fee) )
          {
            return result;
          }
        }
      }
      else
      {
        if ( o.getColor ().equals (color) )
        {
          sum += o.getValue ();
          result.add (o);
          if ( sum >= amount )
          {
            if ( fee > 0 )
            {
              result.addAll (getSufficientSources (0, fee, null));
            }
            return result;
          }
        }
      }
    }
    return null;
  }

  @Override
  public Address getNextAddress () throws ValidationException
  {
    return getNextKey ().getAddress ();
  }

  @Override
  public Transaction pay (Address receiver, long amount, long fee, boolean senderPaysFee) throws ValidationException
  {
    List<Address> a = new ArrayList<Address> ();
    a.add (receiver);
    List<Long> v = new ArrayList<Long> ();
    v.add (amount);
    return pay (a, v, fee, senderPaysFee);
  }

  @Override
  public Transaction pay (List<Address> receiver, List<Long> amounts, long fee, boolean senderPaysFee) throws ValidationException
  {
    synchronized ( confirmed )
    {
      long amount = 0;
      for ( Long a : amounts )
      {
        amount += a;
      }
      log.trace ("pay " + amount + (senderPaysFee ? " + " + fee : ""));
      List<TransactionOutput> sources = getSufficientSources (amount, senderPaysFee ? fee : 0, null);
      if ( sources == null )
      {
        throw new ValidationException ("Insufficient funds to pay " + amount + (senderPaysFee ? " + " + fee : ""));
      }
      long in = 0;
      for ( TransactionOutput o : sources )
      {
        log.trace ("using input " + o.getTxHash () + "[" + o.getIx () + "] " + o.getValue ());
        in += o.getValue ();
      }
      List<TransactionSink> sinks = new ArrayList<TransactionSink> ();
      Iterator<Long> ai = amounts.iterator ();
      for ( Address r : receiver )
      {
        sinks.add (new TransactionSink (r, ai.next ()));
      }
      if ( !senderPaysFee )
      {
        TransactionSink last = sinks.get (sinks.size () - 1);
        sinks.set (sinks.size () - 1, new TransactionSink (last.getAddress (),
            Math.max (last.getValue () - fee, 0)));
      }
      if ( (in - amount) > (senderPaysFee ? fee : 0) )
      {
        TransactionSink change = new TransactionSink (getNextAddress (), in - amount - (senderPaysFee ? fee : 0));
        log.trace ("change to " + change.getAddress () + " " + change.getValue ());
        sinks.add (change);
      }
      Collections.shuffle (sinks);
      return createSpend (sources, sinks, fee);
    }
  }

  @Override
  public Transaction pay (Address receiver, long amount, boolean senderPaysFee) throws ValidationException
  {
    List<Address> a = new ArrayList<Address> ();
    a.add (receiver);
    List<Long> v = new ArrayList<Long> ();
    v.add (amount);
    return pay (a, v, senderPaysFee);
  }

  @Override
  public Transaction pay (List<Address> receiver, List<Long> amounts, boolean senderPaysFee) throws ValidationException
  {
    long fee = MINIMUM_FEE;
    long estimate = 0;
    Transaction t = null;

    do
    {
      fee = Math.max (fee, estimate);
      t = pay (receiver, amounts, fee, senderPaysFee);
      estimate = estimateFee (t);
      if ( fee < estimate )
      {
        log.trace ("The transaction requires more network fees. Reassembling.");
      }
    } while ( fee < estimate );

    return t;
  }

  public boolean updateWithTransaction (Transaction t)
  {
    synchronized ( confirmed )
    {
      boolean modified = false;
      if ( !t.isDoubleSpend () )
      {
        TransactionOutput spend = null;
        for ( TransactionInput i : t.getInputs () )
        {
          spend = confirmed.get (i.getSourceHash (), i.getIx ());
          if ( spend != null )
          {
            confirmed.remove (i.getSourceHash (), i.getIx ());
            log.trace ("Spend settled output " + i.getSourceHash () + " [" + i.getIx () + "] " + spend.getValue ());
          }
          else
          {
            spend = change.get (i.getSourceHash (), i.getIx ());
            if ( spend != null )
            {
              change.remove (i.getSourceHash (), i.getIx ());
              log.trace ("Spend change output " + i.getSourceHash () + " [" + i.getIx () + "] " + spend.getValue ());
            }
            else
            {
              spend = receiving.get (i.getSourceHash (), i.getIx ());
              if ( spend != null )
              {
                receiving.remove (i.getSourceHash (), i.getIx ());
                log.trace ("Spend receiving output " + i.getSourceHash () + " [" + i.getIx () + "] " + spend.getValue ());
              }
            }
          }
        }
        modified = spend != null;
        for ( TransactionOutput o : t.getOutputs () )
        {
          confirmed.remove (o.getTxHash (), o.getIx ());
          change.remove (o.getTxHash (), o.getIx ());
          receiving.remove (o.getTxHash (), o.getIx ());
          sending.remove (o.getTxHash (), o.getIx ());

          if ( isOwnAddress (o.getOutputAddress ()) )
          {
            modified = true;
            if ( t.getBlockHash () != null )
            {
              confirmed.add (o);
              log.trace ("Settled " + t.getHash () + " [" + o.getIx () + "] (" + o.getOutputAddress () + ") " + o.getValue ());
            }
            else
            {
              if ( spend != null )
              {
                change.add (o);
                log.trace ("Change " + t.getHash () + " [" + o.getIx () + "] (" + o.getOutputAddress () + ") "
                    + o.getValue ());
              }
              else
              {
                receiving.add (o);
                log.trace ("Receiving " + t.getHash () + " [" + o.getIx () + "] (" + o.getOutputAddress () + ") "
                    + o.getValue ());
              }
            }
          }
          else
          {
            if ( t.getBlockHash () == null && spend != null )
            {
              modified = true;
              sending.add (o);
              log.trace ("Sending " + t.getHash () + " [" + o.getIx () + "] (" + o.getOutputAddress () + ") " + o.getValue ());
            }
          }
        }
        if ( modified )
        {
          transactions.put (t.getHash (), t);
        }
      }
      else
      {
        for ( long ix = 0; ix < t.getOutputs ().size (); ++ix )
        {
          TransactionOutput out = null;
          out = confirmed.remove (t.getHash (), ix);
          if ( out == null )
          {
            out = change.remove (t.getHash (), ix);
          }
          if ( out == null )
          {
            out = receiving.remove (t.getHash (), ix);
          }
          if ( out == null )
          {
            out = sending.remove (t.getHash (), ix);
          }
          if ( out != null )
          {
            log.trace ("Remove DS " + out.getTxHash () + " [" + out.getIx () + "] (" + out.getOutputAddress () + ")"
                + out.getValue ());
          }
          modified |= out != null;
        }
        transactions.remove (t.getHash ());
      }
      return modified;
    }
  }

  @Override
  public long getBalance ()
  {
    synchronized ( confirmed )
    {
      return confirmed.getTotal () + change.getTotal () + receiving.getTotal ();
    }
  }

  @Override
  public long getConfirmed ()
  {
    synchronized ( confirmed )
    {
      return confirmed.getTotal ();
    }
  }

  @Override
  public long getSending ()
  {
    synchronized ( confirmed )
    {
      return sending.getTotal ();
    }
  }

  @Override
  public long getReceiving ()
  {
    synchronized ( confirmed )
    {
      return receiving.getTotal ();
    }
  }

  @Override
  public long getChange ()
  {
    synchronized ( confirmed )
    {
      return change.getTotal ();
    }
  }

  @Override
  public Collection<TransactionOutput> getConfirmedOutputs ()
  {
    return confirmed.getUTXO ();
  }

  @Override
  public Collection<TransactionOutput> getSendingOutputs ()
  {
    return sending.getUTXO ();
  }

  @Override
  public Collection<TransactionOutput> getReceivingOutputs ()
  {
    return receiving.getUTXO ();
  }

  @Override
  public Collection<TransactionOutput> getChangeOutputs ()
  {
    return change.getUTXO ();
  }

  @Override
  public void addAccountListener (AccountListener listener)
  {
    accountListener.add (listener);
  }

  @Override
  public void removeAccountListener (AccountListener listener)
  {
    accountListener.remove (listener);
  }

  protected void notifyListener (Transaction t)
  {
    synchronized ( accountListener )
    {
      for ( AccountListener l : accountListener )
      {
        try
        {
          l.accountChanged (this, t);
        }
        catch ( Exception e )
        {
          log.error ("Uncaught exception in account listener", e);
        }
      }
    }
  }

  @Override
  public void process (Transaction t)
  {
    if ( updateWithTransaction (t) )
    {
      notifyListener (t);
    }
  }

  @Override
  public List<Transaction> getTransactions ()
  {
    List<Transaction> tl = new ArrayList<Transaction> ();
    tl.addAll (transactions.values ());
    Collections.sort (tl, new Comparator<Transaction> ()
    {
      @Override
      public int compare (Transaction a, Transaction b)
      {
        for ( TransactionInput i : b.getInputs () )
        {
          if ( i.getSourceHash ().equals (a.getHash ()) )
          {
            return -1;
          }
        }
        for ( TransactionInput i : a.getInputs () )
        {
          if ( i.getSourceHash ().equals (b.getHash ()) )
          {
            return 1;
          }
        }
        return 0;
      }
    });

    return tl;
  }

  public void dumpOutputs (PrintStream print)
  {
    print.println ("Confirmed:");
    dumpUTXO (print, confirmed);
    print.println ("Receiving:");
    dumpUTXO (print, receiving);
    print.println ("Change:");
    dumpUTXO (print, change);
    print.println ("Sending:");
    dumpUTXO (print, sending);
  }

  public void dumpKeys (PrintStream print)
  {
    Set<Address> addresses = new HashSet<> ();
    for ( TransactionOutput o : confirmed.getUTXO () )
    {
      addresses.add (o.getOutputAddress ());
    }
    for ( TransactionOutput o : change.getUTXO () )
    {
      addresses.add (o.getOutputAddress ());
    }
    for ( TransactionOutput o : receiving.getUTXO () )
    {
      addresses.add (o.getOutputAddress ());
    }
    for ( Address a : addresses )
    {
      Key k = getKeyForAddress (a);
      if ( k != null )
      {
        print.println (ECKeyPair.serializeWIF (k) + " (" + a + ")");
      }
    }
  }

  private void dumpUTXO (PrintStream print, UTXO set)
  {
    for ( TransactionOutput o : set.getUTXO () )
    {
      print.println (o.getTxHash () + "[" + o.getIx () + "] (" + o.getOutputAddress () + ") " + o.getValue ());
    }
    print.println ();
  }
}
TOP

Related Classes of com.bitsofproof.supernode.wallet.BaseAccountManager$TransactionSink

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.