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 }