mirror of
https://github.com/oxen-io/oxen-core.git
synced 2023-12-14 02:22:56 +01:00
353 lines
11 KiB
C++
353 lines
11 KiB
C++
#include "db_schema.hpp"
|
|
|
|
#include "output.hpp"
|
|
#include "block.hpp"
|
|
|
|
#include <common/hex.h>
|
|
#include <cryptonote_basic/cryptonote_basic.h>
|
|
|
|
#include <iostream>
|
|
|
|
namespace wallet
|
|
{
|
|
WalletDB::~WalletDB()
|
|
{
|
|
}
|
|
|
|
// FIXME: BLOB or TEXT for binary data below?
|
|
void
|
|
WalletDB::create_schema()
|
|
{
|
|
if (db.tableExists("outputs"))
|
|
return;
|
|
|
|
SQLite::Transaction db_tx(db);
|
|
|
|
// TODO: set up removal triggers
|
|
// TODO: table for balance "per account"
|
|
db.exec(
|
|
R"(
|
|
-- CHECK (id = 0) restricts this table to a single row
|
|
CREATE TABLE metadata (
|
|
id INTEGER NOT NULL PRIMARY KEY CHECK (id = 0),
|
|
db_version INTEGER NOT NULL DEFAULT 0,
|
|
balance INTEGER NOT NULL DEFAULT 0,
|
|
unlocked_balance INTEGER NOT NULL DEFAULT 0,
|
|
last_scan_height INTEGER NOT NULL DEFAULT -1,
|
|
scan_target_hash TEXT NOT NULL,
|
|
scan_target_height INTEGER NOT NULL DEFAULT 0,
|
|
output_count INTEGER NOT NULL DEFAULT 0
|
|
);
|
|
|
|
-- insert metadata row as default
|
|
INSERT INTO metadata VALUES (0,0,0,0,-1,"",0,0);
|
|
|
|
CREATE TABLE blocks (
|
|
height INTEGER NOT NULL PRIMARY KEY,
|
|
output_count INTEGER NOT NULL,
|
|
hash TEXT NOT NULL,
|
|
timestamp INTEGER NOT NULL
|
|
);
|
|
|
|
-- update scan height when new block added
|
|
CREATE TRIGGER block_added AFTER INSERT ON blocks
|
|
FOR EACH ROW
|
|
BEGIN
|
|
UPDATE metadata SET last_scan_height = NEW.height WHERE id = 0;
|
|
UPDATE metadata SET output_count = output_count + NEW.output_count WHERE id = 0;
|
|
END;
|
|
|
|
-- update scan height when new block removed
|
|
CREATE TRIGGER block_removed AFTER DELETE ON blocks
|
|
FOR EACH ROW
|
|
BEGIN
|
|
UPDATE metadata SET last_scan_height = OLD.height - 1 WHERE id = 0;
|
|
UPDATE metadata SET output_count = output_count - OLD.output_count WHERE id = 0;
|
|
END;
|
|
|
|
CREATE TABLE transactions (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
block INTEGER NOT NULL REFERENCES blocks(height) ON DELETE CASCADE,
|
|
hash TEXT NOT NULL,
|
|
UNIQUE(hash)
|
|
);
|
|
|
|
-- will default scan many subaddresses, even if never used, so it is useful to mark
|
|
-- if they have been used (for culling this list later, perhaps)
|
|
CREATE TABLE subaddresses (
|
|
major_index INTEGER NOT NULL,
|
|
minor_index INTEGER NOT NULL,
|
|
used BOOLEAN NOT NULL DEFAULT FALSE,
|
|
PRIMARY KEY(major_index, minor_index)
|
|
);
|
|
|
|
-- default "main" subaddress
|
|
INSERT INTO subaddresses VALUES (0,0,TRUE);
|
|
|
|
CREATE TABLE key_images (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
key_image BLOB NOT NULL,
|
|
UNIQUE(key_image)
|
|
);
|
|
|
|
CREATE TABLE outputs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
amount BIGINT NOT NULL,
|
|
output_index INTEGER NOT NULL,
|
|
global_index INTEGER NOT NULL,
|
|
unlock_time INTEGER NOT NULL,
|
|
block_height INTEGER NOT NULL REFERENCES blocks(height),
|
|
spending BOOLEAN NOT NULL DEFAULT FALSE,
|
|
spent_height INTEGER NOT NULL DEFAULT 0,
|
|
tx INTEGER NOT NULL REFERENCES transactions(id) ON DELETE CASCADE,
|
|
output_key BLOB NOT NULL,
|
|
derivation BLOB NOT NULL,
|
|
rct_mask BLOB NOT NULL,
|
|
key_image INTEGER NOT NULL REFERENCES key_images(id),
|
|
subaddress_major INTEGER NOT NULL,
|
|
subaddress_minor INTEGER NOT NULL,
|
|
FOREIGN KEY(subaddress_major, subaddress_minor) REFERENCES subaddresses(major_index, minor_index)
|
|
);
|
|
CREATE INDEX output_key_image ON outputs(key_image);
|
|
|
|
-- update balance when new output added
|
|
CREATE TRIGGER output_received AFTER INSERT ON outputs
|
|
FOR EACH ROW
|
|
BEGIN
|
|
UPDATE metadata SET balance = balance + NEW.amount WHERE id = 0;
|
|
END;
|
|
|
|
-- update balance when output removed (blockchain re-org)
|
|
CREATE TRIGGER output_removed AFTER DELETE ON outputs
|
|
FOR EACH ROW
|
|
BEGIN
|
|
UPDATE metadata SET balance = balance - OLD.amount WHERE id = 0;
|
|
END;
|
|
|
|
CREATE TABLE spends (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
key_image INTEGER NOT NULL REFERENCES key_images(id),
|
|
height INTEGER REFERENCES blocks(height) ON DELETE CASCADE,
|
|
tx INTEGER REFERENCES transactions(id),
|
|
UNIQUE(key_image)
|
|
);
|
|
CREATE INDEX spend_key_image ON spends(key_image);
|
|
|
|
-- update output and balance when output seen as spent
|
|
CREATE TRIGGER output_spend_received AFTER INSERT ON spends
|
|
FOR EACH ROW
|
|
BEGIN
|
|
UPDATE outputs SET spent_height = NEW.height WHERE key_image = NEW.key_image;
|
|
UPDATE metadata SET balance = balance - (SELECT outputs.amount FROM outputs WHERE outputs.key_image = NEW.key_image);
|
|
END;
|
|
|
|
-- update output and balance when output un-seen as spent (blockchain re-org)
|
|
CREATE TRIGGER output_spend_removed AFTER DELETE ON spends
|
|
FOR EACH ROW
|
|
BEGIN
|
|
UPDATE outputs SET spent_height = 0 WHERE key_image = OLD.key_image;
|
|
UPDATE metadata SET balance = balance + (SELECT outputs.amount FROM outputs WHERE outputs.key_image = OLD.key_image);
|
|
END;
|
|
|
|
CREATE TRIGGER key_image_output_removed_cleaner AFTER DELETE ON outputs
|
|
FOR EACH ROW WHEN (SELECT COUNT(*) FROM outputs WHERE key_image = OLD.key_image) = 0
|
|
AND (SELECT COUNT(*) FROM spends WHERE key_image = OLD.key_image) = 0
|
|
BEGIN
|
|
DELETE FROM key_images WHERE id = OLD.key_image;
|
|
END;
|
|
|
|
CREATE TRIGGER key_image_spend_removed_cleaner AFTER DELETE ON spends
|
|
FOR EACH ROW WHEN (SELECT COUNT(*) FROM outputs WHERE key_image = OLD.key_image) = 0
|
|
BEGIN
|
|
DELETE FROM key_images WHERE id = OLD.key_image;
|
|
END;
|
|
|
|
)");
|
|
|
|
db_tx.commit();
|
|
}
|
|
|
|
void
|
|
WalletDB::store_block(const Block& block)
|
|
{
|
|
int64_t output_count = 0;
|
|
for (const auto& tx : block.transactions)
|
|
{
|
|
output_count += tx.tx.vout.size();
|
|
}
|
|
|
|
prepared_exec(
|
|
"INSERT INTO blocks(height,output_count,hash,timestamp) VALUES(?,?,?,?)",
|
|
block.height,
|
|
output_count,
|
|
tools::type_to_hex(block.hash),
|
|
block.timestamp);
|
|
}
|
|
|
|
void
|
|
WalletDB::pop_block()
|
|
{
|
|
prepared_exec("DELETE FROM blocks WHERE height = (SELECT MAX(height) FROM blocks)");
|
|
}
|
|
|
|
void
|
|
WalletDB::store_transaction(
|
|
const crypto::hash& tx_hash, const int64_t height, const std::vector<Output>& outputs)
|
|
{
|
|
auto hash_str = tools::type_to_hex(tx_hash);
|
|
prepared_exec(
|
|
"INSERT INTO transactions(block,hash) VALUES(?,?)", height, hash_str);
|
|
|
|
for (const auto& output : outputs)
|
|
{
|
|
prepared_exec(
|
|
"INSERT INTO key_images(key_image) VALUES(?)", tools::type_to_hex(output.key_image));
|
|
prepared_exec(
|
|
R"(
|
|
INSERT INTO outputs(
|
|
amount,
|
|
output_index,
|
|
global_index,
|
|
unlock_time,
|
|
block_height,
|
|
tx,
|
|
output_key,
|
|
derivation,
|
|
rct_mask,
|
|
key_image,
|
|
subaddress_major,
|
|
subaddress_minor)
|
|
VALUES(?,?,?,?,?,
|
|
(SELECT id FROM transactions WHERE hash = ?),
|
|
?,?,?,
|
|
(SELECT id FROM key_images WHERE key_image = ?),
|
|
?,?);
|
|
)",
|
|
output.amount,
|
|
output.output_index,
|
|
output.global_index,
|
|
output.unlock_time,
|
|
output.block_height,
|
|
hash_str,
|
|
tools::type_to_hex(output.key),
|
|
tools::type_to_hex(output.derivation),
|
|
tools::type_to_hex(output.rct_mask),
|
|
tools::type_to_hex(output.key_image),
|
|
output.subaddress_index.major,
|
|
output.subaddress_index.minor);
|
|
}
|
|
}
|
|
|
|
void
|
|
WalletDB::store_spends(
|
|
const crypto::hash& tx_hash,
|
|
const int64_t height,
|
|
const std::vector<crypto::key_image>& spends)
|
|
{
|
|
auto hash_hex = tools::type_to_hex(tx_hash);
|
|
prepared_exec(
|
|
"INSERT INTO transactions(block,hash) VALUES(?,?) ON CONFLICT DO NOTHING",
|
|
height,
|
|
hash_hex);
|
|
|
|
for (const auto& key_image : spends)
|
|
{
|
|
prepared_exec(
|
|
R"(INSERT INTO spends(key_image, height, tx)
|
|
VALUES((SELECT id FROM key_images WHERE key_image = ?),
|
|
?,
|
|
(SELECT id FROM transactions WHERE hash = ?));)",
|
|
tools::type_to_hex(key_image),
|
|
height,
|
|
hash_hex);
|
|
}
|
|
}
|
|
|
|
int64_t
|
|
WalletDB::last_scan_height()
|
|
{
|
|
return prepared_get<int64_t>("SELECT last_scan_height FROM metadata WHERE id=0;");
|
|
}
|
|
|
|
int64_t
|
|
WalletDB::scan_target_height()
|
|
{
|
|
return prepared_get<int64_t>("SELECT scan_target_height FROM metadata WHERE id=0;");
|
|
}
|
|
|
|
void
|
|
WalletDB::update_top_block_info(int64_t height, const crypto::hash& hash)
|
|
{
|
|
prepared_exec("UPDATE metadata SET scan_target_height = ?, scan_target_hash = ? WHERE id = 0",
|
|
height, tools::type_to_hex(hash));
|
|
}
|
|
|
|
int64_t
|
|
WalletDB::overall_balance()
|
|
{
|
|
return prepared_get<int64_t>("SELECT balance FROM metadata WHERE id=0;");
|
|
}
|
|
|
|
int64_t
|
|
WalletDB::available_balance(std::optional<int64_t> min_amount)
|
|
{
|
|
std::string query = "SELECT sum(amount) FROM outputs WHERE spent_height = 0 AND spending = FALSE";
|
|
|
|
if (min_amount)
|
|
{
|
|
query += " AND amount > ?";
|
|
return prepared_get<int64_t>(query, *min_amount);
|
|
}
|
|
|
|
return prepared_get<int64_t>(query);
|
|
}
|
|
|
|
std::vector<Output>
|
|
WalletDB::available_outputs(std::optional<int64_t> min_amount)
|
|
{
|
|
std::vector<Output> outs;
|
|
|
|
std::string query = "SELECT amount, output_index, global_index, "
|
|
"unlock_time, block_height, output_key, derivation, rct_mask, key_images.key_image, "
|
|
"spent_height, spending FROM outputs JOIN key_images ON outputs.key_image = key_images.id WHERE spent_height = 0 AND spending = FALSE ";
|
|
|
|
if (min_amount)
|
|
{
|
|
query += "AND amount > ? ";
|
|
}
|
|
|
|
query += "ORDER BY amount";
|
|
|
|
auto st = prepared_st(query);
|
|
|
|
if (min_amount)
|
|
st->bind(1, *min_amount);
|
|
|
|
while (st->executeStep())
|
|
{
|
|
auto& out = outs.emplace_back();
|
|
auto from_db = db::get<int64_t, int64_t, int64_t, int64_t, int64_t, std::string,
|
|
std::string, std::string, std::string, int64_t, int64_t>(st);
|
|
out.amount = std::get<0>(from_db);
|
|
out.output_index = std::get<1>(from_db);
|
|
out.global_index = std::get<2>(from_db);
|
|
out.unlock_time = std::get<3>(from_db);
|
|
out.block_height = std::get<4>(from_db);
|
|
tools::hex_to_type(std::get<5>(from_db), out.key);
|
|
tools::hex_to_type(std::get<6>(from_db), out.derivation);
|
|
tools::hex_to_type(std::get<7>(from_db), out.rct_mask);
|
|
tools::hex_to_type(std::get<8>(from_db), out.key_image);
|
|
out.spent_height = std::get<9>(from_db);
|
|
out.spending = std::get<10>(from_db);
|
|
}
|
|
|
|
return outs;
|
|
}
|
|
|
|
int64_t
|
|
WalletDB::chain_output_count()
|
|
{
|
|
return prepared_get<int64_t>("SELECT output_count FROM metadata WHERE id=0;");
|
|
}
|
|
|
|
} // namespace wallet
|