1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
use rusqlite::{params, Connection, OptionalExtension};

pub fn get_schema_version(connection: &Connection) -> anyhow::Result<u32> {
    let version: Option<u32> = connection
        .query_row(
            "SELECT version FROM schema_version WHERE id = 1",
            [],
            |row| row.get(0),
        )
        .optional()?;
    Ok(version.unwrap_or(0))
}

pub fn update_schema_version(connection: &Connection, version: u32) -> anyhow::Result<()> {
    connection.execute(
        "INSERT INTO schema_version(id, version) VALUES (1, ?1) \
    ON CONFLICT (id) DO UPDATE SET version = excluded.version",
        params![version],
    )?;
    Ok(())
}

pub fn reset_db(connection: &Connection) -> anyhow::Result<()> {
    // don't drop account data: accounts, taddrs, secret_shares
    connection.execute("DROP TABLE blocks", [])?;
    connection.execute("DROP TABLE transactions", [])?;
    connection.execute("DROP TABLE received_notes", [])?;
    connection.execute("DROP TABLE sapling_witnesses", [])?;
    connection.execute("DROP TABLE diversifiers", [])?;
    connection.execute("DROP TABLE historical_prices", [])?;
    update_schema_version(connection, 0)?;
    Ok(())
}

pub fn init_db(connection: &Connection) -> anyhow::Result<()> {
    connection.execute(
        "CREATE TABLE IF NOT EXISTS schema_version (
            id INTEGER PRIMARY KEY NOT NULL,
            version INTEGER NOT NULL)",
        [],
    )?;

    let version = get_schema_version(connection)?;

    if version < 1 {
        connection.execute(
            "CREATE TABLE IF NOT EXISTS accounts (
            id_account INTEGER PRIMARY KEY,
            name TEXT NOT NULL,
            seed TEXT,
            aindex INTEGER NOT NULL,
            sk TEXT,
            ivk TEXT NOT NULL UNIQUE,
            address TEXT NOT NULL)",
            [],
        )?;

        connection.execute(
            "CREATE TABLE IF NOT EXISTS blocks (
            height INTEGER PRIMARY KEY,
            hash BLOB NOT NULL,
            timestamp INTEGER NOT NULL,
            sapling_tree BLOB NOT NULL)",
            [],
        )?;

        connection.execute(
            "CREATE TABLE IF NOT EXISTS transactions (
            id_tx INTEGER PRIMARY KEY,
            account INTEGER NOT NULL,
            txid BLOB NOT NULL,
            height INTEGER NOT NULL,
            timestamp INTEGER NOT NULL,
            value INTEGER NOT NULL,
            address TEXT,
            memo TEXT,
            tx_index INTEGER,
            CONSTRAINT tx_account UNIQUE (height, tx_index, account))",
            [],
        )?;

        connection.execute(
            "CREATE TABLE IF NOT EXISTS received_notes (
            id_note INTEGER PRIMARY KEY,
            account INTEGER NOT NULL,
            position INTEGER NOT NULL,
            tx INTEGER NOT NULL,
            height INTEGER NOT NULL,
            output_index INTEGER NOT NULL,
            diversifier BLOB NOT NULL,
            value INTEGER NOT NULL,
            rcm BLOB NOT NULL,
            nf BLOB NOT NULL UNIQUE,
            spent INTEGER,
            excluded BOOL,
            CONSTRAINT tx_output UNIQUE (tx, output_index))",
            [],
        )?;

        connection.execute(
            "CREATE TABLE IF NOT EXISTS sapling_witnesses (
            id_witness INTEGER PRIMARY KEY,
            note INTEGER NOT NULL,
            height INTEGER NOT NULL,
            witness BLOB NOT NULL,
            CONSTRAINT witness_height UNIQUE (note, height))",
            [],
        )?;

        connection.execute(
            "CREATE TABLE IF NOT EXISTS diversifiers (
            account INTEGER PRIMARY KEY NOT NULL,
            diversifier_index BLOB NOT NULL)",
            [],
        )?;

        connection.execute(
            "CREATE TABLE IF NOT EXISTS taddrs (
            account INTEGER PRIMARY KEY NOT NULL,
            sk TEXT NOT NULL,
            address TEXT NOT NULL)",
            [],
        )?;

        connection.execute(
            "CREATE TABLE IF NOT EXISTS historical_prices (
                currency TEXT NOT NULL,
                timestamp INTEGER NOT NULL,
                price REAL NOT NULL,
                PRIMARY KEY (currency, timestamp))",
            [],
        )?;

        connection.execute(
            "CREATE TABLE IF NOT EXISTS contacts (
                id INTEGER PRIMARY KEY,
                name TEXT NOT NULL,
                address TEXT NOT NULL,
                dirty BOOL NOT NULL)",
            [],
        )?;
    }

    if version < 2 {
        connection.execute(
            "CREATE INDEX i_received_notes ON received_notes(account)",
            [],
        )?;
        connection.execute("CREATE INDEX i_account ON accounts(address)", [])?;
        connection.execute("CREATE INDEX i_contact ON contacts(address)", [])?;
        connection.execute("CREATE INDEX i_transaction ON transactions(account)", [])?;
        connection.execute("CREATE INDEX i_witness ON sapling_witnesses(height)", [])?;
    }

    if version < 3 {
        connection.execute(
            "CREATE TABLE IF NOT EXISTS messages (
            id INTEGER PRIMARY KEY,
            account INTEGER NOT NULL,
            sender TEXT,
            recipient TEXT NOT NULL,
            subject TEXT NOT NULL,
            body TEXT NOT NULL,
            timestamp INTEGER NOT NULL,
            height INTEGER NOT NULL,
            read BOOL NOT NULL)",
            [],
        )?;
        // Don't index because it *really* slows down inserts
        // connection.execute(
        //     "CREATE INDEX i_messages ON messages(account)",
        //     [],
        // )?;
    }

    if version < 4 {
        connection.execute("ALTER TABLE messages ADD id_tx INTEGER", [])?;
    }

    if version != 4 {
        update_schema_version(connection, 4)?;
        log::info!("Database migrated");
    }

    // We may get a database that has no valid schema version from a version of single currency Z/YWallet
    // because we kept the same app name in Google/Apple Stores. The upgraded app fails to recognize the db tables
    // At least we monkey patch the accounts table to let the user access the backup page and recover his seed phrase
    let c = connection.query_row(
        "SELECT COUNT(*) FROM pragma_table_info('accounts') WHERE name = 'aindex'",
        params![],
        |row| {
            let c: u32 = row.get(0)?;
            Ok(c)
        },
    )?;
    if c == 0 {
        connection.execute(
            "ALTER TABLE accounts ADD aindex INTEGER NOT NULL DEFAULT (0)",
            params![],
        )?;
    }

    Ok(())
}