2023-08-29 20:53:57 +00:00
--[[
atm.lua is a client program that runs on a computer connected to a backing
currency supply , to facilitate deposits and withdrawals as well as other
banking actions .
Each ATM keeps a secret security key that it uses to authorize secure actions
like recording transactions .
] ] --
local g = require ( " simple-graphics " )
local bankClient = require ( " bank-client " )
2023-09-17 14:14:56 +00:00
-- The name of the peripheral where this ATM can draw money from.
local CURRENCY_SOURCE = " minecraft:barrel_0 "
-- The name of the peripheral where this ATM can deposit money to.
local CURRENCY_SINK = " minecraft:barrel_1 "
-- The name of the peripheral where this ATM interacts with the user.
local CURRENCY_BIN = " minecraft:barrel_2 "
2023-08-29 22:11:55 +00:00
local BANK_HOST = " central-bank "
local SECURITY_KEY = " 4514-1691-1660-7358-1884-0506-0878-7098-1511-3359-3602-3581-6910-0791-1843-5936 "
local modem = peripheral.find ( " modem " ) or error ( " No modem attached. " )
bankClient.init ( peripheral.getName ( modem ) , BANK_HOST , SECURITY_KEY )
2023-09-17 14:14:56 +00:00
if not peripheral.isPresent ( CURRENCY_SOURCE ) then error ( " No CURRENCY_SOURCE peripheral named \" " .. CURRENCY_SOURCE .. " \" was found. " ) end
if not peripheral.isPresent ( CURRENCY_SINK ) then error ( " No CURRENCY_SINK peripheral named \" " .. CURRENCY_SINK .. " \" was found. " ) end
if not peripheral.isPresent ( CURRENCY_BIN ) then error ( " No CURRENCY_BIN peripheral named \" " .. CURRENCY_BIN .. " \" was found. " ) end
2023-08-29 22:11:55 +00:00
2023-08-29 20:53:57 +00:00
local W , H = term.getSize ( )
2023-09-17 14:14:56 +00:00
local function isCurrency ( itemStack )
return itemStack ~= nil and itemStack.name == " minecraft:sunflower " and itemStack.nbt == " 1b95aea642a1b0e9624787ed7227cf35 "
end
local function countCurrency ( peripheralName )
local inv = peripheral.wrap ( peripheralName )
if not inv then return 0 end
local total = 0
for slot , itemStack in pairs ( inv.list ( ) ) do
if isCurrency ( itemStack ) then total = total + itemStack.count end
end
return total
end
local function getFreeSpace ( peripheralName )
local inv = peripheral.wrap ( peripheralName )
if not inv then return 0 end
local space = 0
for i = 1 , inv.size ( ) do
local itemStack = inv.getItemDetail ( i )
if itemStack == nil then
space = space + 64
elseif isCurrency ( itemStack ) then
space = space + ( 64 - itemStack.count )
end
end
return space
end
local function transferCurrency ( fromName , toName , amount )
local sourceInv = peripheral.wrap ( fromName )
local transferred = 0
local attempts = 0
while transferred < amount do
local items = sourceInv.list ( )
for slot , itemStack in pairs ( items ) do
if isCurrency ( itemStack ) then
local amountToTransfer = math.min ( amount - transferred , itemStack.count )
local actualTransferred = sourceInv.pushItems ( toName , slot , amountToTransfer )
transferred = transferred + actualTransferred
end
end
attempts = attempts + 1
if attempts > 10 and transferred < amount then
return false , transferred
end
end
return true , amount
end
local function shortId ( account )
return " * " .. string.sub ( account.id , - 5 , - 1 )
end
local function isDigit ( char )
if # char ~= 1 then return false end
local intValue = string.byte ( char ) - string.byte ( " 0 " )
return intValue >= 0 and intValue <= 9
end
2023-08-29 20:53:57 +00:00
local function drawFrame ( )
g.clear ( term , colors.white )
g.drawXLine ( term , 1 , W , 1 , colors.black )
g.drawText ( term , 2 , 1 , " ATM " , colors.white , colors.black )
2023-09-17 14:14:56 +00:00
if bankClient.loggedIn ( ) then
local txt = " Logged in as " .. bankClient.state . auth.username
local len = # txt
g.drawText ( term , W - len , 1 , " Logged in as " , colors.lightGray , colors.black )
g.drawText ( term , W - len + 13 , 1 , bankClient.state . auth.username , colors.yellow , colors.black )
end
2023-08-29 20:53:57 +00:00
end
2023-08-29 21:25:57 +00:00
local function tryReadDiskCredentials ( name )
if disk.hasData ( name ) then
local dataFile = fs.combine ( disk.getMountPath ( name ) , " bank-credentials.json " )
if fs.exists ( dataFile ) then
local f = io.open ( dataFile , " r " )
local content = textutils.unserializeJSON ( f : read ( " *a " ) )
f : close ( )
if (
content ~= nil and
content.username and
type ( content.username ) == " string " and
content.password and
type ( content.password ) == " string "
) then
return content
end
end
end
return nil
end
2023-08-29 21:55:48 +00:00
local function tryLoginViaInput ( )
2023-08-29 20:53:57 +00:00
drawFrame ( )
2023-08-29 21:55:48 +00:00
g.drawTextCenter ( term , W / 2 , 3 , " Enter your username and password below. " , colors.black , colors.white )
2023-08-29 22:11:55 +00:00
g.drawText ( term , 16 , 5 , " Username " , colors.black , colors.white )
g.drawXLine ( term , 16 , 34 , 6 , colors.lightGray )
g.drawText ( term , 16 , 8 , " Password " , colors.black , colors.white )
g.drawXLine ( term , 16 , 34 , 9 , colors.lightGray )
2023-08-29 21:55:48 +00:00
g.fillRect ( term , 22 , 11 , 9 , 3 , colors.green )
g.drawTextCenter ( term , W / 2 , 12 , " Login " , colors.white , colors.green )
g.fillRect ( term , 22 , 15 , 9 , 3 , colors.red )
g.drawTextCenter ( term , W / 2 , 16 , " Cancel " , colors.white , colors.red )
local username = " "
local password = " "
local selectedInput = " username "
while true do
local usernameColor = colors.lightGray
if selectedInput == " username " then usernameColor = colors.gray end
2023-08-29 22:11:55 +00:00
g.drawXLine ( term , 16 , 34 , 6 , usernameColor )
g.drawText ( term , 16 , 6 , string.rep ( " * " , # username ) , colors.white , usernameColor )
2023-08-29 21:55:48 +00:00
local passwordColor = colors.lightGray
if selectedInput == " password " then passwordColor = colors.gray end
2023-08-29 22:11:55 +00:00
g.drawXLine ( term , 16 , 34 , 9 , passwordColor )
g.drawText ( term , 16 , 9 , string.rep ( " * " , # password ) , colors.white , passwordColor )
2023-08-29 21:55:48 +00:00
local event , p1 , p2 , p3 = os.pullEvent ( )
if event == " char " then
local char = p1
if selectedInput == " username " and # username < 12 then
username = username .. char
elseif selectedInput == " password " and # password < 18 then
password = password .. char
end
elseif event == " key " then
local keyCode = p1
local held = p2
if keyCode == keys.backspace then
if selectedInput == " username " and # username > 0 then
username = string.sub ( username , 1 , # username - 1 )
elseif selectedInput == " password " and # password > 0 then
password = string.sub ( password , 1 , # password - 1 )
end
2023-08-29 22:11:55 +00:00
elseif keyCode == keys.tab and selectedInput == " username " then
selectedInput = " password "
2023-09-17 14:14:56 +00:00
elseif keyCode == keys.enter and selectedInput == " password " then
return { username = username , password = password } -- Do login right away.
2023-08-29 21:55:48 +00:00
end
elseif event == " mouse_click " then
local button = p1
local x = p2
local y = p3
2023-08-29 22:11:55 +00:00
if y == 6 and x >= 16 and x <= 34 then
2023-08-29 21:55:48 +00:00
selectedInput = " username "
2023-08-29 22:11:55 +00:00
elseif y == 9 and x >= 16 and x <= 34 then
2023-08-29 21:55:48 +00:00
selectedInput = " password "
elseif y >= 11 and y <= 13 and x >= 22 and x <= 30 then
2023-08-29 22:11:55 +00:00
return { username = username , password = password } -- Do login
2023-08-29 21:55:48 +00:00
elseif y >= 15 and y <= 17 and x >= 22 and x <= 30 then
2023-08-29 22:11:55 +00:00
return nil -- Cancel
2023-08-29 21:55:48 +00:00
end
end
end
end
local function showLoginUI ( )
2023-08-29 21:09:38 +00:00
while true do
2023-08-29 21:55:48 +00:00
drawFrame ( )
g.drawTextCenter ( term , W / 2 , 3 , " Welcome to HandieBank ATM! " , colors.green , colors.white )
g.drawTextCenter ( term , W / 2 , 5 , " Insert your card below, or click to login. " , colors.black , colors.white )
g.fillRect ( term , 22 , 7 , 9 , 3 , colors.green )
g.drawTextCenter ( term , W / 2 , 8 , " Login " , colors.white , colors.green )
2023-08-29 21:09:38 +00:00
local event , p1 , p2 , p3 = os.pullEvent ( )
if event == " disk " then
2023-08-29 21:25:57 +00:00
local credentials = tryReadDiskCredentials ( p1 )
if credentials then
return credentials
2023-08-29 21:18:23 +00:00
else
2023-08-29 21:25:57 +00:00
disk.eject ( p1 )
2023-08-29 21:18:23 +00:00
end
2023-08-29 21:09:38 +00:00
elseif event == " mouse_click " then
local button = p1
local x = p2
local y = p3
2023-08-29 21:25:57 +00:00
if button == 1 and x >= 22 and x <= 30 and y >= 7 and y <= 9 then
2023-08-29 21:55:48 +00:00
local credentials = tryLoginViaInput ( )
if credentials then return credentials end
2023-08-29 21:09:38 +00:00
end
end
end
2023-08-29 20:53:57 +00:00
end
2023-08-29 22:11:55 +00:00
local function checkCredentialsUI ( credentials )
drawFrame ( )
g.drawTextCenter ( term , W / 2 , 3 , " Checking your credentials... " , colors.black , colors.white )
2023-08-29 22:24:06 +00:00
os.sleep ( 1 )
2023-08-29 22:11:55 +00:00
bankClient.logIn ( credentials.username , credentials.password )
local accounts , errorMsg = bankClient.getAccounts ( )
if not accounts then
bankClient.logOut ( )
g.drawTextCenter ( term , W / 2 , 5 , errorMsg , colors.red , colors.white )
os.sleep ( 2 )
return false
end
2023-08-29 22:24:06 +00:00
g.drawTextCenter ( term , W / 2 , 5 , " Authentication successful. " , colors.green , colors.white )
2023-08-29 22:11:55 +00:00
os.sleep ( 1 )
return true
end
2023-09-17 14:14:56 +00:00
local function currencyBinPreviewUpdater ( x , y , fg , bg , delay )
delay = delay or 1
return function ( )
while true do
local amount = countCurrency ( CURRENCY_BIN )
g.drawXLine ( term , x , x + 10 , y , bg )
g.drawText ( term , x , y , tostring ( amount ) , fg , bg )
os.sleep ( delay )
end
end
end
local function showDepositUI ( account )
drawFrame ( )
g.drawTextCenter ( term , W / 2 , 3 , " Deposit HandieMarks to your account " .. shortId ( account ) .. " . " , colors.black , colors.white )
g.drawTextCenter ( term , W / 2 , 5 , " Add currency to the bin, then click to continue. " , colors.black , colors.white )
g.drawText ( term , 20 , 8 , " Amount to Deposit " , colors.black , colors.white )
local continueButtonCoords = g.drawButton ( term , 20 , 12 , 11 , 3 , " Continue " , colors.white , colors.green )
local cancelButtonCoords = g.drawButton ( term , 20 , 16 , 11 , 3 , " Cancel " , colors.white , colors.red )
local state = { cancel = false , doDeposit = false }
parallel.waitForAny (
currencyBinPreviewUpdater ( 20 , 9 , colors.orange , colors.gray , 0.5 ) ,
function ( )
while true do
local event , button , x , y = os.pullEvent ( " mouse_click " )
if button == 1 then
if g.isButtonPressed ( x , y , continueButtonCoords ) and countCurrency ( CURRENCY_BIN ) > 0 then
state.doDeposit = true
return
elseif g.isButtonPressed ( x , y , cancelButtonCoords ) then
state.cancel = true
return
end
end
end
end
)
if state.cancel then return false end
if state.doDeposit then
local function tryReturnFundsInError ( amount )
local returnSuccess , returnAmount = transferCurrency ( CURRENCY_SOURCE , CURRENCY_BIN , amount )
if not returnSuccess then
local missingAmount = amount - returnAmount
g.appendAndDrawConsole ( term , console , " Couldn't return all funds. You are still owed " .. tostring ( missingAmount ) .. " $HMK. Please contact an administrator for assistance. " , cx , cy )
os.sleep ( 3 )
else
g.appendAndDrawConsole ( term , console , " Your funds have been returned to the bin. Please collect them. " , cx , cy )
os.sleep ( 3 )
end
end
-- Clear the buttons and show some status.
g.fillRect ( term , 1 , W , 12 , H - 11 , colors.white )
local console = g.createConsole ( W / 2 , H - 11 , colors.white , colors.black , " UP " )
local cx = W / 2 - W / 4
local cy = 11
local amount = countCurrency ( CURRENCY_BIN )
g.appendAndDrawConsole ( term , console , " Making deposit with value of " .. tostring ( amount ) .. " $HMK... " , cx , cy )
os.sleep ( 1 )
local success , actualAmount = transferCurrency ( CURRENCY_BIN , CURRENCY_SINK , amount )
if not success then
g.appendAndDrawConsole ( term , console , " Transfer failed! Actual transfer: " .. tostring ( actualAmount ) .. " $HMK. Please contact an administrator to report the issue. " , cx , cy )
os.sleep ( 1 )
tryReturnFundsInError ( actualAmount )
return false
end
g.appendAndDrawConsole ( term , console , " Transfer complete. " , cx , cy )
os.sleep ( 1 )
local tx , errorMsg = bankClient.recordTransaction ( account.id , amount , " ATM deposit " )
if not tx then
g.appendAndDrawConsole ( term , console , " Failed to post transaction: " .. errorMsg , cx , cy )
tryReturnFundsInError ( amount )
os.sleep ( 3 )
return false
end
g.appendAndDrawConsole ( term , console , " Transaction posted to account. " , cx , cy )
os.sleep ( 2 )
return true
end
end
local function showWithdrawUI ( account )
drawFrame ( )
g.drawTextCenter ( term , W / 2 , 3 , " Withdraw HandieMarks from your account " .. shortId ( account ) .. " . " , colors.black , colors.white )
g.drawTextCenter ( term , W / 2 , 5 , " Enter an amount to withdraw: " , colors.black , colors.white )
g.drawXLine ( term , 20 , 30 , 6 , colors.gray )
g.drawTextCenter ( term , W / 2 , 7 , " (Current balance: " .. tostring ( account.balance ) .. " $HMK) " , colors.gray , colors.white )
local continueButtonCoords = g.drawButton ( term , 20 , 12 , 11 , 3 , " Continue " , colors.white , colors.green )
local cancelButtonCoords = g.drawButton ( term , 20 , 16 , 11 , 3 , " Cancel " , colors.white , colors.red )
local inputValue = " "
local function drawInputValue ( val )
g.drawXLine ( term , 20 , 30 , 6 , colors.gray )
local amountColor = colors.orange
local intValue = tonumber ( val )
if intValue ~= nil and intValue > account.balance then
amountColor = colors.red
end
g.drawText ( term , 20 , 6 , val , colors.orange , colors.gray )
end
while true do
local event , p1 , p2 , p3 = os.pullEvent ( )
if event == " char " and isDigit ( p1 ) and # inputValue < 10 then
inputValue = inputValue .. p1
drawInputValue ( inputValue )
elseif event == " key " and p1 == keys.backspace and # inputValue > 0 then
inputValue = string.sub ( inputValue , 1 , # inputValue - 1 )
drawInputValue ( inputValue )
elseif event == " mouse_click " and p1 == 1 then
local x = p2
local y = p3
local amount = tonumber ( inputValue )
if g.isButtonPressed ( x , y , continueButtonCoords ) and amount ~= nil and amount > 0 and amount <= account.balance then
local function tryReclaimFundsInError ( amount )
local returnSuccess , returnAmount = transferCurrency ( CURRENCY_BIN , CURRENCY_SINK , amount )
if not returnSuccess then
g.appendAndDrawConsole ( term , console , " Failed to reclaim funds. Please contact an administrator. " , cx , cy )
end
end
-- Do withdrawal
g.fillRect ( term , 1 , W , 12 , H - 11 , colors.white )
local console = g.createConsole ( W / 2 , H - 11 , colors.white , colors.black , " UP " )
local cx = W / 2 - W / 4
local cy = 11
g.appendAndDrawConsole ( term , console , " Making withdrawal of " .. tostring ( amount ) .. " $HMK from account " .. shortId ( account ) .. " . " , cx , cy )
os.sleep ( 1 )
local withdrawn = 0
while withdrawn < amount do
local freeSpace = getFreeSpace ( CURRENCY_BIN )
if freeSpace < 1 then
g.appendAndDrawConsole ( term , console , " No space available in the bin. Please take some currency out to continue. " , cx , cy )
while getFreeSpace ( CURRENCY_BIN ) < 1 do
g.appendAndDrawConsole ( term , console , " Waiting for free space... " , cx , cy )
os.sleep ( 3 )
end
end
local amountToTransfer = math.min ( freeSpace , amount - withdrawn )
local success , actualTransfer = transferCurrency ( CURRENCY_SOURCE , CURRENCY_BIN , amountToTransfer )
withdrawn = withdrawn + actualTransfer
if not success then
-- Failure! Send the money back, if we can.
g.appendAndDrawConsole ( term , console , " Transfer failed! Please contact an administrator to report the issue. " , cx , cy )
os.sleep ( 3 )
tryReclaimFundsInError ( withdrawn )
return false
else
g.appendAndDrawConsole ( term , console , " Transferred " .. tostring ( actualTransfer ) .. " $HMK. " , cx , cy )
os.sleep ( 1 )
end
end
local tx , errorMsg = bankClient.recordTransaction ( account.id , amount * - 1 , " ATM withdrawal " )
if not tx then
g.appendAndDrawConsole ( term , console , " Failed to post transaction: " .. errorMsg , cx , cy )
tryReclaimFundsInError ( amount )
os.sleep ( 3 )
return false
end
g.appendAndDrawConsole ( term , console , " Transaction posted to account. " , cx , cy )
os.sleep ( 2 )
return true
elseif g.isButtonPressed ( x , y , cancelButtonCoords ) then
return false
end
end
end
end
2023-08-29 22:38:58 +00:00
local function showAccountUI ( account )
while true do
drawFrame ( )
g.drawXLine ( term , 1 , W , 2 , colors.gray )
g.drawText ( term , 2 , 2 , " Account: " .. account.name , colors.white , colors.gray )
2023-08-29 23:01:13 +00:00
g.drawText ( term , W - 3 , 2 , " Back " , colors.white , colors.blue )
2023-08-29 22:38:58 +00:00
g.drawText ( term , 2 , 4 , " ID " , colors.gray , colors.white )
g.drawText ( term , 2 , 5 , account.id , colors.black , colors.white )
2023-08-29 23:01:13 +00:00
g.drawText ( term , 2 , 7 , " Name " , colors.gray , colors.white )
g.drawText ( term , 2 , 8 , account.name , colors.black , colors.white )
g.drawText ( term , 2 , 10 , " Balance ($HMK) " , colors.gray , colors.white )
2023-09-17 14:14:56 +00:00
g.drawText ( term , 2 , 11 , tostring ( account.balance ) , colors.orange , colors.white )
local buttons = { }
buttons.deposit = g.drawButton ( term , 35 , 4 , 17 , 3 , " Deposit " , colors.white , colors.green )
if account.balance > 0 then
buttons.withdraw = g.drawButton ( term , 35 , 8 , 17 , 3 , " Withdraw " , colors.white , colors.purple )
buttons.transfer = g.drawButton ( term , 35 , 12 , 17 , 3 , " Transfer " , colors.white , colors.orange )
else
buttons.close = g.drawButton ( term , 35 , 16 , 17 , 3 , " Close Account " , colors.white , colors.red )
end
2023-08-29 23:01:13 +00:00
local event , button , x , y = os.pullEvent ( " mouse_click " )
if button == 1 then
if y == 2 and x >= W - 3 then
return -- exit back to the accounts UI
2023-09-17 14:14:56 +00:00
elseif g.isButtonPressed ( x , y , buttons.deposit ) then
local success = showDepositUI ( account )
if success then return end -- If successful, go back to the accounts page.
elseif buttons.withdraw and g.isButtonPressed ( x , y , buttons.withdraw ) then
local success = showWithdrawUI ( account )
if success then return end
elseif buttons.transfer and g.isButtonPressed ( x , y , buttons.transfer ) then
-- Do Transfer
return
elseif buttons.close and g.isButtonPressed ( x , y , buttons.close ) then
2023-08-29 23:01:13 +00:00
end
end
2023-08-29 22:38:58 +00:00
end
end
2023-08-29 22:24:06 +00:00
local function showAccountsUI ( )
while true do
drawFrame ( )
g.drawXLine ( term , 1 , 19 , 2 , colors.gray )
g.drawText ( term , 2 , 2 , " Account " , colors.white , colors.gray )
2023-08-29 22:38:58 +00:00
g.drawXLine ( term , 10 , 35 , 2 , colors.lightGray )
2023-08-29 22:28:16 +00:00
g.drawText ( term , 11 , 2 , " Name " , colors.white , colors.lightGray )
2023-08-29 22:38:58 +00:00
g.drawXLine ( term , 36 , W , 2 , colors.gray )
2023-09-17 14:14:56 +00:00
g.drawText ( term , 37 , 2 , " Balance " , colors.white , colors.gray )
g.drawText ( term , W - 6 , 2 , " Log Out " , colors.white , colors.red )
local accounts , errorMsg = bankClient.getAccounts ( )
if accounts then
for i , account in pairs ( accounts ) do
local bg = colors.blue
if i % 2 == 0 then bg = colors.lightBlue end
local fg = colors.white
local y = i + 2
g.drawXLine ( term , 1 , W , y , bg )
g.drawText ( term , 2 , y , shortId ( account ) , fg , bg )
g.drawText ( term , 11 , y , account.name , fg , bg )
g.drawText ( term , 37 , y , tostring ( account.balance ) , fg , bg )
end
else
g.drawTextCenter ( term , W / 2 , 4 , " Error: " .. errorMsg , colors.red , colors.white )
2023-08-29 22:24:06 +00:00
end
local event , button , x , y = os.pullEvent ( " mouse_click " )
2023-09-17 14:14:56 +00:00
if button == 1 then
if accounts and y > 2 and ( y - 2 ) <= # accounts then
showAccountUI ( accounts [ y - 2 ] )
elseif y == 2 and x >= W - 6 then
bankClient.logOut ( )
return
end
2023-08-29 22:24:06 +00:00
end
end
end
2023-09-17 14:14:56 +00:00
local function logoutAfterInactivity ( )
local function now ( ) return os.epoch ( " utc " ) end
local DELAY = 30000
local lastActivity = now ( )
while now ( ) < lastActivity + DELAY do
parallel.waitForAny (
function ( ) os.sleep ( 1 ) end ,
function ( )
local event = os.pullEvent ( )
if event == " mouse_click " or event == " key " or event == " key_up " or event == " char " then
lastActivity = now ( )
end
end
)
end
bankClient.logOut ( )
drawFrame ( )
g.drawText ( term , 2 , 3 , " Logged out due to inactivity. " , colors.gray , colors.white )
os.sleep ( 2 )
end
2023-08-29 21:09:38 +00:00
while true do
local credentials = showLoginUI ( )
2023-08-29 22:11:55 +00:00
local loginSuccess = checkCredentialsUI ( credentials )
if loginSuccess then
2023-09-17 14:14:56 +00:00
parallel.waitForAny ( showAccountsUI , logoutAfterInactivity )
2023-08-29 22:11:55 +00:00
end
2023-08-29 21:09:38 +00:00
end