bitcoin-atm

bitcoin atm for pyc inc.

git clone https://9o.is/git/bitcoin-atm.git

Overlord.scala

(8568B)


      1 package inc.pyc.chimera
      2 
      3 import minions._
      4 import lycia._
      5 import inc.pyc._
      6 import bill._
      7 import acceptor._
      8 import Events._
      9 import Commands._
     10 import akka.actor._
     11 import SupervisorStrategy._
     12 import FSM._
     13 import concurrent._
     14 import duration._
     15 
     16 
     17 /**
     18  * Respect
     19  */
     20 class Overlord extends FSM[State, Data] 
     21   with LoggingFSM[State, Data]
     22   with MinionFactory
     23   with Bitcoin 
     24   with NetworkHelper 
     25   with StateWatchHelper
     26   with Acceptor {
     27   
     28   import context.{dispatcher, system}
     29   
     30   override val supervisorStrategy =
     31     OneForOneStrategy(maxNrOfRetries = 10, withinTimeRange = 1 minute) {
     32       case _: java.util.concurrent.TimeoutException => Restart
     33       case _: IllegalArgumentException => Stop
     34       case _: Exception => Escalate
     35     }
     36 
     37   /**
     38    * Starts a bunch of services
     39    */
     40   override def preStart(): Unit = {
     41     log info "Starting Overlord ... "
     42     initAcceptor
     43     initNetwork
     44     initWallet
     45     initTicker
     46   }
     47   
     48   override def postStop(): Unit = {
     49     log info "Stopping Overlord ... "
     50     stopAcceptor
     51     stopNetwork
     52     stopTicker
     53     stopWallet
     54     super.postStop
     55   }
     56 
     57   startWith(Uninitialized, NullData)
     58 
     59   when (Uninitialized, stateTimeout = 30 seconds) {
     60     case Event(StateTimeout, _) =>
     61       goto (Malfunctioning) using Reason(Msg.unableToStart)
     62   }
     63 
     64   when (Idle) {
     65     case Event(Continue, _) =>
     66       goto (QrScan)
     67   }
     68 
     69   when (QrScan, stateTimeout = 30 seconds) (idleFallback orElse {
     70     case Event(qr: QrCode, _) =>
     71       validateQr(qr)
     72       goto (QrValidate)
     73       
     74     case Event(Previous, _) =>
     75       goto (Idle)
     76   })
     77 
     78   when (QrValidate, stateTimeout = 3 seconds) (qrScanFallback orElse {
     79     case Event(address: BitcoinAddress, _) =>
     80       //loginUser(address)
     81       goto (UserLogin) using address
     82       
     83     case Event(InvalidBitcoinAddress, _) =>
     84       goto (QrScan)
     85   })
     86 
     87   // testing
     88   when (UserLogin, stateTimeout = 0.5 seconds) {
     89     case Event(StateTimeout, address: BitcoinAddress) =>
     90       loginNumber(address)
     91       goto (PhoneLogin)
     92   }
     93   
     94   /*
     95   when (UserLogin, stateTimeout = 3 seconds) (qrScanFallback orElse {
     96     case Event(userInfo: UserInfo, address: BitcoinAddress) =>
     97       val data = AuditData(address, userInfo)
     98       audit(data)
     99       goto (HistoryAudit) using data
    100     
    101     case Event(LoggedOut, address: BitcoinAddress) =>
    102       loginNumber(address)
    103       goto (PhoneLogin)
    104   })
    105   */
    106   
    107   when (PhoneLogin, stateTimeout = 3 seconds) (qrScanFallback orElse {
    108     case Event(phone: Phone, address: BitcoinAddress) =>
    109       val userInfo = UserInfo()
    110       		.withPhoneNumber(phone)
    111       		.withPurchaseLimit(Lycia.phoneVerifiedBuyLimit)
    112       
    113       val data = AuditData(address, userInfo)
    114       audit(data)
    115       goto (HistoryAudit) using data
    116       
    117     case Event(LoggedOut, address: BitcoinAddress) =>
    118       val data = AuditData(address)
    119       audit(data)
    120       goto (HistoryAudit) using data
    121   })
    122   
    123   when (HistoryAudit, stateTimeout = 3 seconds) (qrScanFallback orElse {
    124     case Event(toSpend: LeftToSpend, audit: AuditData) =>
    125       val create = CreatorTx(audit, toSpend, price)
    126       createTx(create)
    127       goto (TxCreate) using create
    128   })
    129 
    130   when (TxCreate, stateTimeout = 1 second) (qrScanFallback orElse {
    131     case Event(tx: IncompleteTx, _) =>
    132       goto (CashInsert) using tx
    133   })
    134 
    135   when(CashInsert) {
    136     case Event(inserted: Inserted, tx: IncompleteTx) =>
    137       val inspect = InspectBill(inserted, tx, balance)
    138       validateBill(inspect)
    139       goto(CashValidate)
    140 
    141     case Event(Buy, tx: IncompleteTx) =>
    142       sell(tx)
    143       goto(Sending)
    144       
    145     case Event(Previous, _) =>
    146       goto (Idle) using NullData
    147   }
    148 
    149   when(CashValidate, stateTimeout = 5 seconds)(cashInsertFallback orElse {
    150     case Event(ValidBill, _) =>
    151       acceptor ! Stack
    152       stay
    153 
    154     case Event(InvalidBill, _) =>
    155       acceptor ! Return
    156       goto (LimitReached)
    157       
    158     case Event(InsufficientFunds, _) =>
    159       acceptor ! Return
    160       goto (InsufficientFunds)
    161 
    162     case Event(confirmed: Confirmed, tx: IncompleteTx) =>
    163       confirmBill(confirmed, tx)
    164       stay forMax (2 seconds)
    165 
    166     case Event(tx: IncompleteTx, _) =>
    167       goto(CashInsert) using tx
    168   })
    169   
    170   when (InsufficientFunds, stateTimeout = 4 seconds) {
    171     case Event(StateTimeout, _) =>
    172       stateData match {
    173         case _: IncompleteTx => goto (CashInsert)
    174         case _               => goto (Idle) using NullData
    175       }
    176   }
    177     
    178   when (LimitReached) {
    179     case Event(Previous, _) =>
    180       goto (CashInsert)
    181       
    182     case Event(info: UserVerify, _) =>
    183       loginUser(info)
    184       stay
    185       
    186     case Event(sms: SMSCode, _) =>
    187       System.client.updateSession(sms)
    188       stay
    189       
    190     case Event(userInfo: UserInfo, tx: IncompleteTx) =>
    191       val newTx = tx.withUserInfo(userInfo)
    192       if(newTx == tx) goto (LimitReached)
    193       else goto (CashInsert) using newTx
    194 
    195     case Event(Phone(number, _, true), tx: IncompleteTx) =>
    196       val newTx = tx
    197         .setLimit(Lycia.phoneVerifiedBuyLimit)
    198       	.withPhoneNumber(Phone(number, true, true))
    199       
    200       saveNumber(tx.address, Phone(number))
    201       saveTx(newTx)
    202       goto (CashInsert) using newTx
    203       
    204     case Event(phone: Phone, tx: IncompleteTx) =>
    205       verifyNumber(phone)
    206       val newTx = tx.withPhoneNumber(phone)
    207       stay using newTx
    208   }
    209 
    210   when (Sending, stateTimeout = 45 seconds) (errorFallback orElse {
    211     case Event(tx: CompleteTx, _) =>
    212       completeTx(tx)
    213       goto (ThankYou) using tx
    214   })
    215   
    216   when (ThankYou, stateTimeout = 5 seconds) {
    217     case Event(Touch, _) =>
    218       goto (Receipt)
    219       
    220     case Event(StateTimeout, _) =>
    221       goto (Receipt)
    222   }
    223   
    224   when (Ad, stateTimeout = 30 seconds)(idleFallback orElse {
    225     case Event(Touch, _) =>
    226       goto (Idle)
    227   })
    228 
    229   when (Receipt, stateTimeout = 45 seconds) (idleFallback orElse {
    230     case Event(Continue, tx: CompleteTx) =>
    231       goto (Ad) using NullData // finished
    232     
    233     case Event(email: Email, tx: CompleteTx) =>
    234       sendReceipt(email, tx)
    235       // TODO if not logged in, create a user acct but don't send email registratiion
    236       saveReceiptEmail(tx.address, email)
    237       goto (Ad) using NullData
    238   })
    239 
    240   when (ErrorState, stateTimeout = 45 seconds) (idleFallback orElse {
    241     case Event(phone: Phone, tx: IncompleteTx) =>
    242       sendSupport(phone, tx)
    243       goto (Malfunctioning) using NullData
    244     
    245     case Event(email: Email, tx: IncompleteTx) =>
    246       sendSupport(email, tx)
    247       goto (Malfunctioning) using NullData
    248   })
    249 
    250   whenUnhandled (
    251     handleTicker orElse
    252     handleWallet orElse
    253     handleNetworkOutage orElse
    254     handleAcceptor orElse {
    255       
    256       case Event(DataRequest, _) =>
    257         sender ! DataReply(stateData)
    258         stay
    259         
    260       case Event(Touch, _) =>
    261         stay
    262         
    263       case Event(Status.Failure(error), _) =>
    264         log error ("Minion Fail in {}/{}: {}", stateName, stateData, error)
    265         stay
    266 
    267       case Event(event, _) =>
    268         log warning ("unhandled message: {} in {}/{}", event, stateName, stateData)
    269         stay
    270     })
    271 
    272   onTransition (acceptorTransitions orElse {
    273     case _ -> InsufficientFunds =>
    274       log warning ("Insufficient funds: {}/{}", stateName, stateData)
    275       
    276     case _ -> ErrorState =>
    277       stateData match {
    278         case tx: IncompleteTx => tx.userInfo.email.foreach {
    279           email => self ! Email(email)
    280         }
    281         case _ =>
    282       }
    283   })
    284 
    285   /**
    286    * When state times out, return cash in escrow.
    287    */
    288   def cashInsertFallback: StateFunction = {
    289     case Event(StateTimeout, _) =>
    290       acceptor ! Return
    291       goto (CashInsert)
    292   }
    293 
    294   /**
    295    * When state times out, go back to the `Idle` screen.
    296    */
    297   def idleFallback: StateFunction = {
    298     case Event(StateTimeout, _) =>
    299       goto (Idle) using NullData
    300   }
    301 
    302   /**
    303    * When state times out, go back to the `QrScan` screen.
    304    */
    305   def qrScanFallback: StateFunction = {
    306     case Event(qr: QrCode, _) => stay
    307     case Event(StateTimeout, _) =>
    308       goto (QrScan) using NullData
    309   }
    310 
    311   /**
    312    * When state times out in a crucial state, like 
    313    * sending bitcoins.
    314    */
    315   def errorFallback: StateFunction = {
    316     case Event(StateTimeout, _) =>
    317       errorNeedsAssistance
    318       goto (ErrorState)
    319   }
    320   
    321   /**
    322    * Checks if the current state is an 
    323    * active transaction state.
    324    */
    325   def sensitiveState: Boolean = {
    326     stateData match {
    327       case _: IncompleteTx    => true
    328       case _                  => false
    329     }
    330   }
    331 
    332   /**
    333    * Log error that asks for assistance.
    334    */
    335   def errorNeedsAssistance {
    336     log error ("Manual Assistance Required! "+
    337         "Failed with {}/{}", stateName, stateData)
    338   }
    339   
    340   initialize
    341 }