Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- --[[
- 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")
- -- 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"
- 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)
- 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
- local W, H = term.getSize()
- 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
- 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)
- 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
- end
- 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
- local function tryLoginViaInput()
- drawFrame()
- g.drawTextCenter(term, W/2, 3, "Enter your username and password below.", colors.black, colors.white)
- 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)
- 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
- g.drawXLine(term, 16, 34, 6, usernameColor)
- g.drawText(term, 16, 6, string.rep("*", #username), colors.white, usernameColor)
- local passwordColor = colors.lightGray
- if selectedInput == "password" then passwordColor = colors.gray end
- g.drawXLine(term, 16, 34, 9, passwordColor)
- g.drawText(term, 16, 9, string.rep("*", #password), colors.white, passwordColor)
- 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
- elseif keyCode == keys.tab and selectedInput == "username" then
- selectedInput = "password"
- elseif keyCode == keys.enter and selectedInput == "password" then
- return {username = username, password = password} -- Do login right away.
- end
- elseif event == "mouse_click" then
- local button = p1
- local x = p2
- local y = p3
- if y == 6 and x >= 16 and x <= 34 then
- selectedInput = "username"
- elseif y == 9 and x >= 16 and x <= 34 then
- selectedInput = "password"
- elseif y >= 11 and y <= 13 and x >= 22 and x <= 30 then
- return {username = username, password = password} -- Do login
- elseif y >= 15 and y <= 17 and x >= 22 and x <= 30 then
- return nil -- Cancel
- end
- end
- end
- end
- local function showLoginUI()
- while true do
- 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)
- local event, p1, p2, p3 = os.pullEvent()
- if event == "disk" then
- local credentials = tryReadDiskCredentials(p1)
- if credentials then
- return credentials
- else
- disk.eject(p1)
- end
- elseif event == "mouse_click" then
- local button = p1
- local x = p2
- local y = p3
- if button == 1 and x >= 22 and x <= 30 and y >= 7 and y <= 9 then
- local credentials = tryLoginViaInput()
- if credentials then return credentials end
- end
- end
- end
- end
- local function checkCredentialsUI(credentials)
- drawFrame()
- g.drawTextCenter(term, W/2, 3, "Checking your credentials...", colors.black, colors.white)
- os.sleep(1)
- 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
- g.drawTextCenter(term, W/2, 5, "Authentication successful.", colors.green, colors.white)
- os.sleep(1)
- return true
- end
- 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
- 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)
- g.drawText(term, W-3, 2, "Back", colors.white, colors.blue)
- g.drawText(term, 2, 4, "ID", colors.gray, colors.white)
- g.drawText(term, 2, 5, account.id, colors.black, colors.white)
- 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)
- 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
- 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
- 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
- end
- end
- end
- end
- 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)
- g.drawXLine(term, 10, 35, 2, colors.lightGray)
- g.drawText(term, 11, 2, "Name", colors.white, colors.lightGray)
- g.drawXLine(term, 36, W, 2, colors.gray)
- 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)
- end
- local event, button, x, y = os.pullEvent("mouse_click")
- 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
- end
- end
- end
- 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
- while true do
- local credentials = showLoginUI()
- local loginSuccess = checkCredentialsUI(credentials)
- if loginSuccess then
- parallel.waitForAny(showAccountsUI, logoutAfterInactivity)
- end
- end
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement