Advertisement
HandieAndy

atm.lua

Aug 29th, 2023 (edited)
1,899
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 22.08 KB | Gaming | 0 0
  1. --[[
  2. atm.lua is a client program that runs on a computer connected to a backing
  3. currency supply, to facilitate deposits and withdrawals as well as other
  4. banking actions.
  5.  
  6. Each ATM keeps a secret security key that it uses to authorize secure actions
  7. like recording transactions.
  8. ]]--
  9.  
  10. local g = require("simple-graphics")
  11. local bankClient = require("bank-client")
  12.  
  13. -- The name of the peripheral where this ATM can draw money from.
  14. local CURRENCY_SOURCE = "minecraft:barrel_0"
  15. -- The name of the peripheral where this ATM can deposit money to.
  16. local CURRENCY_SINK = "minecraft:barrel_1"
  17. -- The name of the peripheral where this ATM interacts with the user.
  18. local CURRENCY_BIN = "minecraft:barrel_2"
  19.  
  20. local BANK_HOST = "central-bank"
  21. local SECURITY_KEY = "4514-1691-1660-7358-1884-0506-0878-7098-1511-3359-3602-3581-6910-0791-1843-5936"
  22. local modem = peripheral.find("modem") or error("No modem attached.")
  23. bankClient.init(peripheral.getName(modem), BANK_HOST, SECURITY_KEY)
  24. if not peripheral.isPresent(CURRENCY_SOURCE) then error("No CURRENCY_SOURCE peripheral named \""..CURRENCY_SOURCE.."\" was found.") end
  25. if not peripheral.isPresent(CURRENCY_SINK) then error("No CURRENCY_SINK peripheral named \""..CURRENCY_SINK.."\" was found.") end
  26. if not peripheral.isPresent(CURRENCY_BIN) then error("No CURRENCY_BIN peripheral named \""..CURRENCY_BIN.."\" was found.") end
  27.  
  28. local W, H = term.getSize()
  29.  
  30. local function isCurrency(itemStack)
  31.     return itemStack ~= nil and itemStack.name == "minecraft:sunflower" and itemStack.nbt == "1b95aea642a1b0e9624787ed7227cf35"
  32. end
  33.  
  34. local function countCurrency(peripheralName)
  35.     local inv = peripheral.wrap(peripheralName)
  36.     if not inv then return 0 end
  37.     local total = 0
  38.     for slot, itemStack in pairs(inv.list()) do
  39.         if isCurrency(itemStack) then total = total + itemStack.count end
  40.     end
  41.     return total
  42. end
  43.  
  44. local function getFreeSpace(peripheralName)
  45.     local inv = peripheral.wrap(peripheralName)
  46.     if not inv then return 0 end
  47.     local space = 0
  48.     for i = 1, inv.size() do
  49.         local itemStack = inv.getItemDetail(i)
  50.         if itemStack == nil then
  51.             space = space + 64
  52.         elseif isCurrency(itemStack) then
  53.             space = space + (64 - itemStack.count)
  54.         end
  55.     end
  56.     return space
  57. end
  58.  
  59. local function transferCurrency(fromName, toName, amount)
  60.     local sourceInv = peripheral.wrap(fromName)
  61.     local transferred = 0
  62.     local attempts = 0
  63.     while transferred < amount do
  64.         local items = sourceInv.list()
  65.         for slot, itemStack in pairs(items) do
  66.             if isCurrency(itemStack) then
  67.                 local amountToTransfer = math.min(amount - transferred, itemStack.count)
  68.                 local actualTransferred = sourceInv.pushItems(toName, slot, amountToTransfer)
  69.                 transferred = transferred + actualTransferred
  70.             end
  71.         end
  72.         attempts = attempts + 1
  73.         if attempts > 10 and transferred < amount then
  74.             return false, transferred
  75.         end
  76.     end
  77.     return true, amount
  78. end
  79.  
  80. local function shortId(account)
  81.     return "*" .. string.sub(account.id, -5, -1)
  82. end
  83.  
  84. local function isDigit(char)
  85.     if #char ~= 1 then return false end
  86.     local intValue = string.byte(char) - string.byte("0")
  87.     return intValue >= 0 and intValue <= 9
  88. end
  89.  
  90. local function drawFrame()
  91.     g.clear(term, colors.white)
  92.     g.drawXLine(term, 1, W, 1, colors.black)
  93.     g.drawText(term, 2, 1, "ATM", colors.white, colors.black)
  94.     if bankClient.loggedIn() then
  95.         local txt = "Logged in as " .. bankClient.state.auth.username
  96.         local len = #txt
  97.         g.drawText(term, W-len, 1, "Logged in as", colors.lightGray, colors.black)
  98.         g.drawText(term, W-len+13, 1, bankClient.state.auth.username, colors.yellow, colors.black)
  99.     end
  100. end
  101.  
  102. local function tryReadDiskCredentials(name)
  103.     if disk.hasData(name) then
  104.         local dataFile = fs.combine(disk.getMountPath(name), "bank-credentials.json")
  105.         if fs.exists(dataFile) then
  106.             local f = io.open(dataFile, "r")
  107.             local content = textutils.unserializeJSON(f:read("*a"))
  108.             f:close()
  109.             if (
  110.                 content ~= nil and
  111.                 content.username and
  112.                 type(content.username) == "string" and
  113.                 content.password and
  114.                 type(content.password) == "string"
  115.             ) then
  116.                 return content
  117.             end
  118.         end
  119.     end
  120.     return nil
  121. end
  122.  
  123. local function tryLoginViaInput()
  124.     drawFrame()
  125.     g.drawTextCenter(term, W/2, 3, "Enter your username and password below.", colors.black, colors.white)
  126.     g.drawText(term, 16, 5, "Username", colors.black, colors.white)
  127.     g.drawXLine(term, 16, 34, 6, colors.lightGray)
  128.     g.drawText(term, 16, 8, "Password", colors.black, colors.white)
  129.     g.drawXLine(term, 16, 34, 9, colors.lightGray)
  130.  
  131.     g.fillRect(term, 22, 11, 9, 3, colors.green)
  132.     g.drawTextCenter(term, W/2, 12, "Login", colors.white, colors.green)
  133.  
  134.     g.fillRect(term, 22, 15, 9, 3, colors.red)
  135.     g.drawTextCenter(term, W/2, 16, "Cancel", colors.white, colors.red)
  136.  
  137.     local username = ""
  138.     local password = ""
  139.     local selectedInput = "username"
  140.     while true do
  141.         local usernameColor = colors.lightGray
  142.         if selectedInput == "username" then usernameColor = colors.gray end
  143.         g.drawXLine(term, 16, 34, 6, usernameColor)
  144.         g.drawText(term, 16, 6, string.rep("*", #username), colors.white, usernameColor)
  145.  
  146.         local passwordColor = colors.lightGray
  147.         if selectedInput == "password" then passwordColor = colors.gray end
  148.         g.drawXLine(term, 16, 34, 9, passwordColor)
  149.         g.drawText(term, 16, 9, string.rep("*", #password), colors.white, passwordColor)
  150.  
  151.         local event, p1, p2, p3 = os.pullEvent()
  152.         if event == "char" then
  153.             local char = p1
  154.             if selectedInput == "username" and #username < 12 then
  155.                 username = username .. char
  156.             elseif selectedInput == "password" and #password < 18 then
  157.                 password = password .. char
  158.             end
  159.         elseif event == "key" then
  160.             local keyCode = p1
  161.             local held = p2
  162.             if keyCode == keys.backspace then
  163.                 if selectedInput == "username" and #username > 0 then
  164.                     username = string.sub(username, 1, #username - 1)
  165.                 elseif selectedInput == "password" and #password > 0 then
  166.                     password = string.sub(password, 1, #password - 1)
  167.                 end
  168.             elseif keyCode == keys.tab and selectedInput == "username" then
  169.                 selectedInput = "password"
  170.             elseif keyCode == keys.enter and selectedInput == "password" then
  171.                 return {username = username, password = password} -- Do login right away.
  172.             end
  173.         elseif event == "mouse_click" then
  174.             local button = p1
  175.             local x = p2
  176.             local y = p3
  177.             if y == 6 and x >= 16 and x <= 34 then
  178.                 selectedInput = "username"
  179.             elseif y == 9 and x >= 16 and x <= 34 then
  180.                 selectedInput = "password"
  181.             elseif y >= 11 and y <= 13 and x >= 22 and x <= 30 then
  182.                 return {username = username, password = password} -- Do login
  183.             elseif y >= 15 and y <= 17 and x >= 22 and x <= 30 then
  184.                 return nil -- Cancel
  185.             end
  186.         end
  187.     end
  188. end
  189.  
  190. local function showLoginUI()
  191.     while true do
  192.         drawFrame()
  193.         g.drawTextCenter(term, W/2, 3, "Welcome to HandieBank ATM!", colors.green, colors.white)
  194.         g.drawTextCenter(term, W/2, 5, "Insert your card below, or click to login.", colors.black, colors.white)
  195.         g.fillRect(term, 22, 7, 9, 3, colors.green)
  196.         g.drawTextCenter(term, W/2, 8, "Login", colors.white, colors.green)
  197.         local event, p1, p2, p3 = os.pullEvent()
  198.         if event == "disk" then
  199.             local credentials = tryReadDiskCredentials(p1)
  200.             if credentials then
  201.                 return credentials
  202.             else
  203.                 disk.eject(p1)
  204.             end
  205.         elseif event == "mouse_click" then
  206.             local button = p1
  207.             local x = p2
  208.             local y = p3
  209.             if button == 1 and x >= 22 and x <= 30 and y >= 7 and y <= 9 then
  210.                 local credentials = tryLoginViaInput()
  211.                 if credentials then return credentials end
  212.             end
  213.         end
  214.     end
  215. end
  216.  
  217. local function checkCredentialsUI(credentials)
  218.     drawFrame()
  219.     g.drawTextCenter(term, W/2, 3, "Checking your credentials...", colors.black, colors.white)
  220.     os.sleep(1)
  221.     bankClient.logIn(credentials.username, credentials.password)
  222.     local accounts, errorMsg = bankClient.getAccounts()
  223.     if not accounts then
  224.         bankClient.logOut()
  225.         g.drawTextCenter(term, W/2, 5, errorMsg, colors.red, colors.white)
  226.         os.sleep(2)
  227.         return false
  228.     end
  229.     g.drawTextCenter(term, W/2, 5, "Authentication successful.", colors.green, colors.white)
  230.     os.sleep(1)
  231.     return true
  232. end
  233.  
  234. local function currencyBinPreviewUpdater(x, y, fg, bg, delay)
  235.     delay = delay or 1
  236.     return function()
  237.         while true do
  238.             local amount = countCurrency(CURRENCY_BIN)
  239.             g.drawXLine(term, x, x + 10, y, bg)
  240.             g.drawText(term, x, y, tostring(amount), fg, bg)
  241.             os.sleep(delay)
  242.         end
  243.     end
  244. end
  245.  
  246. local function showDepositUI(account)
  247.     drawFrame()
  248.     g.drawTextCenter(term, W/2, 3, "Deposit HandieMarks to your account "..shortId(account)..".", colors.black, colors.white)
  249.     g.drawTextCenter(term, W/2, 5, "Add currency to the bin, then click to continue.", colors.black, colors.white)
  250.  
  251.     g.drawText(term, 20, 8, "Amount to Deposit", colors.black, colors.white)
  252.    
  253.     local continueButtonCoords = g.drawButton(term, 20, 12, 11, 3, "Continue", colors.white, colors.green)
  254.     local cancelButtonCoords = g.drawButton(term, 20, 16, 11, 3, "Cancel", colors.white, colors.red)
  255.  
  256.     local state = {cancel = false, doDeposit = false}
  257.     parallel.waitForAny(
  258.         currencyBinPreviewUpdater(20, 9, colors.orange, colors.gray, 0.5),
  259.         function ()
  260.             while true do
  261.                 local event, button, x, y = os.pullEvent("mouse_click")
  262.                 if button == 1 then
  263.                     if g.isButtonPressed(x, y, continueButtonCoords) and countCurrency(CURRENCY_BIN) > 0 then
  264.                         state.doDeposit = true
  265.                         return
  266.                     elseif g.isButtonPressed(x, y, cancelButtonCoords) then
  267.                         state.cancel = true
  268.                         return
  269.                     end
  270.                 end
  271.             end
  272.         end
  273.     )
  274.  
  275.     if state.cancel then return false end
  276.     if state.doDeposit then
  277.         local function tryReturnFundsInError(amount)
  278.             local returnSuccess, returnAmount = transferCurrency(CURRENCY_SOURCE, CURRENCY_BIN, amount)
  279.             if not returnSuccess then
  280.                 local missingAmount = amount - returnAmount
  281.                 g.appendAndDrawConsole(term, console, "Couldn't return all funds. You are still owed "..tostring(missingAmount).." $HMK. Please contact an administrator for assistance.", cx, cy)
  282.                 os.sleep(3)
  283.             else
  284.                 g.appendAndDrawConsole(term, console, "Your funds have been returned to the bin. Please collect them.", cx, cy)
  285.                 os.sleep(3)
  286.             end
  287.         end
  288.         -- Clear the buttons and show some status.
  289.         g.fillRect(term, 1, W, 12, H-11, colors.white)
  290.         local console = g.createConsole(W/2, H-11, colors.white, colors.black, "UP")
  291.         local cx = W/2 - W/4
  292.         local cy = 11
  293.         local amount = countCurrency(CURRENCY_BIN)
  294.         g.appendAndDrawConsole(term, console, "Making deposit with value of "..tostring(amount).." $HMK...", cx, cy)
  295.         os.sleep(1)
  296.         local success, actualAmount = transferCurrency(CURRENCY_BIN, CURRENCY_SINK, amount)
  297.         if not success then
  298.             g.appendAndDrawConsole(term, console, "Transfer failed! Actual transfer: "..tostring(actualAmount).." $HMK. Please contact an administrator to report the issue.", cx, cy)
  299.             os.sleep(1)
  300.             tryReturnFundsInError(actualAmount)
  301.             return false
  302.         end
  303.         g.appendAndDrawConsole(term, console, "Transfer complete.", cx, cy)
  304.         os.sleep(1)
  305.         local tx, errorMsg = bankClient.recordTransaction(account.id, amount, "ATM deposit")
  306.         if not tx then
  307.             g.appendAndDrawConsole(term, console, "Failed to post transaction: " .. errorMsg, cx, cy)
  308.             tryReturnFundsInError(amount)
  309.             os.sleep(3)
  310.             return false
  311.         end
  312.         g.appendAndDrawConsole(term, console, "Transaction posted to account.", cx, cy)
  313.         os.sleep(2)
  314.         return true
  315.     end
  316. end
  317.  
  318. local function showWithdrawUI(account)
  319.     drawFrame()
  320.     g.drawTextCenter(term, W/2, 3, "Withdraw HandieMarks from your account "..shortId(account)..".", colors.black, colors.white)
  321.     g.drawTextCenter(term, W/2, 5, "Enter an amount to withdraw:", colors.black, colors.white)
  322.     g.drawXLine(term, 20, 30, 6, colors.gray)
  323.     g.drawTextCenter(term, W/2, 7, "(Current balance: "..tostring(account.balance).." $HMK)", colors.gray, colors.white)
  324.     local continueButtonCoords = g.drawButton(term, 20, 12, 11, 3, "Continue", colors.white, colors.green)
  325.     local cancelButtonCoords = g.drawButton(term, 20, 16, 11, 3, "Cancel", colors.white, colors.red)
  326.  
  327.     local inputValue = ""
  328.     local function drawInputValue(val)
  329.         g.drawXLine(term, 20, 30, 6, colors.gray)
  330.         local amountColor = colors.orange
  331.         local intValue = tonumber(val)
  332.         if intValue ~= nil and intValue > account.balance then
  333.             amountColor = colors.red
  334.         end
  335.         g.drawText(term, 20, 6, val, colors.orange, colors.gray)
  336.     end
  337.     while true do
  338.         local event, p1, p2, p3 = os.pullEvent()
  339.         if event == "char" and isDigit(p1) and #inputValue < 10 then
  340.             inputValue = inputValue .. p1
  341.             drawInputValue(inputValue)
  342.         elseif event == "key" and p1 == keys.backspace and #inputValue > 0 then
  343.             inputValue = string.sub(inputValue, 1, #inputValue - 1)
  344.             drawInputValue(inputValue)
  345.         elseif event == "mouse_click" and p1 == 1 then
  346.             local x = p2
  347.             local y = p3
  348.             local amount = tonumber(inputValue)
  349.             if g.isButtonPressed(x, y, continueButtonCoords) and amount ~= nil and amount > 0 and amount <= account.balance then
  350.                 local function tryReclaimFundsInError(amount)
  351.                     local returnSuccess, returnAmount = transferCurrency(CURRENCY_BIN, CURRENCY_SINK, amount)
  352.                     if not returnSuccess then
  353.                         g.appendAndDrawConsole(term, console, "Failed to reclaim funds. Please contact an administrator.", cx, cy)
  354.                     end
  355.                 end
  356.                 -- Do withdrawal
  357.                 g.fillRect(term, 1, W, 12, H-11, colors.white)
  358.                 local console = g.createConsole(W/2, H-11, colors.white, colors.black, "UP")
  359.                 local cx = W/2 - W/4
  360.                 local cy = 11
  361.                 g.appendAndDrawConsole(term, console, "Making withdrawal of " .. tostring(amount) .. " $HMK from account " .. shortId(account) .. ".", cx, cy)
  362.                 os.sleep(1)
  363.                 local withdrawn = 0
  364.                 while withdrawn < amount do
  365.                     local freeSpace = getFreeSpace(CURRENCY_BIN)
  366.                     if freeSpace < 1 then
  367.                         g.appendAndDrawConsole(term, console, "No space available in the bin. Please take some currency out to continue.", cx, cy)
  368.                         while getFreeSpace(CURRENCY_BIN) < 1 do
  369.                             g.appendAndDrawConsole(term, console, "Waiting for free space...", cx, cy)
  370.                             os.sleep(3)
  371.                         end
  372.                     end
  373.                     local amountToTransfer = math.min(freeSpace, amount - withdrawn)
  374.                     local success, actualTransfer = transferCurrency(CURRENCY_SOURCE, CURRENCY_BIN, amountToTransfer)
  375.                     withdrawn = withdrawn + actualTransfer
  376.                     if not success then
  377.                         -- Failure! Send the money back, if we can.
  378.                         g.appendAndDrawConsole(term, console, "Transfer failed! Please contact an administrator to report the issue.", cx, cy)
  379.                         os.sleep(3)
  380.                         tryReclaimFundsInError(withdrawn)
  381.                         return false
  382.                     else
  383.                         g.appendAndDrawConsole(term, console, "Transferred " .. tostring(actualTransfer) .. " $HMK.", cx, cy)
  384.                         os.sleep(1)
  385.                     end
  386.                 end
  387.                 local tx, errorMsg = bankClient.recordTransaction(account.id, amount * -1, "ATM withdrawal")
  388.                 if not tx then
  389.                     g.appendAndDrawConsole(term, console, "Failed to post transaction: " .. errorMsg, cx, cy)
  390.                     tryReclaimFundsInError(amount)
  391.                     os.sleep(3)
  392.                     return false
  393.                 end
  394.                 g.appendAndDrawConsole(term, console, "Transaction posted to account.", cx, cy)
  395.                 os.sleep(2)
  396.                 return true
  397.             elseif g.isButtonPressed(x, y, cancelButtonCoords) then
  398.                 return false
  399.             end
  400.         end
  401.     end
  402. end
  403.  
  404. local function showAccountUI(account)
  405.     while true do
  406.         drawFrame()
  407.         g.drawXLine(term, 1, W, 2, colors.gray)
  408.         g.drawText(term, 2, 2, "Account: " .. account.name, colors.white, colors.gray)
  409.         g.drawText(term, W-3, 2, "Back", colors.white, colors.blue)
  410.  
  411.         g.drawText(term, 2, 4, "ID", colors.gray, colors.white)
  412.         g.drawText(term, 2, 5, account.id, colors.black, colors.white)
  413.         g.drawText(term, 2, 7, "Name", colors.gray, colors.white)
  414.         g.drawText(term, 2, 8, account.name, colors.black, colors.white)
  415.         g.drawText(term, 2, 10, "Balance ($HMK)", colors.gray, colors.white)
  416.         g.drawText(term, 2, 11, tostring(account.balance), colors.orange, colors.white)
  417.  
  418.         local buttons = {}
  419.         buttons.deposit = g.drawButton(term, 35, 4, 17, 3, "Deposit", colors.white, colors.green)
  420.         if account.balance > 0 then
  421.             buttons.withdraw = g.drawButton(term, 35, 8, 17, 3, "Withdraw", colors.white, colors.purple)
  422.             buttons.transfer = g.drawButton(term, 35, 12, 17, 3, "Transfer", colors.white, colors.orange)
  423.         else
  424.             buttons.close = g.drawButton(term, 35, 16, 17, 3, "Close Account", colors.white, colors.red)
  425.         end
  426.         local event, button, x, y = os.pullEvent("mouse_click")
  427.         if button == 1 then
  428.             if y == 2 and x >= W-3 then
  429.                 return -- exit back to the accounts UI
  430.             elseif g.isButtonPressed(x, y, buttons.deposit) then
  431.                 local success = showDepositUI(account)
  432.                 if success then return end -- If successful, go back to the accounts page.
  433.             elseif buttons.withdraw and g.isButtonPressed(x, y, buttons.withdraw) then
  434.                 local success = showWithdrawUI(account)
  435.                 if success then return end
  436.             elseif buttons.transfer and g.isButtonPressed(x, y, buttons.transfer) then
  437.                 -- Do Transfer
  438.                 return
  439.             elseif buttons.close and g.isButtonPressed(x, y, buttons.close) then
  440.                
  441.             end
  442.         end
  443.     end
  444. end
  445.  
  446. local function showAccountsUI()
  447.     while true do
  448.         drawFrame()
  449.         g.drawXLine(term, 1, 19, 2, colors.gray)
  450.         g.drawText(term, 2, 2, "Account", colors.white, colors.gray)
  451.         g.drawXLine(term, 10, 35, 2, colors.lightGray)
  452.         g.drawText(term, 11, 2, "Name", colors.white, colors.lightGray)
  453.         g.drawXLine(term, 36, W, 2, colors.gray)
  454.         g.drawText(term, 37, 2, "Balance", colors.white, colors.gray)
  455.         g.drawText(term, W-6, 2, "Log Out", colors.white, colors.red)
  456.         local accounts, errorMsg = bankClient.getAccounts()
  457.         if accounts then
  458.             for i, account in pairs(accounts) do
  459.                 local bg = colors.blue
  460.                 if i % 2 == 0 then bg = colors.lightBlue end
  461.                 local fg = colors.white
  462.                 local y = i + 2
  463.                 g.drawXLine(term, 1, W, y, bg)
  464.                 g.drawText(term, 2, y, shortId(account), fg, bg)
  465.                 g.drawText(term, 11, y, account.name, fg, bg)
  466.                 g.drawText(term, 37, y, tostring(account.balance), fg, bg)
  467.             end
  468.         else
  469.             g.drawTextCenter(term, W/2, 4, "Error: " .. errorMsg, colors.red, colors.white)
  470.         end
  471.         local event, button, x, y = os.pullEvent("mouse_click")
  472.         if button == 1 then
  473.             if accounts and y > 2 and (y - 2) <= #accounts then
  474.                 showAccountUI(accounts[y-2])
  475.             elseif y == 2 and x >= W-6 then
  476.                 bankClient.logOut()
  477.                 return
  478.             end
  479.         end
  480.     end
  481. end
  482.  
  483. local function logoutAfterInactivity()
  484.     local function now() return os.epoch("utc") end
  485.     local DELAY = 30000
  486.     local lastActivity = now()
  487.     while now() < lastActivity + DELAY do
  488.         parallel.waitForAny(
  489.             function () os.sleep(1) end,
  490.             function ()
  491.                 local event = os.pullEvent()
  492.                 if event == "mouse_click" or event == "key" or event == "key_up" or event == "char" then
  493.                     lastActivity = now()
  494.                 end
  495.             end
  496.         )
  497.     end
  498.     bankClient.logOut()
  499.     drawFrame()
  500.     g.drawText(term, 2, 3, "Logged out due to inactivity.", colors.gray, colors.white)
  501.     os.sleep(2)
  502. end
  503.  
  504. while true do
  505.     local credentials = showLoginUI()
  506.     local loginSuccess = checkCredentialsUI(credentials)
  507.     if loginSuccess then
  508.         parallel.waitForAny(showAccountsUI, logoutAfterInactivity)
  509.     end
  510. end
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement