Electronic trading exchange in Minetest
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1361 lines
29 KiB

  1. local insecure_env = ...
  2. local sql = insecure_env.require("lsqlite3")
  3. local exports = {}
  4. local S = minetest.get_translator("global_exchange")
  5. local order_book_cache = (function(cache)
  6. return function(ex_name)
  7. local maybe_cache = cache[ex_name]
  8. if not maybe_cache then
  9. maybe_cache = {}
  10. cache[ex_name] = maybe_cache
  11. end
  12. return maybe_cache
  13. end
  14. end)({})
  15. local init_query = [=[
  16. BEGIN TRANSACTION;
  17. CREATE TABLE IF NOT EXISTS Credit
  18. (
  19. Owner TEXT PRIMARY KEY NOT NULL,
  20. Balance INTEGER NOT NULL
  21. );
  22. CREATE TABLE IF NOT EXISTS Log
  23. (
  24. Id INTEGER PRIMARY KEY AUTOINCREMENT,
  25. Recipient TEXT NOT NULL,
  26. Time INTEGER NOT NULL,
  27. Message TEXT NOT NULL
  28. );
  29. CREATE TABLE IF NOT EXISTS Orders
  30. (
  31. Id INTEGER PRIMARY KEY AUTOINCREMENT,
  32. Poster TEXT NOT NULL,
  33. Exchange TEXT NOT NULL,
  34. Type TEXT NOT NULL CHECK(Type IN ("buy", "sell")),
  35. Time INTEGER NOT NULL,
  36. Item TEXT NOT NULL,
  37. Wear INTEGER NOT NULL CHECK(Wear >= 0 AND Wear <= 65535),
  38. Amount INTEGER NOT NULL CHECK(Amount > 0),
  39. Rate INTEGER NOT NULL CHECK(Rate > 0)
  40. );
  41. CREATE TABLE IF NOT EXISTS Inbox
  42. (
  43. Id INTEGER PRIMARY KEY AUTOINCREMENT,
  44. Recipient TEXT NOT NULL,
  45. Item TEXT NOT NULL,
  46. Wear INTEGER NOT NULL CHECK(Wear >= 0 AND Wear <= 65535),
  47. Amount INTEGER NOT NULL CHECK(Amount > 0)
  48. );
  49. CREATE INDEX IF NOT EXISTS index_log
  50. ON Log (Recipient, Time);
  51. CREATE INDEX IF NOT EXISTS index_orders
  52. ON Orders (Exchange, Type, Item, Rate, Wear, Time);
  53. CREATE INDEX IF NOT EXISTS index_own_orders
  54. ON Orders (Poster, Time);
  55. CREATE INDEX IF NOT EXISTS index_inbox
  56. ON Inbox (Recipient, Item, Wear);
  57. CREATE VIEW IF NOT EXISTS distinct_items AS
  58. SELECT DISTINCT Item, Wear FROM Orders;
  59. CREATE VIEW IF NOT EXISTS market_summary AS
  60. SELECT
  61. distinct_items.Item AS Item,
  62. distinct_items.Wear AS Wear,
  63. (
  64. SELECT SUM(Orders.Amount) FROM Orders
  65. WHERE Orders.Item = distinct_items.Item
  66. AND Orders.Wear >= distinct_items.Wear
  67. AND Orders.Type = "buy"
  68. ) AS Buy_Volume,
  69. (
  70. SELECT MAX(Orders.Rate) FROM Orders
  71. WHERE Orders.Item = distinct_items.Item
  72. AND Orders.Wear >= distinct_items.Wear
  73. AND Orders.Type = "buy"
  74. ) AS Buy_Max,
  75. (
  76. SELECT SUM(Orders.Amount) FROM Orders
  77. WHERE Orders.Item = distinct_items.Item
  78. AND Orders.Wear <= distinct_items.Wear
  79. AND Orders.Type = "sell"
  80. ) AS Sell_Volume,
  81. (
  82. SELECT MIN(Orders.Rate) FROM Orders
  83. WHERE Orders.Item = distinct_items.Item
  84. AND Orders.Wear <= distinct_items.Wear
  85. AND Orders.Type = "sell"
  86. ) AS Sell_Min
  87. FROM distinct_items;
  88. END TRANSACTION;
  89. ]=]
  90. local new_act_query = [=[
  91. INSERT INTO Credit (Owner, Balance)
  92. VALUES (:owner, :start_balance);
  93. ]=]
  94. local get_balance_query = [[
  95. SELECT Balance FROM Credit
  96. WHERE Owner = ?;
  97. ]]
  98. local set_balance_query = [[
  99. UPDATE Credit
  100. SET Balance = :new_balance
  101. WHERE Owner = :p_name;
  102. ]]
  103. local log_query = [[
  104. INSERT INTO Log (Recipient, Time, Message)
  105. VALUES(:recipient, :time, :message);
  106. ]]
  107. local add_order_query = [=[
  108. INSERT INTO Orders (Poster, Exchange, Type, Time, Item, Wear, Amount, Rate)
  109. VALUES (:p_name, :ex_name, :order_type, :time, :item_name, :wear, :amount, :rate);
  110. ]=]
  111. local del_order_query = [=[
  112. DELETE FROM Orders
  113. WHERE Id = ?;
  114. ]=]
  115. local reduce_order_query = [=[
  116. UPDATE Orders
  117. SET Amount = Amount - :delta
  118. WHERE Id = :id;
  119. ]=]
  120. local get_order_query = [=[
  121. SELECT * FROM Orders
  122. WHERE Id = ?
  123. ]=]
  124. local cancel_order_query = [=[
  125. DELETE FROM Orders
  126. WHERE Id = :id
  127. AND Poster = :p_name
  128. ]=]
  129. local search_bids_query = [=[
  130. SELECT * FROM Orders
  131. WHERE Exchange = :ex_name
  132. AND Type = "buy"
  133. AND Item = :item_name
  134. ORDER BY Rate ASC, Wear DESC;
  135. ]=]
  136. local search_asks_query = [=[
  137. SELECT * FROM Orders
  138. WHERE Exchange = :ex_name
  139. AND Type = "sell"
  140. AND Item = :item_name
  141. ORDER BY Rate DESC, Wear ASC;
  142. ]=]
  143. local qual_bids_query = [=[
  144. SELECT * FROM Orders
  145. WHERE Exchange = :ex_name
  146. AND Type = "buy"
  147. AND Item = :item_name
  148. AND Rate >= :rate_min
  149. AND Wear >= :wear_min
  150. ORDER BY Rate DESC, Time ASC;
  151. ]=]
  152. local qual_asks_query = [=[
  153. SELECT * FROM Orders
  154. WHERE Exchange = :ex_name
  155. AND Type = "sell"
  156. AND Item = :item_name
  157. AND Rate <= :rate_max
  158. AND Wear <= :wear_max
  159. ORDER BY Rate ASC, Time ASC;
  160. ]=]
  161. local search_own_query = [=[
  162. SELECT * FROM Orders
  163. WHERE Poster = ?
  164. ORDER BY Time ASC;
  165. ]=]
  166. local order_book_asks_query = [=[
  167. SELECT Type, Rate, Wear, SUM(Amount) AS Amount FROM Orders
  168. GROUP BY Rate, Wear
  169. HAVING Exchange = :ex_name
  170. AND Type = "sell"
  171. AND Item = :item_name
  172. ORDER BY Rate ASC, Wear ASC
  173. LIMIT 3;
  174. ]=]
  175. local order_book_bids_query = [=[
  176. SELECT Type, Rate, Wear, SUM(Amount) AS Amount FROM Orders
  177. GROUP BY Rate, Wear
  178. HAVING Exchange = :ex_name
  179. AND Type = "buy"
  180. AND Item = :item_name
  181. ORDER BY Rate DESC, Wear DESC
  182. LIMIT 3;
  183. ]=]
  184. local insert_inbox_query = [=[
  185. INSERT INTO Inbox(Recipient, Item, Wear, Amount)
  186. VALUES(:p_name, :item_name, :wear, :amount);
  187. ]=]
  188. local add_inbox_query = [=[
  189. UPDATE Inbox
  190. SET Amount = Amount + :change
  191. WHERE Id = :id;
  192. ]=]
  193. local view_inbox_query = [=[
  194. SELECT * FROM Inbox
  195. WHERE Recipient = ?
  196. ORDER BY Item ASC, Wear ASC;
  197. ]=]
  198. local search_inbox_query = [=[
  199. SELECT * FROM Inbox
  200. WHERE Recipient = :p_name
  201. AND Item = :item_name
  202. AND Wear = :wear;
  203. ]=]
  204. local get_inbox_query = [=[
  205. SELECT Amount, Wear FROM Inbox
  206. WHERE Id = :id;
  207. ]=]
  208. local red_inbox_query = [=[
  209. UPDATE Inbox
  210. SET Amount = Amount - :change
  211. WHERE Id = :id;
  212. ]=]
  213. local del_inbox_query = [=[
  214. DELETE FROM Inbox
  215. WHERE Id = :id;
  216. ]=]
  217. local summary_query = [=[
  218. SELECT * FROM market_summary;
  219. ]=]
  220. local transaction_log_query = [=[
  221. SELECT Time, Message FROM Log
  222. WHERE Recipient = ?
  223. ORDER BY Time DESC;
  224. ]=]
  225. local ex_methods = {}
  226. local ex_meta = { __index = ex_methods }
  227. local function sql_error(err)
  228. error("SQL error: " .. err)
  229. end
  230. local function is_integer(x)
  231. local num = tonumber(x)
  232. return num and math.floor(num) == num
  233. end
  234. local function exec_stmt(db, stmt, names)
  235. stmt:bind_names(names)
  236. local res = stmt:step()
  237. stmt:reset()
  238. if res == sqlite3.BUSY then
  239. return false, S("Database Busy.")
  240. elseif res ~= sqlite3.DONE then
  241. sql_error(db:errmsg())
  242. else
  243. return true
  244. end
  245. end
  246. function exports.open_exchange(path)
  247. local db = assert(sqlite3.open(path))
  248. local res = db:exec(init_query)
  249. if res ~= sqlite3.OK then
  250. sql_error(db:errmsg())
  251. end
  252. local stmts = {
  253. new_act_stmt = assert(db:prepare(new_act_query)),
  254. get_balance_stmt = assert(db:prepare(get_balance_query)),
  255. set_balance_stmt = assert(db:prepare(set_balance_query)),
  256. log_stmt = assert(db:prepare(log_query)),
  257. search_asks_stmt = assert(db:prepare(search_asks_query)),
  258. search_bids_stmt = assert(db:prepare(search_bids_query)),
  259. qual_asks_stmt = assert(db:prepare(qual_asks_query)),
  260. qual_bids_stmt = assert(db:prepare(qual_bids_query)),
  261. search_own_stmt = assert(db:prepare(search_own_query)),
  262. add_order_stmt = assert(db:prepare(add_order_query)),
  263. get_order_stmt = assert(db:prepare(get_order_query)),
  264. del_order_stmt = assert(db:prepare(del_order_query)),
  265. reduce_order_stmt = assert(db:prepare(reduce_order_query)),
  266. cancel_order_stmt = assert(db:prepare(cancel_order_query)),
  267. insert_inbox_stmt = assert(db:prepare(insert_inbox_query)),
  268. add_inbox_stmt = assert(db:prepare(add_inbox_query)),
  269. order_book_bids_stmt = assert(db:prepare(order_book_bids_query)),
  270. order_book_asks_stmt = assert(db:prepare(order_book_asks_query)),
  271. view_inbox_stmt = assert(db:prepare(view_inbox_query)),
  272. search_inbox_stmt = assert(db:prepare(search_inbox_query)),
  273. get_inbox_stmt = assert(db:prepare(get_inbox_query)),
  274. red_inbox_stmt = assert(db:prepare(red_inbox_query)),
  275. del_inbox_stmt = assert(db:prepare(del_inbox_query)),
  276. summary_stmt = assert(db:prepare(summary_query)),
  277. transaction_log_stmt = assert(db:prepare(transaction_log_query)),
  278. }
  279. local ret = {
  280. db = db,
  281. stmts = stmts,
  282. }
  283. setmetatable(ret, ex_meta)
  284. return ret
  285. end
  286. function ex_methods.close(self)
  287. for k, v in pairs(self.stmts) do
  288. v:finalize()
  289. end
  290. self.db:close()
  291. end
  292. -- Returns success boolean
  293. function ex_methods.log(self, message, recipient)
  294. recipient = recipient or ""
  295. local db = self.db
  296. local stmt = self.stmts.log_stmt
  297. stmt:bind_names({
  298. recipient = recipient,
  299. time = os.time(),
  300. message = message,
  301. })
  302. local res = stmt:step()
  303. stmt:reset()
  304. if res == sqlite3.ERROR then
  305. sql_error(db:errmsg())
  306. elseif res == sqlite3.MISUSE then
  307. error(S("Programmer error."))
  308. elseif res == sqlite3.BUSY then
  309. return false, S("Failed to log message.")
  310. else
  311. return true
  312. end
  313. end
  314. -- Returns success boolean and error.
  315. function ex_methods.new_account(self, p_name, amt)
  316. local db = self.db
  317. amt = amt or 0
  318. local exists = self:get_balance(p_name)
  319. if exists then
  320. return false, S("Account already exists.")
  321. end
  322. db:exec("BEGIN TRANSACTION;")
  323. local stmt = self.stmts.new_act_stmt
  324. stmt:bind_names({
  325. owner = p_name,
  326. start_balance = amt,
  327. time = os.time(),
  328. })
  329. local res = stmt:step()
  330. if res == sqlite3.MISUSE then
  331. error(S("Programmer error."))
  332. elseif res == sqlite3.BUSY then
  333. stmt:reset()
  334. db:exec("ROLLBACK;")
  335. return false, S("Database Busy.")
  336. elseif res ~= sqlite3.DONE then
  337. sql_error(db:errmsg())
  338. end
  339. stmt:reset()
  340. local log_succ1, log_err1 =
  341. self:log("Account opened with balance " .. amt, p_name)
  342. local log_succ2, log_err2 =
  343. self:log(p_name .. " opened an account with balance " .. amt)
  344. if not log_succ1 then
  345. db:exec("ROLLBACK;")
  346. return false, log_err1
  347. end
  348. if not log_succ2 then
  349. db:exec("ROLLBACK;")
  350. return false, log_err2
  351. end
  352. db:exec("COMMIT;")
  353. return true
  354. end
  355. -- Returns nil if no balance.
  356. function ex_methods.get_balance(self, p_name)
  357. local db = self.db
  358. local stmt = self.stmts.get_balance_stmt
  359. stmt:bind_values(p_name)
  360. local res = stmt:step()
  361. if res == sqlite3.ERROR then
  362. sql_error(db:errmsg())
  363. elseif res == sqlite3.MISUSE then
  364. error(S("Programmer error."))
  365. elseif res == sqlite3.ROW then
  366. local balance = stmt:get_value(0)
  367. stmt:reset()
  368. return balance
  369. end
  370. stmt:reset()
  371. return nil
  372. end
  373. -- Returns success boolean, and error message if false.
  374. function ex_methods.set_balance(self, p_name, new_bal)
  375. local db = self.db
  376. local set_stmt = self.stmts.set_balance_stmt
  377. local bal = self:get_balance(p_name)
  378. if not bal then
  379. return false, S("@1 does not have an account.", p_name)
  380. end
  381. set_stmt:bind_names({
  382. p_name = p_name,
  383. new_balance = new_bal,
  384. })
  385. local res = set_stmt:step()
  386. if res == sqlite3.ERROR then
  387. sql_error(db:errmsg())
  388. elseif res == sqlite3.MISUSE then
  389. error(S("Programmer error."))
  390. elseif res == sqlite3.BUSY then
  391. set_stmt:reset()
  392. return false, S("Database busy.")
  393. else
  394. set_stmt:reset()
  395. return true
  396. end
  397. end
  398. -- Change balance by the given amount. Returns a success boolean, and error
  399. -- message on fail.
  400. function ex_methods.change_balance(self, p_name, delta)
  401. if not is_integer(delta) then
  402. error("Non-integer credit delta")
  403. end
  404. local bal = self:get_balance(p_name)
  405. if not bal then
  406. return false, S("@1 does not have an account.", p_name)
  407. end
  408. if bal + delta < 0 then
  409. return false, S("@1 does not have enough money.", p_name)
  410. end
  411. return self:set_balance(p_name, bal + delta)
  412. end
  413. -- Sends credits from one user to another. Returns a success boolean, and error
  414. -- message on fail.
  415. function ex_methods.transfer_credits(self, sender, receiver, amt)
  416. local db = self.db
  417. if not is_integer(amt) then
  418. return false, S("Non-integer credit amount")
  419. end
  420. db:exec("BEGIN TRANSACTION;")
  421. local succ_minus, err = self:change_balance(sender, -amt)
  422. if not succ_minus then
  423. db:exec("ROLLBACK")
  424. return false, err
  425. end
  426. local succ_plus, err = self:change_balance(receiver, amt)
  427. if not succ_plus then
  428. db:exec("ROLLBACK")
  429. return false, err
  430. end
  431. local succ_log1 = self:log("Sent " .. amt .. " credits to " .. receiver, sender)
  432. if not succ_log1 then
  433. db:exec("ROLLBACK")
  434. return false, S("Failed to log sender message")
  435. end
  436. local succ_log2 = self:log("Received " .. amt .. " credits from " .. sender, receiver)
  437. if not succ_log2 then
  438. db:exec("ROLLBACK")
  439. return false, S("Failed to log receiver message")
  440. end
  441. db:exec("COMMIT;")
  442. return true
  443. end
  444. function ex_methods.give_credits(self, p_name, amt, msg)
  445. local db = self.db
  446. db:exec("BEGIN TRANSACTION;")
  447. local succ_change, err = self:change_balance(p_name, amt)
  448. if not succ_change then
  449. db:exec("ROLLBACK;")
  450. return false, err
  451. end
  452. local succ_log, err = self:log(msg, p_name)
  453. if not succ_log then
  454. db:exec("ROLLBACK;")
  455. return false, er
  456. end
  457. db:exec("COMMIT;")
  458. return true
  459. end
  460. -- The best asks & bids for an item, grouped and sorted by rate and wear.
  461. -- Result fields: Type, Rate, Wear, Amount.
  462. function ex_methods.order_book(self, ex_name, item_name)
  463. if not ex_name or not item_name then return {} end
  464. local ex_cache = order_book_cache(ex_name)
  465. if ex_cache[item_name] then
  466. return ex_cache[item_name]
  467. end
  468. local res = {}
  469. local stmt = self.stmts.order_book_asks_stmt
  470. stmt:bind_names({
  471. ex_name = ex_name,
  472. item_name = item_name,
  473. })
  474. -- Insert asks in reverse order
  475. for row in stmt:nrows() do
  476. table.insert(res, 1, row)
  477. end
  478. stmt:reset()
  479. local stmt = self.stmts.order_book_bids_stmt
  480. stmt:bind_names({
  481. ex_name = ex_name,
  482. item_name = item_name,
  483. })
  484. -- Append bids in normal order
  485. for row in stmt:nrows() do
  486. table.insert(res, row)
  487. end
  488. stmt:reset()
  489. ex_cache[item_name] = res
  490. return res
  491. end
  492. -- Returns a list of orders, sorted by price.
  493. function ex_methods.search_orders(self, ex_name, order_type, item_name)
  494. local stmt
  495. if order_type == "buy" then
  496. stmt = self.stmts.search_bids_stmt
  497. else
  498. stmt = self.stmts.search_asks_stmt
  499. end
  500. stmt:bind_names({
  501. ex_name = ex_name,
  502. item_name = item_name,
  503. })
  504. local orders = {}
  505. for tab in stmt:nrows() do
  506. table.insert(orders, tab)
  507. end
  508. stmt:reset()
  509. return orders
  510. end
  511. -- Same as above, except not sorted in any particular order.
  512. function ex_methods.search_player_orders(self, p_name)
  513. local stmt = self.stmts.search_own_stmt
  514. stmt:bind_values(p_name)
  515. local orders = {}
  516. for tab in stmt:nrows() do
  517. table.insert(orders, tab)
  518. end
  519. stmt:reset()
  520. return orders
  521. end
  522. -- Adds a new order. Returns success, and an error string if failed.
  523. function ex_methods.add_order(self, p_name, ex_name, order_type, item_name, wear, amount, rate)
  524. if not is_integer(amount) then
  525. return false, S("Noninteger quantity")
  526. elseif amount <= 0 then
  527. return false, S("Nonpositive quantity")
  528. elseif not is_integer(rate) then
  529. return false, S("Noninteger rate")
  530. elseif rate <= 0 then
  531. return false, S("Nonpositive rate")
  532. elseif not is_integer(wear) then
  533. return false, S("Noninteger wear")
  534. elseif wear < 0 or wear > 65535 then
  535. return false, S("Invalid wear")
  536. end
  537. order_book_cache(ex_name)[item_name] = nil
  538. local db = self.db
  539. local stmt = self.stmts.add_order_stmt
  540. stmt:bind_names({
  541. p_name = p_name,
  542. ex_name = ex_name,
  543. order_type = order_type,
  544. time = os.time(),
  545. item_name = item_name,
  546. wear = wear,
  547. amount = amount,
  548. rate = rate,
  549. })
  550. local res = stmt:step()
  551. if res == sqlite3.BUSY then
  552. stmt:reset()
  553. return false, S("Database Busy.")
  554. elseif res ~= sqlite3.DONE then
  555. sql_error(db:errmsg())
  556. end
  557. stmt:reset()
  558. return true
  559. end
  560. -- Returns true, or false and an error message.
  561. function ex_methods.cancel_order(self, p_name, id)
  562. local params = {
  563. p_name = p_name,
  564. id = id,
  565. }
  566. local db = self.db
  567. db:exec("BEGIN TRANSACTION;")
  568. local get_stmt = self.stmts.get_order_stmt
  569. get_stmt:bind_values(id)
  570. local res = get_stmt:step()
  571. local order
  572. if res == sqlite3.ERROR then
  573. sql_error(db:errmsg())
  574. elseif res == sqlite3.MISUSE then
  575. error(S("Programmer error."))
  576. elseif res == sqlite3.ROW then
  577. order = get_stmt:get_named_values()
  578. get_stmt:reset()
  579. else
  580. db:exec("ROLLBACK;")
  581. return false, S("No such order.")
  582. end
  583. order_book_cache(order.Exchange)[order.Item] = nil
  584. local cancel_stmt = self.stmts.cancel_order_stmt
  585. local canc_succ, canc_err = exec_stmt(db, cancel_stmt, params)
  586. if not canc_succ then
  587. db:exec("ROLLBACK;")
  588. return false, canc_err
  589. end
  590. local message = "Cancelled an order to " ..
  591. order.Type .. " " .. order.Amount .. " " .. order.Item .. "."
  592. local succ, err = self:log(message, p_name)
  593. if not succ then
  594. db:exec("ROLLBACK")
  595. return false, err
  596. end
  597. db:exec("COMMIT;")
  598. return true, order
  599. end
  600. -- Puts things in a player's item inbox. Returns success, and also returns an
  601. -- error message if failed.
  602. function ex_methods.put_in_inbox(self, p_name, item_name, wear, amount)
  603. local db = self.db
  604. local search_stmt = self.stmts.search_inbox_stmt
  605. db:exec("BEGIN TRANSACTION;")
  606. search_stmt:bind_names({
  607. p_name = p_name,
  608. item_name = item_name,
  609. wear = wear,
  610. })
  611. local res = search_stmt:step()
  612. local row = nil
  613. if res == sqlite3.BUSY then
  614. search_stmt:reset()
  615. db:exec("ROLLBACK;")
  616. return false, S("Database Busy.")
  617. elseif res == sqlite3.ROW then
  618. row = search_stmt:get_named_values()
  619. elseif res ~= sqlite3.DONE then
  620. sql_error(db:errmsg())
  621. end
  622. search_stmt:reset()
  623. local stmt
  624. if row then
  625. stmt = self.stmts.add_inbox_stmt
  626. stmt:bind_names({
  627. id = row.Id,
  628. change = amount,
  629. })
  630. else
  631. stmt = self.stmts.insert_inbox_stmt
  632. stmt:bind_names({
  633. p_name = p_name,
  634. item_name = item_name,
  635. wear = wear,
  636. amount = amount,
  637. })
  638. end
  639. local res = stmt:step()
  640. if res == sqlite3.BUSY then
  641. stmt:reset()
  642. db:exec("ROLLBACK;")
  643. return false, S("Database Busy.")
  644. elseif res ~= sqlite3.DONE then
  645. sql_error(db:errmsg())
  646. end
  647. stmt:reset()
  648. db:exec("COMMIT;")
  649. return true
  650. end
  651. -- Tries to buy from orders at the provided rate, and posts an offer with any
  652. -- remaining desired amount. Returns success. If succeeded, also returns amount
  653. -- bought. If failed, returns an error message
  654. function ex_methods.buy(self, p_name, ex_name, item_name, wear, amount, rate)
  655. if not is_integer(amount) then
  656. return false, S("Noninteger quantity")
  657. elseif amount <= 0 then
  658. return false, S("Nonpositive quantity")
  659. elseif not is_integer(rate) then
  660. return false, S("Noninteger rate")
  661. elseif rate <= 0 then
  662. return false, S("Nonpositive rate")
  663. elseif not is_integer(wear) then
  664. return false, S("Noninteger wear")
  665. elseif wear < 0 or wear > 65535 then
  666. return false, S("Invalid wear")
  667. end
  668. local db = self.db
  669. db:exec("BEGIN TRANSACTION");
  670. local balance = self:get_balance(p_name)
  671. if not balance then
  672. db:exec("ROLLBACK;")
  673. return false, S("@1 does not have an account.", p_name)
  674. end
  675. local bought = {}
  676. local remaining = amount
  677. local out_of_funds = false
  678. local last_row_rate = rate
  679. local del_stmt = self.stmts.del_order_stmt
  680. local red_stmt = self.stmts.reduce_order_stmt
  681. local search_stmt = self.stmts.qual_asks_stmt
  682. search_stmt:bind_names({
  683. ex_name = ex_name,
  684. item_name = item_name,
  685. rate_max = rate,
  686. wear_max = wear,
  687. })
  688. for row in search_stmt:nrows() do
  689. local poster = row.Poster
  690. local row_wear = row.Wear
  691. local row_amount = row.Amount
  692. local row_rate = row.Rate
  693. local row_bought = math.min(row_amount, remaining)
  694. if poster ~= p_name then
  695. local can_afford = math.floor(balance / row_rate)
  696. last_row_rate = row_rate
  697. out_of_funds = can_afford < row_bought
  698. row_bought = math.min(row_bought, can_afford)
  699. -- asking prices can only increase from here
  700. if row_bought == 0 then break end
  701. end
  702. local red_del_stmt
  703. if row_bought < row_amount then
  704. red_stmt:bind_names({
  705. id = row.Id,
  706. delta = row_bought,
  707. })
  708. red_del_stmt = red_stmt
  709. else -- row_bought == row_amount
  710. del_stmt:bind_values(row.Id)
  711. red_del_stmt = del_stmt
  712. end
  713. local red_del_res = red_del_stmt:step()
  714. if red_del_res == sqlite3.BUSY then
  715. red_del_stmt:reset()
  716. search_stmt:reset()
  717. db:exec("ROLLBACK;")
  718. return false, S("Database Busy.")
  719. elseif red_del_res ~= sqlite3.DONE then
  720. red_del_stmt:reset()
  721. search_stmt:reset()
  722. sql_error(db:errmsg())
  723. end
  724. red_del_stmt:reset()
  725. if poster ~= p_name then
  726. local cost = row_rate * row_bought
  727. local ch_succ, ch_err = self:change_balance(poster, cost)
  728. if not ch_succ then
  729. search_stmt:reset()
  730. db:exec("ROLLBACK;")
  731. return false, ch_err
  732. end
  733. local ch_succ, ch_err = self:change_balance(p_name, -cost)
  734. if not ch_succ then
  735. search_stmt:reset()
  736. db:exec("ROLLBACK;")
  737. return false, ch_err
  738. end
  739. balance = balance - cost
  740. local log_succ, log_err =
  741. self:log(p_name .. " bought " .. row_bought .. " " ..
  742. item_name .. " from you. (+" .. cost .. ")",
  743. poster)
  744. if not log_succ then
  745. search_stmt:reset()
  746. db:exec("ROLLBACK;")
  747. return false, log_err
  748. end
  749. local log_succ, log_err =
  750. self:log("Bought " .. row_bought .. " " .. item_name ..
  751. " from " .. poster .. ". (-" .. cost .. ")",
  752. p_name)
  753. if not log_succ then
  754. search_stmt:reset()
  755. db:exec("ROLLBACK;")
  756. return false, log_err
  757. end
  758. else
  759. local log_succ, log_err =
  760. self:log("Bought " .. row_bought .. " " ..
  761. item_name .. " from yourself.",
  762. p_name)
  763. if not log_succ then
  764. search_stmt:reset()
  765. db:exec("ROLLBACK;")
  766. return false, log_err
  767. end
  768. end
  769. order_book_cache(ex_name)[item_name] = nil
  770. table.insert(bought, { amount = row_bought, wear = row_wear })
  771. remaining = remaining - row_bought
  772. if remaining == 0 or out_of_funds then break end
  773. end
  774. search_stmt:reset()
  775. if remaining > 0 then
  776. if out_of_funds then
  777. local log_succ, log_err =
  778. self:log("Insufficient funds to buy " .. remaining .. " " ..
  779. item_name .. " at " .. last_row_rate .. "/ea.",
  780. p_name)
  781. if not log_succ then
  782. db:exec("ROLLBACK;")
  783. return false, log_err
  784. end
  785. else
  786. local add_succ, add_err =
  787. self:add_order(p_name, ex_name, "buy", item_name, wear, remaining, rate)
  788. if not add_succ then
  789. db:exec("ROLLBACK;")
  790. return false, add_err
  791. end
  792. local log_succ, log_err =
  793. self:log("Posted buy offer for " .. remaining .. " " ..
  794. item_name .. " at " .. rate .. "/ea.",
  795. p_name)
  796. if not log_succ then
  797. db:exec("ROLLBACK;")
  798. return false, log_err
  799. end
  800. end
  801. end
  802. db:exec("COMMIT;")
  803. return true, bought
  804. end
  805. -- Tries to sell to orders at the provided rate, and posts an offer with any
  806. -- remaining desired amount. Returns success. If failed, returns an error message.
  807. function ex_methods.sell(self, p_name, ex_name, item_name, wear, amount, rate)
  808. if not is_integer(amount) then
  809. return false, S("Noninteger quantity")
  810. elseif amount <= 0 then
  811. return false, S("Nonpositive quantity")
  812. elseif not is_integer(rate) then
  813. return false, S("Noninteger rate")
  814. elseif rate <= 0 then
  815. return false, S("Nonpositive rate")
  816. elseif not is_integer(wear) then
  817. return false, S("Noninteger wear")
  818. elseif wear < 0 or wear > 65535 then
  819. return false, S("Invalid wear")
  820. end
  821. local db = self.db
  822. db:exec("BEGIN TRANSACTION");
  823. local remaining = amount
  824. local del_stmt = self.stmts.del_order_stmt
  825. local red_stmt = self.stmts.reduce_order_stmt
  826. local search_stmt = self.stmts.qual_bids_stmt
  827. search_stmt:bind_names({
  828. ex_name = ex_name,
  829. item_name = item_name,
  830. rate_min = rate,
  831. wear_min = wear,
  832. })
  833. for row in search_stmt:nrows() do
  834. local poster = row.Poster
  835. local row_amount = row.Amount
  836. local row_rate = row.Rate
  837. local row_sold = math.min(row_amount, remaining)
  838. local out_of_funds = false
  839. if poster ~= p_name then
  840. local bal = self:get_balance(poster) or 0
  841. local can_afford = math.floor(bal / row_rate)
  842. out_of_funds = can_afford < row_sold
  843. row_sold = math.min(row_sold, can_afford)
  844. end
  845. local red_del_stmt
  846. if row_sold < row_amount and not out_of_funds then
  847. red_stmt:bind_names({
  848. id = row.Id,
  849. delta = row_sold,
  850. })
  851. red_del_stmt = red_stmt
  852. else -- row_sold == row_amount or out_of_funds
  853. del_stmt:bind_values(row.Id)
  854. red_del_stmt = del_stmt
  855. end
  856. local red_del_res = red_del_stmt:step()
  857. red_del_stmt:reset()
  858. if red_del_res == sqlite3.BUSY then
  859. search_stmt:reset()
  860. db:exec("ROLLBACK;")
  861. return false, S("Database Busy.")
  862. elseif red_del_res ~= sqlite3.DONE then
  863. search_stmt:reset()
  864. sql_error(db:errmsg())
  865. end
  866. local in_succ, in_err =
  867. self:put_in_inbox(poster, item_name, wear, row_sold)
  868. if not in_succ then
  869. search_stmt:reset()
  870. db:exec("ROLLBACK;")
  871. return false, in_err
  872. end
  873. if poster ~= p_name then
  874. local revenue = row_sold * row_rate
  875. local ch_succ, ch_err = self:change_balance(poster, -revenue)
  876. if not ch_succ then
  877. search_stmt:reset()
  878. db:exec("ROLLBACK;")
  879. return false, ch_err
  880. end
  881. local ch_succ, ch_err = self:change_balance(p_name, revenue)
  882. if not ch_succ then
  883. search_stmt:reset()
  884. db:exec("ROLLBACK;")
  885. return false, ch_err
  886. end
  887. local log_succ, log_err =
  888. self:log(p_name .. " sold " .. row_sold .. " " ..
  889. item_name .. " to you. (-" .. revenue .. ")",
  890. poster)
  891. if not log_succ then
  892. search_stmt:reset()
  893. db:exec("ROLLBACK;")
  894. return false, log_err
  895. end
  896. if out_of_funds then
  897. local log_succ, log_err =
  898. self:log("Insufficient funds to buy " ..
  899. math.min(row_amount - row_sold, remaining) ..
  900. " " .. item_name .. " from " .. p_name ..
  901. " at " .. row_rate .. "/ea.",
  902. poster)
  903. if not log_succ then
  904. db:exec("ROLLBACK;")
  905. return false, log_err
  906. end
  907. end
  908. local log_succ, log_err =
  909. self:log("Sold " .. row_sold .. " " .. item_name ..
  910. " to " .. poster .. ". (+" .. revenue .. ")",
  911. p_name)
  912. if not log_succ then
  913. search_stmt:reset()
  914. db:exec("ROLLBACK;")
  915. return false, log_err
  916. end
  917. else
  918. local log_succ, log_err =
  919. self:log("Sold " .. row_sold .. " " ..
  920. item_name .. " to yourself.",
  921. p_name)
  922. if not log_succ then
  923. search_stmt:reset()
  924. db:exec("ROLLBACK;")
  925. return false, log_err
  926. end
  927. end
  928. order_book_cache(ex_name)[item_name] = nil
  929. remaining = remaining - row_sold
  930. if remaining == 0 then break end
  931. end
  932. search_stmt:reset()
  933. if remaining > 0 then
  934. local add_succ, add_err =
  935. self:add_order(p_name, ex_name, "sell", item_name, wear, remaining, rate)
  936. if not add_succ then
  937. db:exec("ROLLBACK;")
  938. return false, add_err
  939. end
  940. end
  941. db:exec("COMMIT;")
  942. return true
  943. end
  944. -- On success, returns true and a list of inbox entries.
  945. -- TODO: On failure, return false and an error message.
  946. function ex_methods.view_inbox(self, p_name)
  947. local stmt = self.stmts.view_inbox_stmt
  948. stmt:bind_values(p_name)
  949. local res = {}
  950. for row in stmt:nrows() do
  951. table.insert(res, row)
  952. end
  953. stmt:reset()
  954. return true, res
  955. end
  956. -- Returns success boolean. On success, also returns the number actually
  957. -- taken. On failure, also returns an error message
  958. function ex_methods.take_inbox(self, id, amount)
  959. local db = self.db
  960. local get_stmt = self.stmts.get_inbox_stmt
  961. local red_stmt = self.stmts.red_inbox_stmt
  962. local del_stmt = self.stmts.del_inbox_stmt
  963. get_stmt:bind_names({ id = id })
  964. local res = get_stmt:step()
  965. if res == sqlite3.BUSY then
  966. get_stmt:reset()
  967. return false, S("Database Busy.")
  968. elseif res == sqlite3.DONE then
  969. get_stmt:reset()
  970. return false, S("Order does not exist.")
  971. elseif res ~= sqlite3.ROW then
  972. sql_error(db:errmsg())
  973. end
  974. local available = get_stmt:get_value(0)
  975. get_stmt:reset()
  976. db:exec("BEGIN TRANSACTION;")
  977. local red_del_stmt
  978. if available > amount then
  979. red_stmt:bind_names({
  980. id = id,
  981. change = amount
  982. })
  983. red_del_stmt = red_stmt
  984. else
  985. del_stmt:bind_values(id)
  986. red_del_stmt = del_stmt
  987. end
  988. local red_del_res = red_del_stmt:step()
  989. if red_del_res == sqlite3.BUSY then
  990. red_del_stmt:reset()
  991. db:exec("ROLLBACK;")
  992. return false, S("Database Busy.")
  993. elseif red_del_res ~= sqlite3.DONE then
  994. sql_error(db:errmsg())
  995. end
  996. red_del_stmt:reset()
  997. db:exec("COMMIT;")
  998. return true, math.min(amount, available)
  999. end
  1000. -- Returns a list of tables with fields:
  1001. -- item_name: Name of the item
  1002. -- buy_volume: Number of items sought
  1003. -- buy_max: Maximum buy rate
  1004. -- sell_volume: Number of items for sale
  1005. -- sell_min: Minimum sell rate
  1006. function ex_methods.market_summary(self)
  1007. local stmt = self.stmts.summary_stmt
  1008. local res = {}
  1009. for a in stmt:nrows() do
  1010. table.insert(res, a)
  1011. end
  1012. stmt:reset()
  1013. return res
  1014. end
  1015. -- Returns a list of log entries, sorted by time.
  1016. function ex_methods.player_log(self, p_name)
  1017. local stmt = self.stmts.transaction_log_stmt
  1018. stmt:bind_values(p_name)
  1019. local res = {}
  1020. for row in stmt:nrows() do
  1021. table.insert(res, row)
  1022. end
  1023. stmt:reset()
  1024. return res
  1025. end
  1026. function exports.test()
  1027. local ex = exports.open_exchange("test.db")
  1028. local alice_bal = ex:get_balance("Alice")
  1029. local bob_bal = ex:get_balance("Bob")
  1030. local function print_balances()
  1031. print("Alice: ", ex:get_balance("Alice"))
  1032. print("Bob: ", ex:get_balance("Bob"))
  1033. end
  1034. -- Initialize balances
  1035. if alice_bal then
  1036. ex:set_balance("Alice", 420)
  1037. else
  1038. ex:new_account("Alice", 420)
  1039. end
  1040. if bob_bal then
  1041. ex:set_balance("Bob", 2015)
  1042. else
  1043. ex:new_account("Bob", 2015)
  1044. end
  1045. print_balances()
  1046. -- Transfer a valid amount
  1047. print("Transfering 1000 credits from Bob to Alice")
  1048. local succ, err = ex:transfer_credits("Bob", "Alice", 1000)
  1049. print("Success: ", succ, " ", err)
  1050. print_balances()
  1051. -- Transfer an invalid amount
  1052. print("Transfering 3000 credits from Alice to Bob")
  1053. local succ, err = ex:transfer_credits("Alice", "Bob", 3000)
  1054. print("Success: ", succ, " ", err)
  1055. print_balances()
  1056. -- Simulate a transaction
  1057. print("Alice posting an offer to buy 10 cobble at 2 credits each")
  1058. local succ, err = ex:buy("Alice", "", "default:cobble", 32767, 10, 2)
  1059. print("Success: ", succ, " ", err)
  1060. print_balances()
  1061. print("Bob posting an offer to sell 20 cobble at 1 credits each")
  1062. local succ, err = ex:sell("Bob", "", "default:cobble", 32767, 20, 1)
  1063. print("Success: ", succ, " ", err)
  1064. print_balances()
  1065. ex:close()
  1066. end
  1067. return exports
  1068. -- vim:set ts=4 sw=4 noet: