forked from minetest-mods/global_exchange
1216 lines
25 KiB
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
|