Add data access layer for dataDb

Update the scanBlocks request and integrate with the data that it generates inside dataDb
This commit is contained in:
Kevin Gorham 2018-12-20 20:44:09 -05:00
parent a1f4491fba
commit 1501f1a7d2
14 changed files with 746 additions and 10 deletions

View File

@ -0,0 +1,39 @@
CREATE TABLE IF NOT EXISTS blocks (
height INTEGER PRIMARY KEY,
time INTEGER,
sapling_tree BLOB
);
CREATE TABLE IF NOT EXISTS transactions (
id_tx INTEGER PRIMARY KEY,
txid BLOB NOT NULL UNIQUE,
block INTEGER,
raw BLOB,
FOREIGN KEY (block) REFERENCES blocks(height)
);
CREATE TABLE IF NOT EXISTS received_notes (
id_note INTEGER PRIMARY KEY,
tx INTEGER NOT NULL,
output_index INTEGER NOT NULL,
account INTEGER NOT NULL,
diversifier BLOB NOT NULL,
value INTEGER NOT NULL,
rcm BLOB NOT NULL,
nf BLOB NOT NULL UNIQUE,
memo BLOB,
spent INTEGER,
FOREIGN KEY (tx) REFERENCES transactions(id_tx),
FOREIGN KEY (spent) REFERENCES transactions(id_tx),
CONSTRAINT tx_output UNIQUE (tx, output_index)
);
CREATE TABLE IF NOT EXISTS sapling_witnesses (
id_witness INTEGER PRIMARY KEY,
note INTEGER NOT NULL,
block INTEGER NOT NULL,
witness BLOB NOT NULL,
FOREIGN KEY (note) REFERENCES received_notes(id_note),
FOREIGN KEY (block) REFERENCES blocks(height),
CONSTRAINT witness_height UNIQUE (note, block)
);

View File

@ -29,7 +29,7 @@ apply plugin: 'com.github.ben-manes.versions'
apply plugin: 'com.github.dcendents.android-maven'
group = 'cash.z.android.wallet'
version = '1.2.0'
version = '1.2.4'
repositories {
google()
@ -42,8 +42,8 @@ android {
defaultConfig {
minSdkVersion 16
targetSdkVersion 28
versionCode = 1_02_00
versionName = "1.2.0"
versionCode = 1_02_03
versionName = "1.2.3"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@ -60,6 +60,10 @@ android {
}
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {

View File

@ -0,0 +1,57 @@
package cash.z.wallet.sdk.dao
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.room.Room
import androidx.test.platform.app.InstrumentationRegistry
import cash.z.wallet.sdk.db.CompactBlockDb
import cash.z.wallet.sdk.db.DerivedDataDb
import cash.z.wallet.sdk.vo.CompactBlock
import cash.z.wallet.sdk.vo.Transaction
import org.junit.*
import org.junit.Assert.*
class TransactionDaoTest {
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
private lateinit var dao: TransactionDao
private lateinit var db: DerivedDataDb
@Before
fun initDb() {
db = Room.inMemoryDatabaseBuilder(
InstrumentationRegistry.getInstrumentation().context,
DerivedDataDb::class.java
)
.build()
.apply { dao = transactionDao() }
}
@After
fun close() {
db.close()
}
@Test
fun testDbExists() {
assertNotNull(db)
}
@Test
fun testDaoExists() {
assertNotNull(dao)
}
@Test
fun testDaoInsert() {
Transaction(4, "sample".toByteArray(), 356418, null).let { transaction ->
dao.insert(transaction)
val result = dao.findById(transaction.id)
assertEquals(transaction.id, result?.id)
assertTrue(transaction.transactionId.contentEquals(result!!.transactionId))
dao.delete(transaction)
}
}
}

View File

@ -9,7 +9,7 @@ import cash.z.wallet.sdk.vo.CompactBlock
import org.junit.*
import org.junit.Assert.*
class DbIntegrationTest {
class CacheDbIntegrationTest {
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
@ -39,7 +39,8 @@ class DbIntegrationTest {
fun setup() {
// TODO: put this database in the assets directory and open it from there via .openHelperFactory(new AssetSQLiteOpenHelperFactory()) seen here https://github.com/albertogiunta/sqliteAsset
db = Room
.databaseBuilder(ApplicationProvider.getApplicationContext(), CompactBlockDb::class.java, "compact-blocks.db")
.databaseBuilder(ApplicationProvider.getApplicationContext(), CompactBlockDb::class.java, "dummy-cache.db")
// .databaseBuilder(ApplicationProvider.getApplicationContext(), CompactBlockDb::class.java, "compact-blocks.db")
.setJournalMode(RoomDatabase.JournalMode.TRUNCATE)
.fallbackToDestructiveMigration()
.build()

View File

@ -0,0 +1,95 @@
package cash.z.wallet.sdk.db
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.test.core.app.ApplicationProvider
import cash.z.wallet.sdk.dao.BlockDao
import cash.z.wallet.sdk.dao.CompactBlockDao
import cash.z.wallet.sdk.dao.NoteDao
import cash.z.wallet.sdk.dao.TransactionDao
import cash.z.wallet.sdk.vo.CompactBlock
import org.junit.*
import org.junit.Assert.*
class DerivedDbIntegrationTest {
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
@Test
fun testDbExists() {
assertNotNull(db)
}
@Test
fun testDaoExists_Transaction() {
assertNotNull(transactions)
}
@Test
fun testDaoExists_Block() {
assertNotNull(blocks)
}
@Test
fun testDaoExists_Note() {
assertNotNull(notes)
}
@Test
fun testCount_Transaction() {
assertEquals(5, transactions.count())
}
@Test
fun testCount_Block() {
assertEquals(80101, blocks.count())
}
@Test
fun testCount_Note() {
assertEquals(5, notes.count())
}
@Test
fun testTransactionDaoPrepopulated() {
val tran = transactions.findById(1)
assertEquals(343987, tran?.block)
}
@Test
fun testBlockDaoPrepopulated() {
val tran = blocks.findById(1)?.apply {
assertEquals(343987, this.height)
}
}
companion object {
private lateinit var transactions: TransactionDao
private lateinit var blocks: BlockDao
private lateinit var notes: NoteDao
private lateinit var db: DerivedDataDb
@BeforeClass
@JvmStatic
fun setup() {
// TODO: put this database in the assets directory and open it from there via .openHelperFactory(new AssetSQLiteOpenHelperFactory()) seen here https://github.com/albertogiunta/sqliteAsset
db = Room
.databaseBuilder(ApplicationProvider.getApplicationContext(), DerivedDataDb::class.java, "dummy-data2.db")
.setJournalMode(RoomDatabase.JournalMode.TRUNCATE)
.fallbackToDestructiveMigration()
.build()
.apply {
transactions = transactionDao()
blocks = blockDao()
notes = noteDao()
}
}
@AfterClass
@JvmStatic
fun close() {
db.close()
}
}
}

View File

@ -1,5 +1,6 @@
package cash.z.wallet.sdk.jni
import android.text.format.DateUtils
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.BeforeClass
@ -23,12 +24,13 @@ class JniConverterTest {
fun testScanBlocks() {
// note: for this to work, the db file below must be uploaded to the device. Eventually, this test will be self-contained and remove that requirement.
val result = converter.scanBlocks(
"/data/user/0/cash.z.wallet.sdk.test/databases/compact-block.db",
343900,
344855,
"dummyseed".toByteArray()
"/data/user/0/cash.z.wallet.sdk.test/databases/dummy-cache.db",
"/data/user/0/cash.z.wallet.sdk.test/databases/data.db",
"dummyseed".toByteArray(),
343900
)
assertEquals("Invalid number of results", 2, result.size)
// Thread.sleep(15 * DateUtils.MINUTE_IN_MILLIS)
assertEquals("Invalid number of results", 2, 3)
}
companion object {

View File

@ -0,0 +1,26 @@
package cash.z.wallet.sdk.dao
import androidx.room.*
import cash.z.wallet.sdk.vo.Block
import androidx.lifecycle.LiveData
@Dao
interface BlockDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(block: Block)
@Query("SELECT * FROM blocks WHERE height = :height")
fun findById(height: Int): Block?
@Query("DELETE FROM blocks WHERE height = :height")
fun deleteById(height: Int)
@Delete
fun delete(block: Block)
@Query("SELECT COUNT(height) FROM blocks")
fun count(): Int
}

View File

@ -0,0 +1,25 @@
package cash.z.wallet.sdk.dao
import androidx.room.*
import cash.z.wallet.sdk.vo.Note
@Dao
interface NoteDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(block: Note)
@Query("SELECT * FROM received_notes WHERE id_note = :id")
fun findById(id: Int): Note?
@Query("DELETE FROM received_notes WHERE id_note = :id")
fun deleteById(id: Int)
@Query("SELECT * FROM received_notes WHERE 1")
fun getAll(): List<Note>
@Delete
fun delete(block: Note)
@Query("SELECT COUNT(id_note) FROM received_notes")
fun count(): Int
}

View File

@ -0,0 +1,26 @@
package cash.z.wallet.sdk.dao
import androidx.room.*
import cash.z.wallet.sdk.vo.Transaction
@Dao
interface TransactionDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(block: Transaction)
@Query("SELECT * FROM transactions WHERE id_tx = :id")
fun findById(id: Int): Transaction?
@Query("DELETE FROM transactions WHERE id_tx = :id")
fun deleteById(id: Int)
@Query("SELECT * FROM transactions WHERE 1")
fun getAll(): List<Transaction>
@Delete
fun delete(block: Transaction)
@Query("SELECT COUNT(id_tx) FROM transactions")
fun count(): Int
}

View File

@ -0,0 +1,25 @@
package cash.z.wallet.sdk.db
import androidx.room.Database
import androidx.room.RoomDatabase
import cash.z.wallet.sdk.dao.BlockDao
import cash.z.wallet.sdk.dao.NoteDao
import cash.z.wallet.sdk.dao.TransactionDao
import cash.z.wallet.sdk.vo.Block
import cash.z.wallet.sdk.vo.Note
import cash.z.wallet.sdk.vo.Transaction
@Database(
entities = [
Transaction::class,
Block::class,
Note::class
],
version = 1,
exportSchema = false
)
abstract class DerivedDataDb : RoomDatabase() {
abstract fun transactionDao(): TransactionDao
abstract fun noteDao(): NoteDao
abstract fun blockDao(): BlockDao
}

View File

@ -0,0 +1,29 @@
package cash.z.wallet.sdk.vo
import androidx.room.ColumnInfo
import androidx.room.Entity
@Entity(primaryKeys = ["height"], tableName = "blocks")
data class Block(
val height: Int,
val time: Int,
@ColumnInfo(typeAffinity = ColumnInfo.BLOB, name = "sapling_tree")
val saplingTree: ByteArray
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
return (other is Block)
&& height == other.height
&& time == other.time
&& saplingTree.contentEquals(other.saplingTree)
}
override fun hashCode(): Int {
var result = height
result = 31 * result + time
result = 31 * result + saplingTree.contentHashCode()
return result
}
}

View File

@ -0,0 +1,80 @@
package cash.z.wallet.sdk.vo
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
@Entity(
tableName = "received_notes",
primaryKeys = ["id_note"],
foreignKeys = [ForeignKey(
entity = Transaction::class,
parentColumns = ["id_tx"],
childColumns = ["tx"],
onUpdate = ForeignKey.CASCADE,
onDelete = ForeignKey.CASCADE
), ForeignKey(
entity = Transaction::class,
parentColumns = ["id_tx"],
childColumns = ["spent"],
onUpdate = ForeignKey.CASCADE,
onDelete = ForeignKey.SET_NULL
)]
)
data class Note(
@ColumnInfo(name = "id_note")
val id: Int,
@ColumnInfo(name = "tx")
val transaction: Int,
@ColumnInfo(name = "output_index")
val outputIndex: Int,
val account: Int,
val value: Int,
val spent: Int,
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
val diversifier: ByteArray,
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
val rcm: ByteArray,
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
val nf: ByteArray,
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
val memo: ByteArray?
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
return (other is Note)
&& id == other.id
&& transaction == other.transaction
&& outputIndex == other.outputIndex
&& account == other.account
&& value == other.value
&& spent == other.spent
&& diversifier.contentEquals(other.diversifier)
&& rcm.contentEquals(other.rcm)
&& nf.contentEquals(other.nf)
&& ((memo == null && other.memo == null) || (memo != null && other.memo != null && memo.contentEquals(other.memo)))
}
override fun hashCode(): Int {
var result = id
result = 31 * result + transaction
result = 31 * result + outputIndex
result = 31 * result + account
result = 31 * result + value
result = 31 * result + spent
result = 31 * result + diversifier.contentHashCode()
result = 31 * result + rcm.contentHashCode()
result = 31 * result + nf.contentHashCode()
result = 31 * result + (memo?.contentHashCode() ?: 0)
return result
}
}

View File

@ -0,0 +1,48 @@
package cash.z.wallet.sdk.vo
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import org.jetbrains.annotations.NotNull
@Entity(
primaryKeys = ["id_tx"], tableName = "transactions",
foreignKeys = [ForeignKey(
entity = Block::class,
parentColumns = ["height"],
childColumns = ["block"],
onDelete = ForeignKey.CASCADE
)]
)
data class Transaction(
@ColumnInfo(name = "id_tx")
val id: Int,
@ColumnInfo(typeAffinity = ColumnInfo.BLOB, name = "txid")
@NotNull
val transactionId: ByteArray,
val block: Int,
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
val raw: ByteArray?
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
return (other is Transaction)
&& id == other.id
&& transactionId.contentEquals(other.transactionId)
&& block == other.block
&& ((raw == null && other.raw == null) || (raw != null && other.raw != null && raw.contentEquals(other.raw)))
}
override fun hashCode(): Int {
var result = id
result = 31 * result + transactionId.contentHashCode()
result = 31 * result + block
result = 31 * result + (raw?.contentHashCode() ?: 0)
return result
}
}

View File

@ -0,0 +1,279 @@
// This file is generated by rust-protobuf 2.2.0. Do not edit
// @generated
// https://github.com/Manishearth/rust-clippy/issues/702
#![allow(unknown_lints)]
#![allow(clippy)]
#![cfg_attr(rustfmt, rustfmt_skip)]
#![allow(box_pointers)]
#![allow(dead_code)]
#![allow(missing_docs)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
#![allow(non_upper_case_globals)]
#![allow(trivial_casts)]
#![allow(unsafe_code)]
#![allow(unused_imports)]
#![allow(unused_results)]
use protobuf::Message as Message_imported_for_functions;
use protobuf::ProtobufEnum as ProtobufEnum_imported_for_functions;
#[derive(PartialEq,Clone,Default)]
pub struct ValueReceived {
// message fields
pub blockHeight: u64,
pub txHash: ::std::vec::Vec<u8>,
pub value: u64,
// special fields
pub unknown_fields: ::protobuf::UnknownFields,
pub cached_size: ::protobuf::CachedSize,
}
impl ValueReceived {
pub fn new() -> ValueReceived {
::std::default::Default::default()
}
// uint64 blockHeight = 1;
pub fn clear_blockHeight(&mut self) {
self.blockHeight = 0;
}
// Param is passed by value, moved
pub fn set_blockHeight(&mut self, v: u64) {
self.blockHeight = v;
}
pub fn get_blockHeight(&self) -> u64 {
self.blockHeight
}
// bytes txHash = 2;
pub fn clear_txHash(&mut self) {
self.txHash.clear();
}
// Param is passed by value, moved
pub fn set_txHash(&mut self, v: ::std::vec::Vec<u8>) {
self.txHash = v;
}
// Mutable pointer to the field.
// If field is not initialized, it is initialized with default value first.
pub fn mut_txHash(&mut self) -> &mut ::std::vec::Vec<u8> {
&mut self.txHash
}
// Take field
pub fn take_txHash(&mut self) -> ::std::vec::Vec<u8> {
::std::mem::replace(&mut self.txHash, ::std::vec::Vec::new())
}
pub fn get_txHash(&self) -> &[u8] {
&self.txHash
}
// uint64 value = 3;
pub fn clear_value(&mut self) {
self.value = 0;
}
// Param is passed by value, moved
pub fn set_value(&mut self, v: u64) {
self.value = v;
}
pub fn get_value(&self) -> u64 {
self.value
}
}
impl ::protobuf::Message for ValueReceived {
fn is_initialized(&self) -> bool {
true
}
fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream) -> ::protobuf::ProtobufResult<()> {
while !is.eof()? {
let (field_number, wire_type) = is.read_tag_unpack()?;
match field_number {
1 => {
if wire_type != ::protobuf::wire_format::WireTypeVarint {
return ::std::result::Result::Err(::protobuf::rt::unexpected_wire_type(wire_type));
}
let tmp = is.read_uint64()?;
self.blockHeight = tmp;
},
2 => {
::protobuf::rt::read_singular_proto3_bytes_into(wire_type, is, &mut self.txHash)?;
},
3 => {
if wire_type != ::protobuf::wire_format::WireTypeVarint {
return ::std::result::Result::Err(::protobuf::rt::unexpected_wire_type(wire_type));
}
let tmp = is.read_uint64()?;
self.value = tmp;
},
_ => {
::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?;
},
};
}
::std::result::Result::Ok(())
}
// Compute sizes of nested messages
#[allow(unused_variables)]
fn compute_size(&self) -> u32 {
let mut my_size = 0;
if self.blockHeight != 0 {
my_size += ::protobuf::rt::value_size(1, self.blockHeight, ::protobuf::wire_format::WireTypeVarint);
}
if !self.txHash.is_empty() {
my_size += ::protobuf::rt::bytes_size(2, &self.txHash);
}
if self.value != 0 {
my_size += ::protobuf::rt::value_size(3, self.value, ::protobuf::wire_format::WireTypeVarint);
}
my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields());
self.cached_size.set(my_size);
my_size
}
fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream) -> ::protobuf::ProtobufResult<()> {
if self.blockHeight != 0 {
os.write_uint64(1, self.blockHeight)?;
}
if !self.txHash.is_empty() {
os.write_bytes(2, &self.txHash)?;
}
if self.value != 0 {
os.write_uint64(3, self.value)?;
}
os.write_unknown_fields(self.get_unknown_fields())?;
::std::result::Result::Ok(())
}
fn get_cached_size(&self) -> u32 {
self.cached_size.get()
}
fn get_unknown_fields(&self) -> &::protobuf::UnknownFields {
&self.unknown_fields
}
fn mut_unknown_fields(&mut self) -> &mut ::protobuf::UnknownFields {
&mut self.unknown_fields
}
fn as_any(&self) -> &::std::any::Any {
self as &::std::any::Any
}
fn as_any_mut(&mut self) -> &mut ::std::any::Any {
self as &mut ::std::any::Any
}
fn into_any(self: Box<Self>) -> ::std::boxed::Box<::std::any::Any> {
self
}
fn descriptor(&self) -> &'static ::protobuf::reflect::MessageDescriptor {
Self::descriptor_static()
}
fn new() -> ValueReceived {
ValueReceived::new()
}
fn descriptor_static() -> &'static ::protobuf::reflect::MessageDescriptor {
static mut descriptor: ::protobuf::lazy::Lazy<::protobuf::reflect::MessageDescriptor> = ::protobuf::lazy::Lazy {
lock: ::protobuf::lazy::ONCE_INIT,
ptr: 0 as *const ::protobuf::reflect::MessageDescriptor,
};
unsafe {
descriptor.get(|| {
let mut fields = ::std::vec::Vec::new();
fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeUint64>(
"blockHeight",
|m: &ValueReceived| { &m.blockHeight },
|m: &mut ValueReceived| { &mut m.blockHeight },
));
fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeBytes>(
"txHash",
|m: &ValueReceived| { &m.txHash },
|m: &mut ValueReceived| { &mut m.txHash },
));
fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeUint64>(
"value",
|m: &ValueReceived| { &m.value },
|m: &mut ValueReceived| { &mut m.value },
));
::protobuf::reflect::MessageDescriptor::new::<ValueReceived>(
"ValueReceived",
fields,
file_descriptor_proto()
)
})
}
}
fn default_instance() -> &'static ValueReceived {
static mut instance: ::protobuf::lazy::Lazy<ValueReceived> = ::protobuf::lazy::Lazy {
lock: ::protobuf::lazy::ONCE_INIT,
ptr: 0 as *const ValueReceived,
};
unsafe {
instance.get(ValueReceived::new)
}
}
}
impl ::protobuf::Clear for ValueReceived {
fn clear(&mut self) {
self.clear_blockHeight();
self.clear_txHash();
self.clear_value();
self.unknown_fields.clear();
}
}
impl ::std::fmt::Debug for ValueReceived {
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
::protobuf::text_format::fmt(self, f)
}
}
impl ::protobuf::reflect::ProtobufValue for ValueReceived {
fn as_ref(&self) -> ::protobuf::reflect::ProtobufValueRef {
::protobuf::reflect::ProtobufValueRef::Message(self)
}
}
static file_descriptor_proto_data: &'static [u8] = b"\
\n\x13ValueReceived.proto\x12\x17cash.z.wallet.sdk.proto\"O\n\rValueRece\
ived\x12\x17\n\x0bblockHeight\x18\x01\x20\x01(\x04B\x02\x18\0\x12\x12\n\
\x06txHash\x18\x02\x20\x01(\x0cB\x02\x18\0\x12\x11\n\x05value\x18\x03\
\x20\x01(\x04B\x02\x18\0B\0b\x06proto3\
";
static mut file_descriptor_proto_lazy: ::protobuf::lazy::Lazy<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::lazy::Lazy {
lock: ::protobuf::lazy::ONCE_INIT,
ptr: 0 as *const ::protobuf::descriptor::FileDescriptorProto,
};
fn parse_descriptor_proto() -> ::protobuf::descriptor::FileDescriptorProto {
::protobuf::parse_from_bytes(file_descriptor_proto_data).unwrap()
}
pub fn file_descriptor_proto() -> &'static ::protobuf::descriptor::FileDescriptorProto {
unsafe {
file_descriptor_proto_lazy.get(|| {
parse_descriptor_proto()
})
}
}