global_exchange/exchange.lua

1216 lines
25 KiB
Lua

local insecure_env = ...
local sql = insecure_env.require("lsqlite3")
local exports = {}
local init_query = [=[
BEGIN TRANSACTION;
CREATE TABLE if not exists Credit
(
Owner TEXT PRIMARY KEY NOT NULL,
Balance INTEGER NOT NULL
);
CREATE TABLE if not exists Log
(
Id INTEGER PRIMARY KEY AUTOINCREMENT,
Recipient TEXT NOT NULL,
Time INTEGER NOT NULL,
Message TEXT NOT NULL
);
CREATE TABLE if not exists Orders
(
Id INTEGER PRIMARY KEY AUTOINCREMENT,
Poster TEXT NOT NULL,
Exchange TEXT NOT NULL,
Type TEXT NOT NULL CHECK(Type IN ("buy", "sell")),
Time INTEGER NOT NULL,
Item TEXT NOT NULL,
Amount INTEGER NOT NULL CHECK(Amount > 0),
Rate INTEGER NOT NULL CHECK(Rate > 0)
);
CREATE TABLE if not exists Inbox
(
Id INTEGER PRIMARY KEY AUTOINCREMENT,
Recipient TEXT NOT NULL,
Item TEXT NOT NULL,
Amount INTEGER NOT NULL CHECK(Amount > 0)
);
CREATE INDEX if not exists credit_owner
ON Credit (Owner);
CREATE INDEX if not exists index_log
ON Log (Recipient, Time);
CREATE INDEX if not exists index_orders
ON Orders (Poster, Type, Time, Item, Rate);
CREATE INDEX if not exists index_inbox
ON Inbox (Recipient);
CREATE VIEW if not exists distinct_items AS
SELECT DISTINCT Item FROM Orders;
CREATE VIEW if not exists market_summary AS
SELECT
distinct_items.Item,
(
SELECT sum(Orders.Amount) FROM Orders
WHERE Orders.Item = distinct_items.Item
AND Orders.Type = "buy"
),
(
SELECT max(Orders.Rate) FROM Orders
WHERE Orders.Item = distinct_items.Item
AND Orders.Type = "buy"
),
(
SELECT sum(Orders.Amount) FROM Orders
WHERE Orders.Item = distinct_items.Item
AND Orders.Type = "sell"
),
(
SELECT min(Orders.Rate) FROM Orders
WHERE Orders.Item = distinct_items.Item
AND Orders.Type = "sell"
)
FROM distinct_items;
END TRANSACTION;
]=]
local new_act_query = [=[
INSERT INTO Credit (Owner, Balance)
VALUES (:owner,:start_balance);
]=]
local get_balance_query = [[
SELECT Balance FROM Credit
WHERE Owner = ?;
]]
local set_balance_query = [[
UPDATE Credit
SET Balance = ?
WHERE Owner = ?;
]]
local log_query = [[
INSERT INTO Log (Recipient, Time, Message)
VALUES(?, ?, ?);
]]
local search_desc_query = [=[
SELECT * FROM Orders
WHERE Exchange = :ex_name
AND Type = :order_type
AND Item = :item_name
ORDER BY Rate DESC;
]=]
local add_order_query = [=[
INSERT INTO Orders (Poster, Exchange, Type, Time, Item, Amount, Rate)
VALUES (:p_name, :ex_name, :order_type, :time, :item_name, :amount, :rate);
]=]
local del_order_query = [=[
DELETE FROM Orders
WHERE Id = ?;
]=]
local reduce_order_query = [=[
UPDATE Orders
SET Amount = Amount - ?
WHERE Id = ?;
]=]
-- Delete an order while also checking the player.
local cancel_order_query = [=[
DELETE FROM Orders
WHERE Id = :id
AND Poster = :p_name
]=]
local refund_order_query = [=[
UPDATE Credit
SET Balance = Balance + coalesce((
SELECT sum(Rate * Amount) FROM Orders
WHERE Poster = :p_name
AND Type = "buy"
AND Id = :id
), 0)
WHERE Owner = :p_name;
]=]
local search_asc_query = [=[
SELECT * FROM Orders
WHERE Exchange = :ex_name
AND Type = :order_type
AND Item = :item_name
ORDER BY Rate ASC;
]=]
local search_min_query = [=[
SELECT * FROM Orders
WHERE Exchange = :ex_name
AND Type = :order_type
AND Item = :item_name
AND Rate >= :rate_min
ORDER BY Rate DESC;
]=]
local search_max_query = [=[
SELECT * FROM Orders
WHERE Exchange = :ex_name
AND Type = :order_type
AND Item = :item_name
AND Rate <= :rate_max
ORDER BY Rate ASC;
]=]
local search_own_query = [=[
SELECT * FROM Orders
WHERE Poster = :p_name;
]=]
local insert_inbox_query = [=[
INSERT INTO Inbox(Recipient, Item, Amount)
VALUES(?, ?, ?);
]=]
local view_inbox_query = [=[
SELECT * FROM Inbox
WHERE Recipient = ?;
]=]
local get_inbox_query = [=[
SELECT Amount FROM Inbox
WHERE Id = :id;
]=]
local red_inbox_query = [=[
UPDATE Inbox
SET Amount = Amount - :change
WHERE Id = :id;
]=]
local del_inbox_query = [=[
DELETE FROM Inbox
WHERE Id = :id;
]=]
local summary_query = [=[
SELECT * FROM market_summary;
]=]
local transaction_log_query = [=[
SELECT Time, Message FROM Log
WHERE Recipient = :p_name
ORDER BY Time DESC;
]=]
local ex_methods = {}
local ex_meta = { __index = ex_methods }
local function sql_error(err)
error("SQL error: " .. err)
end
local function is_integer(num)
return num%1 == 0
end
local function exec_stmt(db, stmt, names)
stmt:bind_names(names)
local res = stmt:step()
stmt:reset()
if res == sqlite3.BUSY then
return false, "Database Busy."
elseif res ~= sqlite3.DONE then
sql_error(db:errmsg())
else
return true
end
end
function exports.open_exchange(path)
local db = assert(sqlite3.open(path))
local res = db:exec(init_query)
if res ~= sqlite3.OK then
sql_error(db:errmsg())
end
local stmts = {
new_act_stmt = assert(db:prepare(new_act_query)),
get_balance_stmt = assert(db:prepare(get_balance_query)),
set_balance_stmt = assert(db:prepare(set_balance_query)),
log_stmt = assert(db:prepare(log_query)),
search_desc_stmt = assert(db:prepare(search_desc_query)),
search_asc_stmt = assert(db:prepare(search_asc_query)),
search_min_stmt = assert(db:prepare(search_min_query)),
search_max_stmt = assert(db:prepare(search_max_query)),
search_own_stmt = assert(db:prepare(search_own_query)),
add_order_stmt = assert(db:prepare(add_order_query)),
del_order_stmt = assert(db:prepare(del_order_query)),
reduce_order_stmt = assert(db:prepare(reduce_order_query)),
cancel_order_stmt = assert(db:prepare(cancel_order_query)),
refund_order_stmt = assert(db:prepare(refund_order_query)),
insert_inbox_stmt = assert(db:prepare(insert_inbox_query)),
view_inbox_stmt = assert(db:prepare(view_inbox_query)),
get_inbox_stmt = assert(db:prepare(get_inbox_query)),
red_inbox_stmt = assert(db:prepare(red_inbox_query)),
del_inbox_stmt = assert(db:prepare(del_inbox_query)),
summary_stmt = assert(db:prepare(summary_query)),
transaction_log_stmt = assert(db:prepare(transaction_log_query)),
}
local ret = { db = db,
stmts = stmts,
}
setmetatable(ret, ex_meta)
return ret
end
function ex_methods.close(self)
for k, v in pairs(self.stmts) do
v:finalize()
end
self.db:close()
end
-- Returns success boolean
function ex_methods.log(self, message, recipient)
recipient = recipient or ""
local db = self.db
local stmt = self.stmts.log_stmt
stmt:bind_values(recipient, os.time(), message)
local res = stmt:step()
stmt:reset()
if res == sqlite3.ERROR then
sql_error(db:errmsg())
elseif res == sqlite3.MISUSE then
error("Programmer error.")
elseif res == sqlite3.BUSY then
return false, "Failed to log message."
else
return true
end
end
-- Returns success boolean and error.
function ex_methods.new_account(self, p_name, amt)
local db = self.db
amt = amt or 0
local exists = self:get_balance(p_name)
if exists then
return false, "Account already exists."
end
db:exec("BEGIN TRANSACTION;")
local stmt = self.stmts.new_act_stmt
stmt:bind_names({
owner = p_name,
start_balance = amt,
time = os.time(),
})
local res = stmt:step()
if res == sqlite3.MISUSE then
error("Programmer error.")
elseif res == sqlite3.BUSY then
stmt:reset()
db:exec("ROLLBACK;")
return false, "Database Busy."
elseif res ~= sqlite3.DONE then
sql_error(db:errmsg())
end
stmt:reset()
local log_succ1, log_err1 =
self:log("Account opened with balance " .. amt, p_name)
local log_succ2, log_err2 =
self:log(p_name .. " opened an account with balance " .. amt)
if not log_succ1 then
db:exec("ROLLBACK;")
return false, log_err1
end
if not log_succ2 then
db:exec("ROLLBACK;")
return false, log_err2
end
db:exec("COMMIT;")
return true
end
-- Returns nil if no balance.
function ex_methods.get_balance(self, p_name)
local db = self.db
local stmt = self.stmts.get_balance_stmt
stmt:bind_values(p_name)
local res = stmt:step()
if res == sqlite3.ERROR then
sql_error(db:errmsg())
elseif res == sqlite3.MISUSE then
error("Programmer error.")
elseif res == sqlite3.ROW then
local balance = stmt:get_value(0)
stmt:reset()
return balance
end
stmt:reset()
end
-- Returns success boolean, and error message if false.
function ex_methods.set_balance(self, p_name, new_bal)
local db = self.db
local set_stmt = self.stmts.set_balance_stmt
local bal = self:get_balance(p_name)
if not bal then
return false, p_name .. " does not have an account."
end
set_stmt:bind_values(new_bal, p_name)
local res = set_stmt:step()
if res == sqlite3.ERROR then
sql_error(db:errmsg())
elseif res == sqlite3.MISUSE then
error("Programmer error.")
elseif res == sqlite3.BUSY then
set_stmt:reset()
return false, "Database busy"
else
set_stmt:reset()
return true
end
end
-- Change balance by the given amount. Returns a success boolean, and error
-- message on fail.
function ex_methods.change_balance(self, p_name, delta)
if not is_integer(delta) then
error("Non-integer credit delta")
end
local bal = self:get_balance(p_name)
if not bal then
return false, p_name .. " does not have an account."
end
if bal + delta < 0 then
return false, p_name .. " does not have enough money."
end
return self:set_balance(p_name, bal + delta)
end
-- Sends credits from one user to another. Returns a success boolean, and error
-- message on fail.
function ex_methods.transfer_credits(self, sender, receiver, amt)
local db = self.db
if not is_integer(amt) then
return false, "Non-integer credit amount"
end
db:exec("BEGIN TRANSACTION;")
local succ_minus, err = self:change_balance(sender, -amt)
if not succ_minus then
db:exec("ROLLBACK")
return false, err
end
local succ_plus, err = self:change_balance(receiver, amt)
if not succ_plus then
db:exec("ROLLBACK")
return false, err
end
local succ_log1 = self:log("Sent " .. amt .. " credits to " .. receiver, sender)
if not succ_log1 then
db:exec("ROLLBACK")
return false, "Failed to log sender message"
end
local succ_log2 = self:log("Received " .. amt .. " credits from " .. sender, receiver)
if not succ_log2 then
db:exec("ROLLBACK")
return false, "Failed to log receiver message"
end
db:exec("COMMIT;")
return true
end
function ex_methods.give_credits(self, p_name, amt, msg)
local db = self.db
db:exec("BEGIN TRANSACTION;")
local succ_change, err = self:change_balance(p_name, amt)
if not succ_change then
db:exec("ROLLBACK;")
return false, err
end
local succ_log, err = self:log(msg, p_name)
if not succ_log then
db:exec("ROLLBACK;")
return false, er
end
db:exec("COMMIT;")
return true
end
-- Returns a list of orders, sorted by price.
function ex_methods.search_orders(self, ex_name, order_type, item_name)
local stmt
if order_type == "buy" then
stmt = self.stmts.search_asc_stmt
else
stmt = self.stmts.search_desc_stmt
end
stmt:bind_names({
ex_name = ex_name,
order_type = order_type,
item_name = item_name,
})
local orders,n = {},1
for tab in stmt:nrows() do
orders[n] = tab
n = n+1
end
stmt:reset()
return orders
end
-- Same as above, except not sorted in any particular order.
function ex_methods.search_player_orders(self, p_name)
local stmt = self.stmts.search_own_stmt
stmt:bind_names({p_name = p_name})
local orders,n = {},1
for tab in stmt:nrows() do
orders[n] = tab
n = n+1
end
stmt:reset()
return orders
end
-- Adds a new order. Returns success, and an error string if failed.
function ex_methods.add_order(self, p_name, ex_name, order_type, item_name, amount, rate)
if math.floor(amount) ~= amount then
return false, "Noninteger quantity"
end
if amount <= 0 then
return false, "Nonpositive quantity"
end
if math.floor(rate) ~= rate then
return false, "Noninteger rate"
end
if rate <= 0 then
return false, "Nonpositive rate"
end
local db = self.db
local stmt = self.stmts.add_order_stmt
stmt:bind_names({
p_name = p_name,
ex_name = ex_name,
order_type = order_type,
time = os.time(),
item_name = item_name,
amount = amount,
rate = rate,
})
local res = stmt:step()
if res == sqlite3.BUSY then
stmt:reset()
return false, "Database Busy"
elseif res ~= sqlite3.DONE then
sql_error(db:errmsg())
end
stmt:reset()
return true
end
-- Returns true, or false and an error message.
function ex_methods.cancel_order(self, p_name, id, order_type, item_name, amount, rate)
local params = { p_name = p_name,
id = id,
}
local db = self.db
db:exec("BEGIN TRANSACTION;")
local refund_stmt = self.stmts.refund_order_stmt
local cancel_stmt = self.stmts.cancel_order_stmt
local ref_succ, ref_err = exec_stmt(db, refund_stmt, params)
if not ref_succ then
db:exec("ROLLBACK")
return false, ref_err
end
local canc_succ, canc_err = exec_stmt(db, cancel_stmt, params)
if not canc_succ then
db:exec("ROLLBACK")
return false, canc_err
end
local message = "Cancelled an order to " ..
order_type .. " " .. amount .. " " .. item_name .. "."
if order_type == "buy" then
message = message .. " (+" .. amount * rate .. ")"
end
local succ, err = self:log(message, p_name)
if not succ then
db:exec("ROLLBACK")
return false, err
end
db:exec("COMMIT;")
return true
end
-- Puts things in a player's item inbox. Returns success, and also returns an
-- error message if failed.
function ex_methods.put_in_inbox(self, p_name, item_name, amount)
local db = self.db
local stmt = self.stmts.insert_inbox_stmt
stmt:bind_values(p_name, item_name, amount)
local res = stmt:step()
if res == sqlite3.BUSY then
return false, "Database Busy."
elseif res ~= sqlite3.DONE then
sql_error(db:errmsg())
end
stmt:reset()
return true
end
-- Tries to buy from orders at the provided rate, and posts an offer with any
-- remaining desired amount. Returns success. If succeeded, also returns amount
-- bought. If failed, returns an error message
function ex_methods.buy(self, p_name, ex_name, item_name, amount, rate)
if not is_integer(amount) then
return false, "Noninteger quantity"
elseif amount <= 0 then
return false, "Nonpositive quantity"
elseif not is_integer(rate) then
return false, "Noninteger rate"
elseif rate <= 0 then
return false, "Nonpositive rate"
end
local db = self.db
local bal = self:get_balance(p_name)
if not bal then
return false, "Nonexistent account."
end
if bal < amount * rate then
return false, "Not enough money."
end
db:exec("BEGIN TRANSACTION");
local remaining = amount
local del_stmt = self.stmts.del_order_stmt
local red_stmt = self.stmts.reduce_order_stmt
local search_stmt = self.stmts.search_max_stmt
search_stmt:bind_names({
ex_name = ex_name,
order_type = "sell",
item_name = item_name,
rate_max = rate,
})
for row in search_stmt:nrows() do
local poster = row.Poster
local row_amount = row.Amount
if row_amount <= remaining then
del_stmt:bind_values(row.Id)
local del_res = del_stmt:step()
if del_res == sqlite3.BUSY then
del_stmt:reset()
search_stmt:reset()
db:exec("ROLLBACK;")
return false, "Database Busy."
elseif del_res ~= sqlite3.DONE then
sql_error(db:errmsg())
end
del_stmt:reset()
local ch_succ, ch_err =
self:change_balance(poster, rate * row_amount)
if not ch_succ then
search_stmt:reset()
db:exec("ROLLBACK;")
return false, ch_err
end
local log_succ, log_err =
self:log(p_name .. " bought " .. row_amount .. " "
.. item_name .. " from you. (+"
.. rate * row_amount .. ")", poster)
if not log_succ then
search_stmt:reset()
db:exec("ROLLBACK;")
return false, log_err
end
local log2_succ, log2_err =
self:log("Bought " .. row_amount .. " " .. item_name
.. " from " .. poster
.. "(-" .. rate * row_amount .. ")",p_name)
if not log2_succ then
search_stmt:reset()
db:exec("ROLLBACK;")
return false, log2_err
end
remaining = remaining - row_amount
else -- row_amount > remaining
red_stmt:bind_values(remaining, row.Id)
local red_res = red_stmt:step()
if red_res == sqlite3.BUSY then
red_stmt:reset()
search_stmt:reset()
db:exec("ROLLBACK;")
return false, "Database Busy."
elseif red_res ~= sqlite3.DONE then
red_stmt:reset()
search_stmt:reset()
sql_error(db:errmsg())
end
red_stmt:reset()
local ch_succ, ch_err =
self:change_balance(poster, rate * remaining)
if not ch_succ then
search_stmt:reset()
db:exec("ROLLBACK;")
return false, ch_err
end
local log_succ, log_err =
self:log(p_name .. " bought " .. remaining .. " "
.. item_name .. " from you. (+"
.. rate * remaining .. ")", poster)
if not log_succ then
search_stmt:reset()
db:exec("ROLLBACK;")
return false, log_err
end
local log2_succ, log2_err =
self:log("Bought " .. remaining .. " " .. item_name
.. " from " .. poster .. " (-"
.. rate * remaining .. ")", p_name)
if not log2_succ then
search_stmt:reset()
db:exec("ROLLBACK;")
return false, log2_err
end
remaining = 0
end
if remaining == 0 then break end
end
search_stmt:reset()
local bought = amount - remaining
local cost = amount * rate
local ch_succ, ch_err = self:change_balance(p_name, -cost)
if not ch_succ then
db:exec("ROLLBACK;")
return false, ch_err
end
if remaining > 0 then
local add_succ, add_err =
self:add_order(p_name, ex_name, "buy", item_name, remaining, rate)
if not add_succ then
db:exec("ROLLBACK;")
return false, add_err
end
local log_succ, log_err =
self:log("Posted buy offer for "
.. remaining .. " " .. item_name .. " at "
.. rate .. "/item (-"
.. remaining * rate .. ")", p_name)
if not log_succ then
db:exec("ROLLBACK;")
return false, log_err
end
end
db:exec("COMMIT;")
return true, bought
end
-- Tries to sell to orders at the provided rate, and posts an offer with any
-- remaining desired amount. Returns success. If failed, returns an error message.
function ex_methods.sell(self, p_name, ex_name, item_name, amount, rate)
if not is_integer(amount) then
return false, "Noninteger quantity"
elseif amount <= 0 then
return false, "Nonpositive quantity"
elseif not is_integer(rate) then
return false, "Noninteger rate"
elseif rate <= 0 then
return false, "Nonpositive rate"
end
local db = self.db
db:exec("BEGIN TRANSACTION");
local remaining = amount
local revenue = 0
local del_stmt = self.stmts.del_order_stmt
local red_stmt = self.stmts.reduce_order_stmt
local search_stmt = self.stmts.search_min_stmt
search_stmt:bind_names({
ex_name = ex_name,
order_type = "buy",
item_name = item_name,
rate_min = rate,
})
for row in search_stmt:nrows() do
local poster = row.Poster
local row_amount = row.Amount
if row_amount <= remaining then
del_stmt:bind_values(row.Id)
local del_res = del_stmt:step()
if del_res == sqlite3.BUSY then
del_stmt:reset()
search_stmt:reset()
db:exec("ROLLBACK;")
return false, "Database Busy."
elseif del_res ~= sqlite3.DONE then
sql_error(db:errmsg())
end
del_stmt:reset()
local in_succ, in_err =
self:put_in_inbox(poster, item_name, row_amount)
if not in_succ then
search_stmt:reset()
db:exec("ROLLBACK;")
return false, in_err
end
local log_succ, log_err =
self:log(p_name .. " sold " .. row_amount .. " "
.. item_name .. " to you." , poster)
if not log_succ then
search_stmt:reset()
db:exec("ROLLBACK;")
return false, log_err
end
local log2_succ, log2_err =
self:log("Sold " .. row_amount .. " " .. item_name
.. " to " .. poster
.. "(+" .. rate * row_amount .. ")",p_name)
if not log2_succ then
search_stmt:reset()
db:exec("ROLLBACK;")
return false, log2_err
end
remaining = remaining - row_amount
revenue = revenue + row_amount * row.Rate
else -- row_amount > remaining
red_stmt:bind_values(remaining, row.Id)
local red_res = red_stmt:step()
if red_res == sqlite3.BUSY then
red_stmt:reset()
search_stmt:reset()
db:exec("ROLLBACK;")
return false, "Database Busy."
elseif red_res ~= sqlite3.DONE then
red_stmt:reset()
search_stmt:reset()
sql_error(db:errmsg())
end
red_stmt:reset()
local in_succ, in_err =
self:put_in_inbox(poster, item_name, remaining)
if not in_succ then
search_stmt:reset()
db:exec("ROLLBACK;")
return false, in_err
end
local log_succ, log_err =
self:log(p_name .. " sold " .. remaining .. " "
.. item_name .. " to you.", poster)
if not log_succ then
search_stmt:reset()
db:exec("ROLLBACK;")
return false, log_err
end
local log2_succ, log2_err =
self:log("Sold " .. row_amount .. " " .. item_name
.. " to " .. poster .. " (+"
.. rate * remaining .. ")", p_name)
if not log2_succ then
search_stmt:reset()
db:exec("ROLLBACK;")
return false, log2_err
end
revenue = revenue + remaining * row.Rate
remaining = 0
end
if remaining == 0 then break end
end
search_stmt:reset()
local ch_succ, ch_err = self:change_balance(p_name, revenue)
if not ch_succ then
db:exec("ROLLBACK;")
return false, ch_err
end
if remaining > 0 then
local add_succ, add_err =
self:add_order(p_name, ex_name, "sell", item_name, remaining, rate)
if not add_succ then
db:exec("ROLLBACK;")
return false, add_err
end
end
db:exec("COMMIT;")
return true
end
-- On success, returns true and a list of inbox entries.
-- TODO: On failure, return false and an error message.
function ex_methods.view_inbox(self, p_name)
local stmt = self.stmts.view_inbox_stmt
stmt:bind_values(p_name)
local res,n = {},1
for row in stmt:nrows() do
res[n] = row
n = n+1
end
stmt:reset()
return true, res
end
-- Returns success boolean. On success, also returns the number actually
-- taken. On failure, also returns an error message
function ex_methods.take_inbox(self, id, amount)
local db = self.db
local get_stmt = self.stmts.get_inbox_stmt
local red_stmt = self.stmts.red_inbox_stmt
local del_stmt = self.stmts.del_inbox_stmt
get_stmt:bind_names({
id = id,
change = amount
})
local res = get_stmt:step()
if res == sqlite3.BUSY then
get_stmt:reset()
return false, "Database Busy."
elseif res == sqlite3.DONE then
get_stmt:reset()
return false, "Order does not exist."
elseif res ~= sqlite3.ROW then
sql_error(db:errmsg())
end
local available = get_stmt:get_value(0)
get_stmt:reset()
db:exec("BEGIN TRANSACTION;")
if available > amount then
red_stmt:bind_names({
id = id,
change = amount
})
local red_res = red_stmt:step()
if red_res == sqlite3.BUSY then
red_stmt:reset()
db:exec("ROLLBACK;")
return false, "Database Busy."
elseif red_res ~= sqlite3.DONE then
sql_error(db:errmsg())
end
red_stmt:reset()
else
del_stmt:bind_names({
id = id,
})
local del_res = del_stmt:step()
if del_res == sqlite3.BUSY then
del_stmt:reset()
db:exec("ROLLBACK;")
return false, "Database Busy."
elseif del_res ~= sqlite3.DONE then
sql_error(db:errmsg())
end
del_stmt:reset()
end
db:exec("COMMIT;")
return true, math.min(amount, available)
end
-- Returns a list of tables with fields:
-- item_name: Name of the item
-- buy_volume: Number of items sought
-- buy_max: Maximum buy rate
-- sell_volume: Number of items for sale
-- sell_min: Minimum sell rate
function ex_methods.market_summary(self)
local stmt = self.stmts.summary_stmt
local res,n = {},1
for a in stmt:rows() do
res[n] = {
item_name = a[1],
buy_volume = a[2],
buy_max = a[3],
sell_volume = a[4],
sell_min = a[5],
}
n = n+1
end
stmt:reset()
return res
end
-- Returns a list of log entries, sorted by time.
function ex_methods.player_log(self, p_name)
local stmt = self.stmts.transaction_log_stmt
stmt:bind_names({ p_name = p_name })
local res,n = {},1
for row in stmt:nrows() do
res[n] = row
n = n+1
end
stmt:reset()
return res
end
function exports.test()
local ex = exports.open_exchange("test.db")
local alice_bal = ex:get_balance("Alice")
local bob_bal = ex:get_balance("Bob")
local function print_balances()
print("Alice: ", ex:get_balance("Alice"))
print("Bob: ", ex:get_balance("Bob"))
end
-- Initialize balances
if alice_bal then
ex:set_balance("Alice", 420)
else
ex:new_account("Alice", 420)
end
if bob_bal then
ex:set_balance("Bob", 2015)
else
ex:new_account("Bob", 2015)
end
print_balances()
-- Transfer a valid amount
print("Transfering 1000 credits from Bob to Alice")
local succ, err = ex:transfer_credits("Bob", "Alice", 1000)
print("Success: ", succ, " ", err)
print_balances()
-- Transfer an invalid amount
print("Transfering 3000 credits from Alice to Bob")
local succ, err = ex:transfer_credits("Alice", "Bob", 3000)
print("Success: ", succ, " ", err)
print_balances()
-- Simulate a transaction
print("Alice posting an offer to buy 10 cobble at 2 credits each")
local succ, err = ex:buy("Alice", "", "default:cobble", 10, 2)
print("Success: ", succ, " ", err)
print_balances()
print("Bob posting an offer to sell 20 cobble at 1 credits each")
local succ, err = ex:sell("Bob", "", "default:cobble", 20, 1)
print("Success: ", succ, " ", err)
print_balances()
ex:close()
end
return exports