Merge pull request #105 from zcash/release/sprint-20-05

Release/sprint 20 05
This commit is contained in:
Kevin Gorham 2020-02-21 18:29:43 -05:00 committed by GitHub
commit 109673fe97
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 1807 additions and 243 deletions

View File

@ -6,12 +6,12 @@ buildscript {
]
ext.versions = [
'architectureComponents': [
'lifecycle': '2.2.0-alpha05',
'room': '2.2.0',
'paging': '2.1.0'
'lifecycle': '2.2.0',
'room': '2.2.3',
'paging': '2.1.1'
],
'grpc':'1.21.0',
'kotlin': '1.3.50',
'grpc':'1.25.0', // NOTE: cannot use a higher version because they use protobuf 3.10+ which is not compatible with 3.0+ so we'd have to implement changes in our protobuf files which breaks everything
'kotlin': '1.3.61',
'coroutines': '1.3.2',
'junitJupiter': '5.5.2'
]
@ -23,16 +23,16 @@ buildscript {
}
}
dependencies {
classpath 'com.android.tools.build:gradle:3.6.0-beta01'
classpath 'com.android.tools.build:gradle:3.6.0-rc02'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}"
classpath "org.jetbrains.kotlin:kotlin-allopen:${versions.kotlin}"
classpath "org.jetbrains.dokka:dokka-gradle-plugin:0.9.18"
classpath "com.github.ben-manes:gradle-versions-plugin:0.21.0"
classpath "org.jetbrains.dokka:dokka-gradle-plugin:0.10.1"
classpath "com.github.ben-manes:gradle-versions-plugin:0.27.0"
classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1'
classpath "com.google.protobuf:protobuf-gradle-plugin:0.8.8"
classpath 'com.getkeepsafe.dexcount:dexcount-gradle-plugin:0.8.6'
classpath "com.google.protobuf:protobuf-gradle-plugin:0.8.11"
classpath 'com.getkeepsafe.dexcount:dexcount-gradle-plugin:1.0.2'
classpath 'com.github.str4d:rust-android-gradle:68b4ecc053'
classpath 'org.owasp:dependency-check-gradle:5.2.1'
classpath 'org.owasp:dependency-check-gradle:5.3.0'
}
}
@ -51,11 +51,12 @@ apply plugin: 'org.mozilla.rust-android-gradle.rust-android'
apply plugin: 'org.owasp.dependencycheck'
group = 'cash.z.android.wallet'
version = '1.0.0-beta01'
version = '1.0.0-beta03'
repositories {
google()
jcenter()
maven { url 'https://jitpack.io' }
}
android {
@ -66,12 +67,18 @@ android {
defaultConfig {
minSdkVersion buildConfig.minSdkVersion
targetSdkVersion buildConfig.targetSdkVersion
versionCode = 1_00_00_201 // last digits are alpha(0XX) beta(2XX) rc(4XX) release(8XX). Ex: 1_08_04_401 is an release candidate build of version 1.8.4 and 1_08_04_800 would be the final release.
versionCode = 1_00_00_203 // last digits are alpha(0XX) beta(2XX) rc(4XX) release(8XX). Ex: 1_08_04_401 is an release candidate build of version 1.8.4 and 1_08_04_800 would be the final release.
versionName = "$version"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments clearPackageData: 'true'
multiDexEnabled true
archivesBaseName = "zcash-android-wallet-sdk-$versionName"
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
}
}
}
buildTypes {
@ -185,7 +192,7 @@ cargo {
dependencies {
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.appcompat:appcompat:1.2.0-alpha02'
// Architecture Components: Lifecycle
implementation "androidx.lifecycle:lifecycle-runtime:${versions.architectureComponents.lifecycle}"
@ -200,7 +207,7 @@ dependencies {
kapt "androidx.room:room-compiler:${versions.architectureComponents.room}"
// Kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${versions.kotlin}"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${versions.kotlin}"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:${versions.coroutines}"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:${versions.coroutines}"
@ -220,8 +227,8 @@ dependencies {
// Tests
testImplementation 'androidx.multidex:multidex:2.0.1'
testImplementation "org.jetbrains.kotlin:kotlin-reflect:${versions.kotlin}"
testImplementation 'org.mockito:mockito-junit-jupiter:2.26.0'
testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0'
testImplementation 'org.mockito:mockito-junit-jupiter:3.2.4'
testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0'
testImplementation "org.junit.jupiter:junit-jupiter-api:${versions.junitJupiter}"
testImplementation "org.junit.jupiter:junit-jupiter-engine:${versions.junitJupiter}"
testImplementation "org.junit.jupiter:junit-jupiter-migrationsupport:${versions.junitJupiter}"
@ -231,13 +238,19 @@ dependencies {
// Attempting to use JUnit5 via https://github.com/mannodermaus/android-junit5 was painful. The plugin configuration
// was buggy, crashing in several places. It also would require a separate test flavor because it's minimum API 26
// because "JUnit 5 uses Java 8-specific APIs that didn't exist on Android before the Oreo release."
androidTestImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0'
androidTestImplementation 'org.mockito:mockito-android:2.25.1'
androidTestImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0'
androidTestImplementation 'org.mockito:mockito-android:3.2.4'
androidTestImplementation "androidx.test:runner:1.2.0"
androidTestImplementation "androidx.test:core:1.2.0"
androidTestImplementation "androidx.arch.core:core-testing:2.1.0"
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test:runner:1.2.0'
// sample mnemonic plugin
androidTestImplementation 'com.github.zcash:zcash-android-wallet-plugins:1.0.0'
androidTestImplementation 'com.madgag.spongycastle:core:1.58.0.0'
androidTestImplementation 'io.github.novacrypto:BIP39:2019.01.27'
androidTestImplementation 'io.github.novacrypto:securestring:2019.01.27'
}
preBuild.dependsOn includeDirBugFix

View File

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip

View File

@ -0,0 +1,40 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "0ca7a6d68543409fd85d2f5bfe9b93c5",
"entities": [
{
"tableName": "compactblocks",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`height` INTEGER NOT NULL, `data` BLOB NOT NULL, PRIMARY KEY(`height`))",
"fields": [
{
"fieldPath": "height",
"columnName": "height",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "data",
"columnName": "data",
"affinity": "BLOB",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"height"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0ca7a6d68543409fd85d2f5bfe9b93c5')"
]
}
}

View File

@ -0,0 +1,345 @@
{
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "d6e9b05e0607d399f821058adb43dc15",
"entities": [
{
"tableName": "transactions",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id_tx` INTEGER, `txid` BLOB NOT NULL, `tx_index` INTEGER, `created` TEXT, `expiry_height` INTEGER, `block` INTEGER, `raw` BLOB, PRIMARY KEY(`id_tx`), FOREIGN KEY(`block`) REFERENCES `blocks`(`height`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
"fields": [
{
"fieldPath": "id",
"columnName": "id_tx",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "transactionId",
"columnName": "txid",
"affinity": "BLOB",
"notNull": true
},
{
"fieldPath": "transactionIndex",
"columnName": "tx_index",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "created",
"columnName": "created",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "expiryHeight",
"columnName": "expiry_height",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "minedHeight",
"columnName": "block",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "raw",
"columnName": "raw",
"affinity": "BLOB",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id_tx"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": [
{
"table": "blocks",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"block"
],
"referencedColumns": [
"height"
]
}
]
},
{
"tableName": "blocks",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`height` INTEGER, `hash` BLOB NOT NULL, `time` INTEGER NOT NULL, `sapling_tree` BLOB NOT NULL, PRIMARY KEY(`height`))",
"fields": [
{
"fieldPath": "height",
"columnName": "height",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "hash",
"columnName": "hash",
"affinity": "BLOB",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "time",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "saplingTree",
"columnName": "sapling_tree",
"affinity": "BLOB",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"height"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "received_notes",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id_note` INTEGER, `tx` INTEGER NOT NULL, `output_index` INTEGER NOT NULL, `account` INTEGER NOT NULL, `value` INTEGER NOT NULL, `spent` INTEGER, `diversifier` BLOB NOT NULL, `rcm` BLOB NOT NULL, `nf` BLOB NOT NULL, `is_change` INTEGER NOT NULL, `memo` BLOB, PRIMARY KEY(`id_note`), FOREIGN KEY(`tx`) REFERENCES `transactions`(`id_tx`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`account`) REFERENCES `accounts`(`account`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`spent`) REFERENCES `transactions`(`id_tx`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
"fields": [
{
"fieldPath": "id",
"columnName": "id_note",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "transactionId",
"columnName": "tx",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "outputIndex",
"columnName": "output_index",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "account",
"columnName": "account",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "value",
"columnName": "value",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "spent",
"columnName": "spent",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "diversifier",
"columnName": "diversifier",
"affinity": "BLOB",
"notNull": true
},
{
"fieldPath": "rcm",
"columnName": "rcm",
"affinity": "BLOB",
"notNull": true
},
{
"fieldPath": "nf",
"columnName": "nf",
"affinity": "BLOB",
"notNull": true
},
{
"fieldPath": "isChange",
"columnName": "is_change",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "memo",
"columnName": "memo",
"affinity": "BLOB",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id_note"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": [
{
"table": "transactions",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"tx"
],
"referencedColumns": [
"id_tx"
]
},
{
"table": "accounts",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"account"
],
"referencedColumns": [
"account"
]
},
{
"table": "transactions",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"spent"
],
"referencedColumns": [
"id_tx"
]
}
]
},
{
"tableName": "accounts",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`account` INTEGER, `extfvk` TEXT NOT NULL, `address` TEXT NOT NULL, PRIMARY KEY(`account`))",
"fields": [
{
"fieldPath": "account",
"columnName": "account",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "extendedFullViewingKey",
"columnName": "extfvk",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "address",
"columnName": "address",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"account"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "sent_notes",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id_note` INTEGER, `tx` INTEGER NOT NULL, `output_index` INTEGER NOT NULL, `from_account` INTEGER NOT NULL, `address` TEXT NOT NULL, `value` INTEGER NOT NULL, `memo` BLOB, PRIMARY KEY(`id_note`), FOREIGN KEY(`tx`) REFERENCES `transactions`(`id_tx`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`from_account`) REFERENCES `accounts`(`account`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
"fields": [
{
"fieldPath": "id",
"columnName": "id_note",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "transactionId",
"columnName": "tx",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "outputIndex",
"columnName": "output_index",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "account",
"columnName": "from_account",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "address",
"columnName": "address",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "value",
"columnName": "value",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "memo",
"columnName": "memo",
"affinity": "BLOB",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id_note"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": [
{
"table": "transactions",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"tx"
],
"referencedColumns": [
"id_tx"
]
},
{
"table": "accounts",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"from_account"
],
"referencedColumns": [
"account"
]
}
]
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd6e9b05e0607d399f821058adb43dc15')"
]
}
}

View File

@ -0,0 +1,345 @@
{
"formatVersion": 1,
"database": {
"version": 4,
"identityHash": "d6e9b05e0607d399f821058adb43dc15",
"entities": [
{
"tableName": "transactions",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id_tx` INTEGER, `txid` BLOB NOT NULL, `tx_index` INTEGER, `created` TEXT, `expiry_height` INTEGER, `block` INTEGER, `raw` BLOB, PRIMARY KEY(`id_tx`), FOREIGN KEY(`block`) REFERENCES `blocks`(`height`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
"fields": [
{
"fieldPath": "id",
"columnName": "id_tx",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "transactionId",
"columnName": "txid",
"affinity": "BLOB",
"notNull": true
},
{
"fieldPath": "transactionIndex",
"columnName": "tx_index",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "created",
"columnName": "created",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "expiryHeight",
"columnName": "expiry_height",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "minedHeight",
"columnName": "block",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "raw",
"columnName": "raw",
"affinity": "BLOB",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id_tx"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": [
{
"table": "blocks",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"block"
],
"referencedColumns": [
"height"
]
}
]
},
{
"tableName": "blocks",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`height` INTEGER, `hash` BLOB NOT NULL, `time` INTEGER NOT NULL, `sapling_tree` BLOB NOT NULL, PRIMARY KEY(`height`))",
"fields": [
{
"fieldPath": "height",
"columnName": "height",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "hash",
"columnName": "hash",
"affinity": "BLOB",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "time",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "saplingTree",
"columnName": "sapling_tree",
"affinity": "BLOB",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"height"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "received_notes",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id_note` INTEGER, `tx` INTEGER NOT NULL, `output_index` INTEGER NOT NULL, `account` INTEGER NOT NULL, `value` INTEGER NOT NULL, `spent` INTEGER, `diversifier` BLOB NOT NULL, `rcm` BLOB NOT NULL, `nf` BLOB NOT NULL, `is_change` INTEGER NOT NULL, `memo` BLOB, PRIMARY KEY(`id_note`), FOREIGN KEY(`tx`) REFERENCES `transactions`(`id_tx`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`account`) REFERENCES `accounts`(`account`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`spent`) REFERENCES `transactions`(`id_tx`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
"fields": [
{
"fieldPath": "id",
"columnName": "id_note",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "transactionId",
"columnName": "tx",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "outputIndex",
"columnName": "output_index",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "account",
"columnName": "account",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "value",
"columnName": "value",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "spent",
"columnName": "spent",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "diversifier",
"columnName": "diversifier",
"affinity": "BLOB",
"notNull": true
},
{
"fieldPath": "rcm",
"columnName": "rcm",
"affinity": "BLOB",
"notNull": true
},
{
"fieldPath": "nf",
"columnName": "nf",
"affinity": "BLOB",
"notNull": true
},
{
"fieldPath": "isChange",
"columnName": "is_change",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "memo",
"columnName": "memo",
"affinity": "BLOB",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id_note"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": [
{
"table": "transactions",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"tx"
],
"referencedColumns": [
"id_tx"
]
},
{
"table": "accounts",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"account"
],
"referencedColumns": [
"account"
]
},
{
"table": "transactions",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"spent"
],
"referencedColumns": [
"id_tx"
]
}
]
},
{
"tableName": "accounts",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`account` INTEGER, `extfvk` TEXT NOT NULL, `address` TEXT NOT NULL, PRIMARY KEY(`account`))",
"fields": [
{
"fieldPath": "account",
"columnName": "account",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "extendedFullViewingKey",
"columnName": "extfvk",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "address",
"columnName": "address",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"account"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "sent_notes",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id_note` INTEGER, `tx` INTEGER NOT NULL, `output_index` INTEGER NOT NULL, `from_account` INTEGER NOT NULL, `address` TEXT NOT NULL, `value` INTEGER NOT NULL, `memo` BLOB, PRIMARY KEY(`id_note`), FOREIGN KEY(`tx`) REFERENCES `transactions`(`id_tx`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`from_account`) REFERENCES `accounts`(`account`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
"fields": [
{
"fieldPath": "id",
"columnName": "id_note",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "transactionId",
"columnName": "tx",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "outputIndex",
"columnName": "output_index",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "account",
"columnName": "from_account",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "address",
"columnName": "address",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "value",
"columnName": "value",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "memo",
"columnName": "memo",
"affinity": "BLOB",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id_note"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": [
{
"table": "transactions",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"tx"
],
"referencedColumns": [
"id_tx"
]
},
{
"table": "accounts",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"from_account"
],
"referencedColumns": [
"account"
]
}
]
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd6e9b05e0607d399f821058adb43dc15')"
]
}
}

View File

@ -0,0 +1,118 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "ea8cbb874a6d62d7b17d7fd5ea82dc8d",
"entities": [
{
"tableName": "pending_transactions",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `toAddress` TEXT NOT NULL, `value` INTEGER NOT NULL, `memo` BLOB, `accountIndex` INTEGER NOT NULL, `minedHeight` INTEGER NOT NULL, `expiryHeight` INTEGER NOT NULL, `cancelled` INTEGER NOT NULL, `encodeAttempts` INTEGER NOT NULL, `submitAttempts` INTEGER NOT NULL, `errorMessage` TEXT, `errorCode` INTEGER, `createTime` INTEGER NOT NULL, `raw` BLOB NOT NULL, `rawTransactionId` BLOB)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "toAddress",
"columnName": "toAddress",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "value",
"columnName": "value",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "memo",
"columnName": "memo",
"affinity": "BLOB",
"notNull": false
},
{
"fieldPath": "accountIndex",
"columnName": "accountIndex",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "minedHeight",
"columnName": "minedHeight",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "expiryHeight",
"columnName": "expiryHeight",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "cancelled",
"columnName": "cancelled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "encodeAttempts",
"columnName": "encodeAttempts",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "submitAttempts",
"columnName": "submitAttempts",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "errorMessage",
"columnName": "errorMessage",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "errorCode",
"columnName": "errorCode",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "createTime",
"columnName": "createTime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "raw",
"columnName": "raw",
"affinity": "BLOB",
"notNull": true
},
{
"fieldPath": "rawTransactionId",
"columnName": "rawTransactionId",
"affinity": "BLOB",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ea8cbb874a6d62d7b17d7fd5ea82dc8d')"
]
}
}

View File

@ -1,51 +1,46 @@
package cash.z.wallet.sdk.util
import androidx.test.platform.app.InstrumentationRegistry
import cash.z.wallet.sdk.ext.TroubleshootingTwig
import cash.z.wallet.sdk.ext.Twig
import cash.z.wallet.sdk.ext.SampleSeedProvider
import cash.z.wallet.sdk.jni.RustBackend
import cash.z.wallet.sdk.secure.Wallet
import cash.z.wallet.sdk.Initializer
import cash.z.wallet.sdk.Initializer.WalletBirthday
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import okio.Okio
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Assert.assertTrue
import org.junit.Test
import java.io.IOException
import kotlin.properties.Delegates
import kotlin.properties.ReadWriteProperty
@ExperimentalCoroutinesApi
class AddressGeneratorUtil {
private val dataDbName = "AddressUtilData.db"
private val context = InstrumentationRegistry.getInstrumentation().context
private val rustBackend = RustBackend.init(context)
private val initializer = Initializer(context).open(WalletBirthday())
private val mnemonics = SimpleMnemonics()
private lateinit var wallet: Wallet
@Before
fun setup() {
Twig.plant(TroubleshootingTwig())
}
private fun deleteDb() {
context.getDatabasePath(dataDbName).absoluteFile.delete()
@Test
fun printMnemonic() {
mnemonics.apply {
val mnemonicPhrase = String(nextMnemonic())
println("example mnemonic: $mnemonicPhrase")
assertEquals(24, mnemonicPhrase.split(" ").size)
}
}
@Test
fun generateAddresses() = runBlocking {
readLines().collect { seed ->
val keyStore = initWallet(seed)
val address = wallet.getAddress()
val pk by keyStore
println("xrxrx2\t$seed\t$address\t$pk")
}
Thread.sleep(5000)
assertEquals("foo", "bar")
readLines()
.map { seedPhrase ->
mnemonics.toSeed(seedPhrase.toCharArray())
}.map { seed ->
initializer.rustBackend.deriveAddress(seed)
}.collect { address ->
println("xrxrx2\t$address")
assertTrue(address.startsWith("zs1"))
}
}
@Throws(IOException::class)
@ -60,11 +55,4 @@ class AddressGeneratorUtil {
}
}
private fun initWallet(seed: String): ReadWriteProperty<Any?, String> {
deleteDb()
val spendingKeyProvider = Delegates.notNull<String>()
wallet = Wallet(context, rustBackend, SampleSeedProvider(seed), spendingKeyProvider)
wallet.initialize()
return spendingKeyProvider
}
}

View File

@ -0,0 +1,143 @@
package cash.z.wallet.sdk.util
import androidx.test.platform.app.InstrumentationRegistry
import cash.z.wallet.sdk.Initializer
import cash.z.wallet.sdk.SdkSynchronizer
import cash.z.wallet.sdk.Synchronizer
import cash.z.wallet.sdk.block.CompactBlockDbStore
import cash.z.wallet.sdk.block.CompactBlockDownloader
import cash.z.wallet.sdk.block.CompactBlockProcessor
import cash.z.wallet.sdk.ext.TroubleshootingTwig
import cash.z.wallet.sdk.ext.Twig
import cash.z.wallet.sdk.ext.twig
import cash.z.wallet.sdk.service.LightWalletGrpcService
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import okio.Okio
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import java.io.IOException
/**
* A tool for validating an existing database and testing reorgs.
*/
@ExperimentalCoroutinesApi
class DataDbScannerUtil {
private val context = InstrumentationRegistry.getInstrumentation().context
private val host = "lightd-main.zecwallet.co"
private val port = 443
private val alias = "ScannerUtil"
// private val mnemonics = SimpleMnemonics()
// private val caceDbPath = Initializer.cacheDbPath(context, alias)
// private val downloader = CompactBlockDownloader(
// LightWalletGrpcService(context, host, port),
// CompactBlockDbStore(context, caceDbPath)
// )
// private val processor = CompactBlockProcessor(downloader)
// private val rustBackend = RustBackend.init(context, cacheDbName, dataDbName)
private val initializer = Initializer(context, host, port, alias)
private lateinit var birthday: Initializer.WalletBirthday
private val birthdayHeight = 600_000
private lateinit var synchronizer: Synchronizer
@Before
fun setup() {
Twig.plant(TroubleshootingTwig())
// cacheBlocks()
birthday = Initializer.DefaultBirthdayStore(context, birthdayHeight, alias).getBirthday()
}
private fun cacheBlocks() = runBlocking {
// twig("downloading compact blocks...")
// val latestBlockHeight = downloader.getLatestBlockHeight()
// val lastDownloaded = downloader.getLastDownloadedHeight()
// val blockRange = (Math.max(birthday, lastDownloaded))..latestBlockHeight
// downloadNewBlocks(blockRange)
// val error = validateNewBlocks(blockRange)
// twig("validation completed with result $error")
// assertEquals(-1, error)
}
private fun deleteDb(dbName: String) {
context.getDatabasePath(dbName).absoluteFile.delete()
}
@Test
fun scanExistingDb() {
initializer.open(birthday)
synchronizer = Synchronizer(context, initializer)
println("sync!")
synchronizer.start()
val scope = (synchronizer as SdkSynchronizer).coroutineScope
scope.launch {
synchronizer.status.collect { status ->
// when (status) {
println("received status of $status")
// }
}
}
println("going to sleep!")
Thread.sleep(125000)
println("I'm back and I'm out!")
synchronizer.stop()
}
//
// @Test
// fun printBalances() = runBlocking {
// readLines()
// .map { seedPhrase ->
// twig("checking balance for: $seedPhrase")
// mnemonics.toSeed(seedPhrase.toCharArray())
// }.collect { seed ->
// initializer.import(seed, birthday, clearDataDb = true, clearCacheDb = false)
// /*
// what I need to do right now
// - for each seed
// - I can reuse the cache of blocks... so just like get the cache once
// - I need to scan into a new database
// - I don't really need a new rustbackend
// - I definitely don't need a new grpc connection
// - can I just use a processor and point it to a different DB?
// + so yeah, I think I need to use the processor directly right here and just swap out its pieces
// - perhaps create a new initializer and use that to configure the processor?
// - or maybe just set the data destination for the processor
// - I might need to consider how state is impacting this design
// - can we be more stateless and thereby improve the flexibility of this code?!!!
// */
// synchronizer?.stop()
// synchronizer = Synchronizer(context, initializer)
//
//// deleteDb(dataDbPath)
//// initWallet(seed)
//// twig("scanning blocks for seed <$seed>")
////// rustBackend.scanBlocks()
//// twig("done scanning blocks for seed $seed")
////// val total = rustBackend.getBalance(0)
//// twig("found total: $total")
////// val available = rustBackend.getVerifiedBalance(0)
//// twig("found available: $available")
//// twig("xrxrx2\t$seed\t$total\t$available")
//// println("xrxrx2\t$seed\t$total\t$available")
// }
//
// Thread.sleep(5000)
// assertEquals("foo", "bar")
// }
}

View File

@ -0,0 +1,82 @@
package cash.z.wallet.sdk.util
import cash.z.android.plugin.MnemonicPlugin
import io.github.novacrypto.bip39.MnemonicGenerator
import io.github.novacrypto.bip39.SeedCalculator
import io.github.novacrypto.bip39.Words
import io.github.novacrypto.bip39.wordlists.English
import java.security.SecureRandom
class SimpleMnemonics : MnemonicPlugin {
override fun nextEntropy(): ByteArray {
return ByteArray(Words.TWENTY_FOUR.byteLength()).apply {
SecureRandom().nextBytes(this)
}
}
override fun nextMnemonic(): CharArray {
return nextMnemonic(nextEntropy())
}
override fun nextMnemonic(entropy: ByteArray): CharArray {
return StringBuilder().let { builder ->
MnemonicGenerator(English.INSTANCE).createMnemonic(entropy) { c ->
builder.append(c)
}
builder.toString().toCharArray()
}
}
override fun nextMnemonicList(): List<CharArray> {
return nextMnemonicList(nextEntropy())
}
override fun nextMnemonicList(entropy: ByteArray): List<CharArray> {
return WordListBuilder().let { builder ->
MnemonicGenerator(English.INSTANCE).createMnemonic(entropy) { c ->
builder.append(c)
}
builder.wordList
}
}
override fun toSeed(mnemonic: CharArray): ByteArray {
return SeedCalculator().calculateSeed(String(mnemonic), "")
}
override fun toWordList(mnemonic: CharArray): List<CharArray> {
val wordList = mutableListOf<CharArray>()
var cursor = 0
repeat(mnemonic.size) { i ->
val isSpace = mnemonic[i] == ' '
if (isSpace || i == (mnemonic.size - 1)) {
val wordSize = i - cursor + if (isSpace) 0 else 1
wordList.add(CharArray(wordSize).apply {
repeat(wordSize) {
this[it] = mnemonic[cursor + it]
}
})
cursor = i + 1
}
}
return wordList
}
class WordListBuilder {
val wordList = mutableListOf<CharArray>()
fun append(c: CharSequence) {
if (c[0] != English.INSTANCE.space) addWord(c)
}
private fun addWord(c: CharSequence) {
c.length.let { size ->
val word = CharArray(size)
repeat(size) {
word[it] = c[it]
}
wordList.add(word)
}
}
}
}

View File

@ -1,3 +1,17 @@
seed-1
seed-2
seed-3
urban kind wise collect social marble riot primary craft lucky head cause syrup odor artist decorate rhythm phone style benefit portion bus truck top
wish puppy smile loan doll curve hole maze file ginger hair nose key relax knife witness cannon grab despair throw review deal slush frame
labor elite banana cement royal tiger smile robust talk street bread bitter admit spy leg alcohol opinion mimic crane bid damp trigger wagon share
icon future member loan initial music bless cigar artist cross scorpion disease click else palm recall obscure horse wire energy frost route stone raven
way fruit group range army seven stem ridge panel duty deal like mango engage adult market drama large year love clay desert culture evoke
stairs bridge romance offer bronze organ soldier point unveil soup figure economy purity rapid eight error make goat poet when letter gold coil gate
execute thing home flat rare pitch plug poverty never design cute essay mosquito unhappy pen phone aerobic basket empower system extend concert leopard leopard
thought balcony raw renew sister define isolate bridge rigid critic extra enhance accuse skin either lock owner boat grid legal coral judge oyster olympic
pull curious short apology slot giraffe island caution cricket attract episode acoustic age fly crucial earth broccoli eternal eyebrow marriage lazy thank actor police
army boat guess direct network version mean rice brown sauce bronze health stable way proud gift primary reason company raw sorry virtual other ahead
humble educate desert govern quality cup illness spatial whale zoo novel hollow velvet erosion gadget glove great occur milk staff gravity word skate soul
horror scene device ahead before blossom surface staff shrug horse wood drill style garage north account twice easily slam require nose sentence catalog mango
bronze this era window wonder strike label grid keep paddle kiwi age input flock just eagle coil like toward burst mobile obtain giant idle
aisle dwarf bulb catch anxiety follow attack that habit exclude laptop spoon enough walnut picture reward pact license behind question save cover exotic drip
two length electric immune antique rotate junior spoon torch liberty eyebrow shoe army away horn anger oak chase grow ride enrich soft push orient
bike crunch vintage smoke okay screen side pattern thrive top timber payment flight garment lift heavy enable sting humble obscure reveal art kangaroo owner
treat stumble only reward else turtle across shop vocal dynamic goddess toss review polar enable plate process cabin injury rifle sword group agree slush

View File

@ -50,7 +50,7 @@ class IntegrationTest {
fun testBalance() = runBlocking {
var availableBalance: Long = 0L
synchronizer.balances.onFirst {
availableBalance = it.available
availableBalance = it.availableZatoshi
}
synchronizer.status.filter { it == SYNCED }.onFirst {
@ -65,7 +65,7 @@ class IntegrationTest {
@Ignore
fun testSpend() = runBlocking {
var success = false
synchronizer.balances.filter { it.available > 0 }.onEach {
synchronizer.balances.filter { it.availableZatoshi > 0 }.onEach {
success = sendFunds()
}.first()
log("asserting $success")

View File

@ -8,7 +8,9 @@ import cash.z.wallet.sdk.ext.*
import cash.z.wallet.sdk.jni.RustBackend
import com.google.gson.Gson
import com.google.gson.stream.JsonReader
import java.io.File
import java.io.InputStreamReader
import java.util.*
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
@ -29,17 +31,20 @@ class Initializer(
}
/**
* The path this initializer will use when creating instances of Rustbackend. This value is
* derived from the appContext when this class is constructed.
*/
private val dbPath: String = appContext.getDatabasePath("unused.db").parentFile?.absolutePath
?: throw InitializerException.DatabasePathException
/**
* The path this initializer will use when cheching for and downloaading sapling params. This
* The path this initializer will use when checking for and downloading sapling params. This
* value is derived from the appContext when this class is constructed.
*/
private val paramPath: String = "${appContext.cacheDir.absolutePath}/params"
private val pathParams: String = "${appContext.cacheDir.absolutePath}/params"
/**
* The path used for storing cached compact blocks for processing.
*/
private val pathCacheDb: String = cacheDbPath(appContext, alias)
/**
* The path used for storing the data derived from the cached compact blocks.
*/
private val pathDataDb: String = dataDbPath(appContext, alias)
/**
* A wrapped version of [cash.z.wallet.sdk.jni.RustBackendWelding] that will be passed to the
@ -65,43 +70,76 @@ class Initializer(
* Initialize a new wallet with the given seed and birthday. It creates the required database
* tables and loads and configures the [rustBackend] property for use by all other components.
*
* @param seed the seed to use for the newly created wallet.
* @param newWalletBirthday the birthday to use for the newly created wallet. Typically, this
* corresponds to the most recent checkpoint available since new wallets should not have any
* transactions prior to their creation.
* @param numberOfAccounts the number of accounts to create for this wallet. This is not fully
* supported so the default value of 1 is recommended.
* @param clearCacheDb when true, this will delete cacheDb, if it exists, resulting in the fresh
* download of all compact blocks. Otherwise, downloading resumes from the last fetched block.
* @param clearDataDb when true, this will delete the dataDb, if it exists, resulting in the
* fresh scan of all blocks. Otherwise, initialization crashes when previous wallet data exists
* to prevent accidental overwrites.
*
* @return the account spending keys, corresponding to the accounts that get initialized in the
* DB.
* @throws InitializerException.AlreadyInitializedException when the blocks table already exists
* and [clearDataDb] is false.
*/
fun new(
seed: ByteArray,
newWalletBirthday: WalletBirthday,
numberOfAccounts: Int = 1,
overwrite: Boolean = false
clearCacheDb: Boolean = false,
clearDataDb: Boolean = false
): Array<String> {
initRustLibrary()
return initializeAccounts(seed, newWalletBirthday, numberOfAccounts, overwrite)
return initializeAccounts(seed, newWalletBirthday, numberOfAccounts,
clearCacheDb = clearCacheDb, clearDataDb = clearDataDb)
}
/**
/**
* Initialize a new wallet with the imported seed and birthday. It creates the required database
* tables and loads and configures the [rustBackend] property for use by all other components.
*
* @param seed the seed to use for the imported wallet.
* @param previousWalletBirthday the birthday to use for the imported. Typically, this
* corresponds to the height where this wallet was first created, allowing the wallet to be
* optimized not to download or scan blocks from before the wallet existed.
* @param clearCacheDb when true, this will delete cacheDb, if it exists, resulting in the fresh
* download of all compact blocks. Otherwise, downloading resumes from the last fetched block.
* @param clearDataDb when true, this will delete the dataDb, if it exists, resulting in the
* fresh scan of all blocks. Otherwise, this function throws an exception when previous wallet
* data exists to prevent accidental overwrites.
*
* @return the account spending keys, corresponding to the accounts that get initialized in the
* DB.
* @throws InitializerException.AlreadyInitializedException when the blocks table already exists
* and [clearDataDb] is false.
*/
fun import(
seed: ByteArray,
previousWalletBirthday: WalletBirthday,
overwrite: Boolean = false
clearCacheDb: Boolean = false,
clearDataDb: Boolean = false
): Array<String> {
initRustLibrary()
return initializeAccounts(seed, previousWalletBirthday, overwrite = overwrite)
return initializeAccounts(seed, previousWalletBirthday,
clearCacheDb = clearCacheDb, clearDataDb = clearDataDb)
}
/**
* Loads the rust library and previously used birthday for use by all other components. This is
* the most common use case for the initializer--reopening a wallet that was previously created.
*
* @param birthday birthday height of the wallet. This value is passed to the
* [CompactBlockProcessor] and becomes a factor in determining the lower bounds height that this
* wallet will use. This height helps with determining where to start downloading as well as how
* far back to go during a rewind. Every wallet has a birthday and the initializer depends on
* this value but does not own it.
*/
fun open(birthday: WalletBirthday): Initializer {
twig("Opening wallet with birthday ${birthday.height}")
initRustLibrary()
rustBackend.birthdayHeight = birthday.height
requireRustBackend().birthdayHeight = birthday.height
return this
}
@ -119,21 +157,22 @@ class Initializer(
seed: ByteArray,
birthday: WalletBirthday,
numberOfAccounts: Int = 1,
overwrite: Boolean = false
clearCacheDb: Boolean = false,
clearDataDb: Boolean = false
): Array<String> {
this.birthday = birthday
twig("Initializing accounts with birthday ${birthday.height}")
try {
if (overwrite) rustBackend.clear()
requireRustBackend().clear(clearCacheDb, clearDataDb)
// only creates tables, if they don't exist
rustBackend.initDataDb()
requireRustBackend().initDataDb()
twig("Initialized wallet for first run")
} catch (t: Throwable) {
throw InitializerException.FalseStart(t)
}
try {
rustBackend.initBlocksTable(
requireRustBackend().initBlocksTable(
birthday.height,
birthday.hash,
birthday.time,
@ -142,14 +181,14 @@ class Initializer(
twig("seeded the database with sapling tree at height ${birthday.height}")
} catch (t: Throwable) {
if (t.message?.contains("is not empty") == true) {
throw InitializerException.AlreadyInitializedException(t, rustBackend.dbDataPath)
throw InitializerException.AlreadyInitializedException(t, rustBackend.pathDataDb)
} else {
throw InitializerException.FalseStart(t)
}
}
try {
return rustBackend.initAccountsTable(seed, numberOfAccounts).also {
return requireRustBackend().initAccountsTable(seed, numberOfAccounts).also {
twig("Initialized the accounts table with ${numberOfAccounts} account(s)")
}
} catch (t: Throwable) {
@ -166,11 +205,16 @@ class Initializer(
}
/**
* Lazily initializes the rust backend, using values that were captured from the appContext
* that was passed to the constructor.
* Internal function used to initialize the [rustBackend] before use. Initialization should only
* happen as a result of [new], [import] or [open] being called or as part of stand-alone key
* derivation.
*/
private fun initRustLibrary() {
if (!isInitialized) rustBackend = RustBackend().init(dbPath, paramPath, alias)
private fun requireRustBackend(): RustBackend {
if (!isInitialized) {
twig("Initializing cache: $pathCacheDb data: $pathDataDb params: $pathParams")
rustBackend = RustBackend().init(pathCacheDb, pathDataDb, pathParams)
}
return rustBackend
}
@ -184,31 +228,66 @@ class Initializer(
*
* @return the spending keys that correspond to the seed, formatted as Strings.
*/
fun deriveSpendingKeys(seed: ByteArray, numberOfAccounts: Int = 1): Array<String> {
initRustLibrary()
return rustBackend.deriveSpendingKeys(seed, numberOfAccounts)
}
fun deriveSpendingKeys(seed: ByteArray, numberOfAccounts: Int = 1): Array<String> =
requireRustBackend().deriveSpendingKeys(seed, numberOfAccounts)
/**
* Given a seed and a number of accounts, return the associated viewing keys.
*
* @return the viewing keys that correspond to the seed, formatted as Strings.
*/
fun deriveViewingKeys(seed: ByteArray, numberOfAccounts: Int = 1): Array<String> {
initRustLibrary()
return rustBackend.deriveViewingKeys(seed, numberOfAccounts)
}
fun deriveViewingKeys(seed: ByteArray, numberOfAccounts: Int = 1): Array<String> =
requireRustBackend().deriveViewingKeys(seed, numberOfAccounts)
/**
* Given a spending key, return the associated viewing key.
*
* @return the viewing key that corresponds to the spending key.
*/
fun deriveViewingKey(spendingKey: String): String = rustBackend.deriveViewingKey(spendingKey)
fun deriveViewingKey(spendingKey: String): String =
requireRustBackend().deriveViewingKey(spendingKey)
/**
* Given a seed and account index, return the associated address.
*
* @return the address that corresponds to the seed and account index.
*/
fun deriveAddress(seed: ByteArray, accountIndex: Int) =
requireRustBackend().deriveAddress(seed, accountIndex)
/**
* Given a viewing key string, return the associated address.
*
* @return the address that corresponds to the viewing key.
*/
fun deriveAddress(viewingKey: String) =
requireRustBackend().deriveAddress(viewingKey)
companion object {
//
// Path Helpers
//
fun cacheDbPath(appContext: Context, alias: String): String =
aliasToPath(appContext, alias, ZcashSdk.DB_CACHE_NAME)
fun dataDbPath(appContext: Context, alias: String): String =
aliasToPath(appContext, alias, ZcashSdk.DB_DATA_NAME)
private fun aliasToPath(appContext: Context, alias: String, dbFileName: String): String {
val parentDir: String =
appContext.getDatabasePath("unused.db").parentFile?.absolutePath
?: throw InitializerException.DatabasePathException
val prefix = if (alias.endsWith('_')) alias else "${alias}_"
return File(parentDir, "$prefix$dbFileName").absolutePath
}
}
/**
* Model object for holding wallet birthdays. It is only used by this class.
* Model object for holding wallet birthday. It is only used by this class.
*/
data class WalletBirthday(
val height: Int = -1,
@ -241,7 +320,7 @@ class Initializer(
class DefaultBirthdayStore(
private val appContext: Context,
private val importedBirthdayHeight: Int? = null,
val alias: String = "default_prefs"
val alias: String = DEFAULT_ALIAS
) : WalletBirthdayStore {
/**
@ -267,12 +346,6 @@ class Initializer(
init {
validateAlias(alias)
if (importedBirthdayHeight != null) {
saveBirthdayToPrefs(
prefs,
loadBirthdayFromAssets(appContext, importedBirthdayHeight)
)
}
}
override fun hasExistingBirthday(): Boolean = loadBirthdayFromPrefs(prefs) != null
@ -350,6 +423,26 @@ class Initializer(
*/
private const val BIRTHDAY_DIRECTORY = "zcash/saplingtree"
const val DEFAULT_ALIAS = "default_prefs"
// Constructor function
fun NewWalletBirthdayStore(appContext: Context, alias: String = DEFAULT_ALIAS): WalletBirthdayStore {
return DefaultBirthdayStore(appContext, alias = alias).apply {
setBirthday(newWalletBirthday)
}
}
// Constructor function
fun ImportedWalletBirthdayStore(appContext: Context, importedBirthdayHeight: Int?, alias: String = DEFAULT_ALIAS): WalletBirthdayStore {
return DefaultBirthdayStore(appContext, alias = alias).apply {
if (importedBirthdayHeight != null) {
saveBirthdayToPrefs(prefs, loadBirthdayFromAssets(appContext, importedBirthdayHeight))
} else {
setBirthday(newWalletBirthday)
}
}
}
/**
* Load the given birthday file from the assets of the given context. When no height is
* specified, we default to the file with the greatest name.
@ -376,7 +469,7 @@ class Initializer(
if (treeFiles.isNullOrEmpty()) throw BirthdayException.MissingBirthdayFilesException(
BIRTHDAY_DIRECTORY
)
twig("found ${treeFiles.size} sapling tree checkpoints: $treeFiles")
twig("found ${treeFiles.size} sapling tree checkpoints: ${Arrays.toString(treeFiles)}")
val file: String
try {
file = if (birthdayHeight == null) treeFiles.first() else {

View File

@ -121,6 +121,12 @@ class SdkSynchronizer internal constructor(
*/
override var onSubmissionErrorHandler: ((Throwable?) -> Boolean)? = null
/**
* A callback to invoke whenever a chain error is encountered. These occur whenever the
* processor detects a missing or non-chain-sequential block (i.e. a reorg).
*/
override var onChainErrorHandler: ((Int, Int) -> Any)? = null
//
// Public API
@ -176,12 +182,14 @@ class SdkSynchronizer internal constructor(
}
suspend fun refreshBalance() {
twig("refreshing balance")
_balances.send(processor.getBalanceInfo())
}
private fun CoroutineScope.onReady() = launch(CoroutineExceptionHandler(::onCriticalError)) {
twig("Synchronizer Ready. Starting processor!")
processor.onErrorListener = ::onProcessorError
twig("Synchronizer (${this@SdkSynchronizer}) Ready. Starting processor!")
processor.onProcessorErrorListener = ::onProcessorError
processor.onChainErrorListener = ::onChainError
processor.state.onEach {
when (it) {
is Scanned -> {
@ -237,6 +245,18 @@ class SdkSynchronizer internal constructor(
} == true
}
private fun onChainError(errorHeight: Int, rewindHeight: Int) {
twig("Chain error detected at height: $errorHeight. Rewinding to: $rewindHeight")
if (onChainErrorHandler == null) {
twig(
"WARNING: a chain error occurred but no callback is registered to be notified of " +
"chain errors. To respond to these errors (perhaps to update the UI or alert the" +
" user) set synchronizer.onChainErrorHandler to a non-null value"
)
}
onChainErrorHandler?.invoke(errorHeight, rewindHeight)
}
private suspend fun onScanComplete(scannedRange: IntRange) {
// TODO: optimize to skip logic here if there are no new transactions with a block height
// within the given range
@ -351,17 +371,17 @@ fun Synchronizer(
if (seed != null && birthdayStore.hasExistingBirthday()) {
twig("Initializing existing wallet")
initializer.open(birthdayStore.getBirthday())
twig("${initializer.rustBackend.dbDataPath}")
twig("${initializer.rustBackend.pathDataDb}")
} else {
require(seed != null) {
"Failed to initialize. A seed is required when no wallet exists on the device."
}
if (birthdayStore.hasImportedBirthday()) {
twig("Initializing new wallet")
initializer.new(seed, birthdayStore.newWalletBirthday, overwrite = true)
initializer.new(seed, birthdayStore.newWalletBirthday, 1, true, true)
} else {
twig("Initializing imported wallet")
initializer.import(seed, birthdayStore.getBirthday(), overwrite = true)
initializer.import(seed, birthdayStore.getBirthday(), true, true)
}
}
return Synchronizer(appContext, initializer)
@ -389,8 +409,8 @@ fun Synchronizer(
lightwalletdHost: String = ZcashSdk.DEFAULT_LIGHTWALLETD_HOST,
lightwalletdPort: Int = ZcashSdk.DEFAULT_LIGHTWALLETD_PORT,
ledger: TransactionRepository =
PagedTransactionRepository(appContext, 1000, rustBackend.dbDataPath), // TODO: fix this pagesize bug, small pages should not crash the app. It crashes with: Uncaught Exception: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views. and is probably related to FlowPagedList
blockStore: CompactBlockStore = CompactBlockDbStore(appContext, rustBackend.dbCachePath),
PagedTransactionRepository(appContext, 1000, rustBackend.pathDataDb), // TODO: fix this pagesize bug, small pages should not crash the app. It crashes with: Uncaught Exception: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views. and is probably related to FlowPagedList
blockStore: CompactBlockStore = CompactBlockDbStore(appContext, rustBackend.pathCacheDb),
service: LightWalletService = LightWalletGrpcService(appContext, lightwalletdHost, lightwalletdPort),
encoder: TransactionEncoder = WalletTransactionEncoder(rustBackend, ledger),
downloader: CompactBlockDownloader = CompactBlockDownloader(service, blockStore),

View File

@ -187,6 +187,13 @@ interface Synchronizer {
*/
var onSubmissionErrorHandler: ((Throwable?) -> Boolean)?
/**
* A callback to invoke whenever a chain error is encountered. These occur whenever the
* processor detects a missing or non-chain-sequential block (i.e. a reorg).
*/
var onChainErrorHandler: ((Int, Int) -> Any)?
enum class Status {
/**
* Indicates that [stop] has been called on this Synchronizer and it will no longer be used.
@ -232,4 +239,5 @@ interface Synchronizer {
val isNotValid get() = this !is Valid
}
}

View File

@ -45,4 +45,8 @@ class CompactBlockDbStore(
override suspend fun rewindTo(height: Int) = withContext(IO) {
cacheDao.rewindTo(height)
}
override fun close() {
cacheDb.close()
}
}

View File

@ -37,5 +37,10 @@ open class CompactBlockDownloader(
compactBlockStore.getLatestHeight()
}
fun stop() {
lightwalletService.shutdown()
compactBlockStore.close()
}
}

View File

@ -45,7 +45,8 @@ class CompactBlockProcessor(
private val rustBackend: RustBackendWelding,
minimumHeight: Int = SAPLING_ACTIVATION_HEIGHT
) {
var onErrorListener: ((Throwable) -> Boolean)? = null
var onProcessorErrorListener: ((Throwable) -> Boolean)? = null
var onChainErrorListener: ((Int, Int) -> Any)? = null
private val consecutiveChainErrors = AtomicInteger(0)
private val lowerBoundHeight: Int = max(SAPLING_ACTIVATION_HEIGHT, minimumHeight - MAX_REORG_SIZE)
@ -74,7 +75,7 @@ class CompactBlockProcessor(
// using do/while makes it easier to execute exactly one loop which helps with testing this processor quickly
// (because you can start and then immediately set isStopped=true to always get precisely one loop)
do {
retryWithBackoff(::onConnectionError, maxDelayMillis = MAX_BACKOFF_INTERVAL) {
retryWithBackoff(::onProcessorError, maxDelayMillis = MAX_BACKOFF_INTERVAL) {
val result = processNewBlocks()
// immediately process again after failures in order to download new blocks right away
if (result < 0) {
@ -96,10 +97,19 @@ class CompactBlockProcessor(
stop()
}
/**
* Sets the state to [Stopped], which causes the processor loop to exit.
*/
suspend fun stop() {
setState(Stopped)
runCatching {
setState(Stopped)
downloader.stop()
}
}
/**
* Stop processing and throw an error.
*/
private suspend fun fail(error: Throwable) {
stop()
twig("${error.message}")
@ -113,9 +123,25 @@ class CompactBlockProcessor(
* return the block height where an error was found.
*/
private suspend fun processNewBlocks(): Int = withContext(IO) {
verifySetup()
twig("beginning to process new blocks (with lower bound: $lowerBoundHeight)...")
// Get the latest info (but don't transmit it on the channel) and then use that to update the scan/download ranges
updateRanges()
if (currentInfo.lastDownloadRange.isEmpty() && currentInfo.lastScanRange.isEmpty()) {
twig("Nothing to process: no new blocks to download or scan, right now.")
setState(Scanned(currentInfo.lastScanRange))
-1
} else {
downloadNewBlocks(currentInfo.lastDownloadRange)
validateAndScanNewBlocks(currentInfo.lastScanRange)
}
}
/**
* Gets the latest range info and then uses that initialInfo to update (and transmit)
* the scan/download ranges that require processing.
*/
private suspend fun updateRanges() = withContext(IO) {
ProcessorInfo(
networkBlockHeight = downloader.getLatestBlockHeight(),
lastScannedHeight = getLastScannedHeight(),
@ -126,38 +152,50 @@ class CompactBlockProcessor(
lastScannedHeight = initialInfo.lastScannedHeight,
lastDownloadedHeight = initialInfo.lastDownloadedHeight,
lastScanRange = (initialInfo.lastScannedHeight + 1)..initialInfo.networkBlockHeight,
lastDownloadRange = (max(initialInfo.lastDownloadedHeight, initialInfo.lastScannedHeight) + 1)..initialInfo.networkBlockHeight
lastDownloadRange = (max(
initialInfo.lastDownloadedHeight,
initialInfo.lastScannedHeight
) + 1)..initialInfo.networkBlockHeight
)
}
if (currentInfo.lastDownloadRange.isEmpty() && currentInfo.lastScanRange.isEmpty()) {
twig("Nothing to process: no new blocks to download or scan, right now.")
setState(Scanned(currentInfo.lastScanRange))
-1
} else {
setState(Downloading)
downloadNewBlocks(currentInfo.lastDownloadRange)
setState(Validating)
var error = validateNewBlocks(currentInfo.lastScanRange)
if (error < 0) {
// in theory, a scan should not fail after validation succeeds but maybe consider
// changing the rust layer to return the failed block height whenever scan does fail
// rather than a boolean
setState(Scanning)
val success = scanNewBlocks(currentInfo.lastScanRange)
if (!success) throw CompactBlockProcessorException.FailedScan
else {
setState(Scanned(currentInfo.lastScanRange))
}
-1
} else {
error
}
}
}
/**
* Given a range, validate and then scan all blocks. Validation is ensuring that the blocks are
* in ascending order, with no gaps and are also chain-sequential. This means every block's
* prevHash value matches the preceding block in the chain.
*
* @return error code or -1 when there is no error.
*/
private suspend fun validateAndScanNewBlocks(lastScanRange: IntRange): Int = withContext(IO) {
setState(Validating)
var error = validateNewBlocks(lastScanRange)
if (error < 0) {
// in theory, a scan should not fail after validation succeeds but maybe consider
// changing the rust layer to return the failed block height whenever scan does fail
// rather than a boolean
setState(Scanning)
val success = scanNewBlocks(lastScanRange)
if (!success) throw CompactBlockProcessorException.FailedScan()
else {
setState(Scanned(lastScanRange))
}
-1
} else {
error
}
}
/**
* Confirm that the wallet data is properly setup for use.
*/
private fun verifySetup() {
if (!repository.isInitialized()) throw CompactBlockProcessorException.Uninitialized
}
/**
* Download all blocks in the given range.
*/
@VisibleForTesting //allow mocks to verify how this is called, rather than the downloader, which is more complex
internal suspend fun downloadNewBlocks(range: IntRange) = withContext<Unit>(IO) {
if (range.isEmpty()) {
@ -174,7 +212,7 @@ class CompactBlockProcessor(
var progress: Int
twig("found $missingBlockCount missing blocks, downloading in $batches batches of ${DOWNLOAD_BATCH_SIZE}...")
for (i in 1..batches) {
retryUpTo(RETRIES) {
retryUpTo(RETRIES, { CompactBlockProcessorException.FailedDownload(it) }) {
val end = min((range.first + (i * DOWNLOAD_BATCH_SIZE)) - 1, range.last) // subtract 1 on the first value because the range is inclusive
var count = 0
twig("downloaded $downloadedBlockHeight..$end (batch $i of $batches) [${downloadedBlockHeight..end}]") {
@ -183,7 +221,7 @@ class CompactBlockProcessor(
twig("downloaded $count blocks!")
progress = (i / batches.toFloat() * 100).roundToInt()
_progress.send(progress)
updateProgress(lastDownloadedHeight = downloader.getLastDownloadedHeight().also { twig("updating lastDownloadedHeight=$it") })
updateProgress(lastDownloadedHeight = downloader.getLastDownloadedHeight())
downloadedBlockHeight = end
}
}
@ -192,18 +230,27 @@ class CompactBlockProcessor(
_progress.send(100)
}
/**
* Validate all blocks in the given range, ensuring that the blocks are in ascending order, with
* no gaps and are also chain-sequential. This means every block's prevHash value matches the
* preceding block in the chain.
*/
private fun validateNewBlocks(range: IntRange?): Int {
if (range?.isEmpty() != false) {
twig("no blocks to validate: $range")
return -1
}
Twig.sprout("validating")
twig("validating blocks in range $range in db: ${(rustBackend as RustBackend).dbCachePath}")
twig("validating blocks in range $range in db: ${(rustBackend as RustBackend).pathCacheDb}")
val result = rustBackend.validateCombinedChain()
Twig.clip("validating")
return result
}
/**
* Scan all blocks in the given range, decrypting anything that matches our wallet and storing
* the data.
*/
private suspend fun scanNewBlocks(range: IntRange?): Boolean = withContext(IO) {
if (range?.isEmpty() != false) {
twig("no blocks to scan for range $range")
@ -212,13 +259,15 @@ class CompactBlockProcessor(
Twig.sprout("scanning")
twig("scanning blocks for range $range in batches")
var result = false
retryUpTo(3, 500L) {failedAttempts ->
// Attempt to scan a few times to work around any concurrent modification errors, then
// rethrow as an official processorError which is handled by [start.retryWithBackoff]
retryUpTo(3, { CompactBlockProcessorException.FailedScan(it) }) { failedAttempts ->
if (failedAttempts > 0) twig("retrying the scan after $failedAttempts failure(s)...")
do {
var scannedNewBlocks = false
result = rustBackend.scanBlocks(SCAN_BATCH_SIZE)
val lastScannedHeight = getLastScannedHeight()
twig("batch scan complete. Last scanned height: $lastScannedHeight target height: ${range.last}")
twig("batch scanned: $lastScannedHeight/${range.last}")
if (currentInfo.lastScannedHeight != lastScannedHeight) {
scannedNewBlocks = true
updateProgress(lastScannedHeight = lastScannedHeight)
@ -254,20 +303,19 @@ class CompactBlockProcessor(
lastScanRange = lastScanRange,
lastDownloadRange = lastDownloadRange
)
twig("Sending updated currentInfo: $currentInfo")
_processorInfo.send(currentInfo)
}
private suspend fun handleChainError(errorHeight: Int) = withContext(IO) {
val lowerBound = determineLowerBound(errorHeight)
twig("handling chain error at $errorHeight by rewinding to block $lowerBound")
onChainErrorListener?.invoke(errorHeight, lowerBound)
rustBackend.rewindToHeight(lowerBound)
downloader.rewindToHeight(lowerBound)
}
private fun onConnectionError(throwable: Throwable): Boolean {
_state.offer(Disconnected)
return onErrorListener?.invoke(throwable) ?: true
private fun onProcessorError(throwable: Throwable): Boolean {
return onProcessorErrorListener?.invoke(throwable) ?: true
}
private fun determineLowerBound(errorHeight: Int): Int {
@ -299,9 +347,9 @@ class CompactBlockProcessor(
twigTask("checking balance info") {
try {
val balanceTotal = rustBackend.getBalance(accountIndex)
twig("found total balance of: $balanceTotal")
twig("found total balance: $balanceTotal")
val balanceAvailable = rustBackend.getVerifiedBalance(accountIndex)
twig("found available balance of: $balanceAvailable")
twig("found available balance: $balanceAvailable")
WalletBalance(balanceTotal, balanceAvailable)
} catch (t: Throwable) {
twig("failed to get balance due to $t")

View File

@ -23,4 +23,9 @@ interface CompactBlockStore {
* Meaning, if max height is 100 block and rewindTo(50) is called, then the highest block remaining will be 49.
*/
suspend fun rewindTo(height: Int)
/**
* Close any connections to the block store.
*/
fun close()
}

View File

@ -11,7 +11,7 @@ import cash.z.wallet.sdk.entity.CompactBlockEntity
@Database(
entities = [CompactBlockEntity::class],
version = 1,
exportSchema = false
exportSchema = true
)
abstract class CompactBlockDb : RoomDatabase() {
abstract fun complactBlockDao(): CompactBlockDao

View File

@ -20,7 +20,7 @@ import cash.z.wallet.sdk.entity.*
Sent::class
],
version = 3,
exportSchema = false
exportSchema = true
)
abstract class DerivedDataDb : RoomDatabase() {
abstract fun transactionDao(): TransactionDao
@ -168,11 +168,11 @@ interface TransactionDao {
WHERE ( transactions.raw IS NULL
AND received_notes.is_change != 1 )
OR ( transactions.raw IS NOT NULL )
ORDER BY ( minedheight IS NOT NULL ),
minedheight DESC,
blocktimeinseconds DESC,
id DESC
ORDER BY blocktimeinseconds DESC,
minedHeight DESC,
id DESC
LIMIT :limit
""")
fun getAllTransactions(limit: Int = Int.MAX_VALUE): DataSource.Factory<Int, ConfirmedTransaction>
}
}

View File

@ -14,7 +14,7 @@ import kotlinx.coroutines.flow.Flow
PendingTransactionEntity::class
],
version = 1,
exportSchema = false
exportSchema = true
)
abstract class PendingTransactionDb : RoomDatabase() {
abstract fun pendingTransactionDao(): PendingTransactionDao

View File

@ -39,8 +39,13 @@ sealed class CompactBlockProcessorException(message: String, cause: Throwable? =
" than just the database filename because Rust does not access the app Context." +
" So pass in context.getDatabasePath(dbFileName).absolutePath instead of just dbFileName alone.", null)
class FailedReorgRepair(message: String) : CompactBlockProcessorException(message)
object FailedScan : CompactBlockProcessorException("Error while scanning blocks. This most " +
"likely means a block is missing or a reorg was mishandled. See Rust logs for details.")
class FailedDownload(cause: Throwable? = null) : CompactBlockProcessorException("Error while downloading blocks. This most " +
"likely means the server is down or slow to respond. See logs for details.", cause)
class FailedScan(cause: Throwable? = null) : CompactBlockProcessorException("Error while scanning blocks. This most " +
"likely means a block was missed or a reorg was mishandled. See logs for details.", cause)
object Uninitialized : CompactBlockProcessorException("Cannot process blocks because the wallet has not been" +
" initialized. Verify that the seed phrase was properly created or imported. If so, then this problem" +
" can be fixed by re-importing the wallet.")
}
sealed class CompactBlockStreamException(message: String, cause: Throwable? = null) : SdkException(message, cause) {
@ -77,8 +82,8 @@ sealed class InitializerException(message: String, cause: Throwable? = null) :
" because it already exists in $dbPath", cause)
object DatabasePathException :
InitializerException("Critical failure to locate path for storing databases. Perhaps this" +
" device prevents apps from storing data? We cannot manage initialize the wallet" +
" unless we can store data.")
" device prevents apps from storing data? We cannot initialize the wallet unless" +
" we can store data.")
}
sealed class LightwalletException(message: String, cause: Throwable? = null) : SdkException(message, cause) {

View File

@ -1,6 +1,8 @@
package cash.z.wallet.sdk.ext
import java.util.concurrent.CopyOnWriteArraySet
import kotlin.math.roundToInt
import kotlin.math.roundToLong
internal typealias Leaf = String
@ -129,10 +131,10 @@ inline fun <R> Twig.twig(logMessage: String, block: () -> R): R {
* (otherwise the function and its "block" param would have to suspend)
*/
inline fun <R> Twig.twigTask(logMessage: String, block: () -> R): R {
twig("$logMessage - started | on thread ${Thread.currentThread().name})")
twig("$logMessage - started | on thread ${Thread.currentThread().name}")
val start = System.nanoTime()
val result = block()
val elapsed = ((System.nanoTime() - start)/1e6)
val elapsed = ((System.nanoTime() - start) / 1e5).roundToLong() / 10L
twig("$logMessage - completed | in $elapsed ms" +
" on thread ${Thread.currentThread().name}")
return result

View File

@ -6,22 +6,58 @@ import kotlinx.coroutines.delay
import java.io.File
import kotlin.random.Random
suspend inline fun retryUpTo(retries: Int, initialDelayMillis: Long = 10L, block: (Int) -> Unit) {
/**
* Execute the given block and if it fails, retry up to [retries] more times. If none of the
* retries succeed then throw the final error, which can be wrapped in order to add more context.
*
* @param retries the number of times to retry the block after the first attempt fails.
* @param exceptionWrapper a function that can wrap the final failure to add more useful information
* or context. Default behavior is to just return the final exception.
* @param initialDelayMillis the initial amount of time to wait before the first retry.
* @param block the code to execute, which will be wrapped in a try/catch and retried whenever an
* exception is thrown up to [retries] attempts.
*/
suspend inline fun retryUpTo(retries: Int, exceptionWrapper: (Throwable) -> Throwable = { it }, initialDelayMillis: Long = 500L, block: (Int) -> Unit) {
var failedAttempts = 0
while (failedAttempts < retries) {
while (failedAttempts <= retries) {
try {
block(failedAttempts)
return
} catch (t: Throwable) {
failedAttempts++
if (failedAttempts >= retries) throw t
val duration = Math.pow(initialDelayMillis.toDouble(), failedAttempts.toDouble()).toLong()
twig("failed due to $t retrying (${failedAttempts + 1}/$retries) in ${duration}s...")
if (failedAttempts > retries) throw exceptionWrapper(t)
val duration = (initialDelayMillis.toDouble() * Math.pow(2.0, failedAttempts.toDouble() - 1)).toLong()
twig("failed due to $t retrying (${failedAttempts}/$retries) in ${duration}s...")
delay(duration)
}
}
}
/**
* Execute the given block and if it fails, retry up to [retries] more times, using thread sleep
* instead of suspending. If none of the retries succeed then throw the final error. This function
* is intended to be called with no parameters, i.e., it is designed to use its defaults.
*
* @param retries the number of times to retry. Typically, this should be low.
* @param sleepTime the amount of time to sleep in between retries. Typically, this should be an
* amount of time that is hard to perceive.
* @param block the block of logic to try.
*/
inline fun retrySimple(retries: Int = 2, sleepTime: Long = 20L, block: (Int) -> Unit) {
var failedAttempts = 0
while (failedAttempts <= retries) {
try {
block(failedAttempts)
return
} catch (t: Throwable) {
failedAttempts++
if (failedAttempts > retries) throw t
twig("failed due to $t simply retrying (${failedAttempts}/$retries) in ${sleepTime}ms...")
Thread.sleep(sleepTime)
}
}
}
suspend inline fun retryWithBackoff(noinline onErrorListener: ((Throwable) -> Boolean)? = null, initialDelayMillis: Long = 1000L, maxDelayMillis: Long = MAX_BACKOFF_INTERVAL, block: () -> Unit) {
var sequence = 0 // count up to the max and then reset to half. So that we don't repeat the max but we also don't repeat too much.
while (true) {
@ -35,13 +71,13 @@ suspend inline fun retryWithBackoff(noinline onErrorListener: ((Throwable) -> Bo
}
sequence++
// I^(1/4)n + jitter
// initialDelay^(sequence/4) + jitter
var duration = Math.pow(initialDelayMillis.toDouble(), (sequence.toDouble()/4.0)).toLong() + Random.nextLong(1000L)
if (duration > maxDelayMillis) {
duration = maxDelayMillis - Random.nextLong(1000L) // include jitter but don't exceed max delay
sequence /= 2
}
twig("Failed due to $t retrying in ${duration}ms...")
twig("Failed due to $t backing off and retrying in ${duration}ms...")
delay(duration)
}
}

View File

@ -29,6 +29,11 @@ open class ZcashSdkCommon {
*/
val MAX_REORG_SIZE = 100
/**
* The maximum length of a memo.
*/
val MAX_MEMO_SIZE = 512
/**
* The amount of blocks ahead of the current height where new transactions are set to expire. This value is controlled
* by the rust backend but it is helpful to know what it is set to and should be kept in sync.
@ -81,7 +86,7 @@ open class ZcashSdkCommon {
val DB_DATA_NAME = "Data.db"
val DB_CACHE_NAME = "Cache.db"
open val DEFAULT_DB_NAME_PREFIX = "ZcashSdk_"
open val DEFAULT_DB_NAME_PREFIX = "ZcashSdk"
/**
* File name for the sappling spend params

View File

@ -60,7 +60,6 @@ class FlowPagedListBuilder<Key, Value>(
}
do {
twig("zzzzz do this while...")
if (::dataSource.isInitialized) {
dataSource.removeInvalidatedCallback(callback)
}

View File

@ -1,8 +1,6 @@
package cash.z.wallet.sdk.jni
import android.content.Context
import cash.z.wallet.sdk.exception.BirthdayException
import cash.z.wallet.sdk.ext.ZcashSdk
import cash.z.wallet.sdk.ext.ZcashSdk.OUTPUT_PARAM_FILE_NAME
import cash.z.wallet.sdk.ext.ZcashSdk.SPEND_PARAM_FILE_NAME
import cash.z.wallet.sdk.ext.twig
@ -19,42 +17,40 @@ class RustBackend : RustBackendWelding {
load()
}
internal lateinit var dbDataPath: String
internal lateinit var dbCachePath: String
internal lateinit var dbNamePrefix: String
internal lateinit var paramDestinationDir: String
// Paths
internal lateinit var pathDataDb: String
internal lateinit var pathCacheDb: String
internal lateinit var pathParamsDir: String
internal var birthdayHeight: Int = -1
get() = if (field != -1) field else throw BirthdayException.UninitializedBirthdayException
fun init(appContext: Context, dbNamePrefix: String = ZcashSdk.DEFAULT_DB_NAME_PREFIX) =
init(
appContext.getDatabasePath("unused.db").parentFile.absolutePath,
"${appContext.cacheDir.absolutePath}/params",
dbNamePrefix
)
/**
* Loads the library and initializes path variables. Although it is best to only call this
* function once, it is idempotent.
*/
fun init(
dbPath: String,
paramsPath: String,
dbNamePrefix: String = ZcashSdk.DEFAULT_DB_NAME_PREFIX
cacheDbPath: String,
dataDbPath: String,
paramsPath: String
): RustBackend {
this.dbNamePrefix = dbNamePrefix
twig("Creating RustBackend") {
dbCachePath = File(dbPath, "${dbNamePrefix}${ZcashSdk.DB_CACHE_NAME}").absolutePath
dbDataPath = File(dbPath, "${dbNamePrefix}${ZcashSdk.DB_DATA_NAME}").absolutePath
paramDestinationDir = paramsPath
pathCacheDb = cacheDbPath
pathDataDb = dataDbPath
pathParamsDir = paramsPath
}
return this
}
fun clear() {
twig("Deleting databases")
File(dbCachePath).delete()
File(dbDataPath).delete()
fun clear(clearCacheDb: Boolean = true, clearDataDb: Boolean = true) {
if (clearCacheDb) {
twig("Deleting cache database!")
File(pathCacheDb).delete()
}
if (clearDataDb) {
twig("Deleting data database!")
File(pathDataDb).delete()
}
}
@ -62,7 +58,7 @@ class RustBackend : RustBackendWelding {
// Wrapper Functions
//
override fun initDataDb() = initDataDb(dbDataPath)
override fun initDataDb() = initDataDb(pathDataDb)
// override fun initAccountsTable(extfvks: Array<String>) =
// initAccountsTableWithKeys(dbDataPath, extfvks)
@ -70,7 +66,7 @@ class RustBackend : RustBackendWelding {
override fun initAccountsTable(
seed: ByteArray,
numberOfAccounts: Int
) = initAccountsTable(dbDataPath, seed, numberOfAccounts)
) = initAccountsTable(pathDataDb, seed, numberOfAccounts)
override fun initBlocksTable(
height: Int,
@ -79,29 +75,29 @@ class RustBackend : RustBackendWelding {
saplingTree: String
): Boolean {
birthdayHeight = height
return initBlocksTable(dbDataPath, height, hash, time, saplingTree)
return initBlocksTable(pathDataDb, height, hash, time, saplingTree)
}
override fun getAddress(account: Int) = getAddress(dbDataPath, account)
override fun getAddress(account: Int) = getAddress(pathDataDb, account)
override fun getBalance(account: Int) = getBalance(dbDataPath, account)
override fun getBalance(account: Int) = getBalance(pathDataDb, account)
override fun getVerifiedBalance(account: Int) = getVerifiedBalance(dbDataPath, account)
override fun getVerifiedBalance(account: Int) = getVerifiedBalance(pathDataDb, account)
override fun getReceivedMemoAsUtf8(idNote: Long) =
getReceivedMemoAsUtf8(dbDataPath, idNote)
getReceivedMemoAsUtf8(pathDataDb, idNote)
override fun getSentMemoAsUtf8(idNote: Long) = getSentMemoAsUtf8(dbDataPath, idNote)
override fun getSentMemoAsUtf8(idNote: Long) = getSentMemoAsUtf8(pathDataDb, idNote)
override fun validateCombinedChain() = validateCombinedChain(dbCachePath, dbDataPath)
override fun validateCombinedChain() = validateCombinedChain(pathCacheDb, pathDataDb)
override fun rewindToHeight(height: Int) = rewindToHeight(dbDataPath, height)
override fun rewindToHeight(height: Int) = rewindToHeight(pathDataDb, height)
override fun scanBlocks(limit: Int): Boolean {
return if (limit > 0) {
scanBlockBatch(dbCachePath, dbDataPath, limit)
scanBlockBatch(pathCacheDb, pathDataDb, limit)
} else {
scanBlocks(dbCachePath, dbDataPath)
scanBlocks(pathCacheDb, pathDataDb)
}
}
@ -112,14 +108,14 @@ class RustBackend : RustBackendWelding {
value: Long,
memo: ByteArray?
): Long = createToAddress(
dbDataPath,
pathDataDb,
account,
extsk,
to,
value,
memo ?: ByteArray(0),
"${paramDestinationDir}/$SPEND_PARAM_FILE_NAME",
"${paramDestinationDir}/$OUTPUT_PARAM_FILE_NAME"
"${pathParamsDir}/$SPEND_PARAM_FILE_NAME",
"${pathParamsDir}/$OUTPUT_PARAM_FILE_NAME"
)
override fun deriveSpendingKeys(seed: ByteArray, numberOfAccounts: Int) =
@ -130,6 +126,11 @@ class RustBackend : RustBackendWelding {
override fun deriveViewingKey(spendingKey: String) = deriveExtendedFullViewingKey(spendingKey)
override fun deriveAddress(seed: ByteArray, accountIndex: Int) =
deriveAddressFromSeed(seed, accountIndex)
override fun deriveAddress(viewingKey: String) = deriveAddressFromViewingKey(viewingKey)
override fun isValidShieldedAddr(addr: String) =
isValidShieldedAddress(addr)
@ -232,5 +233,9 @@ class RustBackend : RustBackendWelding {
@JvmStatic private external fun deriveExtendedFullViewingKeys(seed: ByteArray, numberOfAccounts: Int): Array<String>
@JvmStatic private external fun deriveExtendedFullViewingKey(spendingKey: String): String
@JvmStatic private external fun deriveAddressFromSeed(seed: ByteArray, accountIndex: Int): String
@JvmStatic private external fun deriveAddressFromViewingKey(key: String): String
}
}

View File

@ -47,4 +47,8 @@ interface RustBackendWelding {
fun deriveViewingKeys(seed: ByteArray, numberOfAccounts: Int = 1): Array<String>
fun deriveViewingKey(spendingKey: String): String
fun deriveAddress(seed: ByteArray, accountIndex: Int = 0): String
fun deriveAddress(viewingKey: String): String
}

View File

@ -4,16 +4,14 @@ import android.content.Context
import androidx.paging.PagedList
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import cash.z.wallet.sdk.db.BlockDao
import cash.z.wallet.sdk.db.DerivedDataDb
import cash.z.wallet.sdk.db.TransactionDao
import cash.z.wallet.sdk.entity.ConfirmedTransaction
import cash.z.wallet.sdk.entity.EncodedTransaction
import cash.z.wallet.sdk.entity.TransactionEntity
import cash.z.wallet.sdk.ext.ZcashSdk
import cash.z.wallet.sdk.ext.android.toFlowPagedList
import cash.z.wallet.sdk.ext.android.toRefreshable
import cash.z.wallet.sdk.ext.twig
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.withContext
@ -39,9 +37,14 @@ open class PagedTransactionRepository(
) : this(
Room.databaseBuilder(context, DerivedDataDb::class.java, dataDbName)
.setJournalMode(RoomDatabase.JournalMode.TRUNCATE)
.addMigrations(MIGRATION_4_3)
.build(),
pageSize
)
init {
derivedDataDb.openHelper.writableDatabase.beginTransaction()
derivedDataDb.openHelper.writableDatabase.endTransaction()
}
private val blocks: BlockDao = derivedDataDb.blockDao()
private val transactions: TransactionDao = derivedDataDb.transactionDao()
@ -80,4 +83,61 @@ open class PagedTransactionRepository(
derivedDataDb.close()
}
}
//
// Migrations
//
companion object {
// val MIGRATION_3_4 = object : Migration(3, 4) {
// override fun migrate(database: SupportSQLiteDatabase) {
// database.execSQL("PRAGMA foreign_keys = OFF;")
// database.execSQL("""
// CREATE TABLE IF NOT EXISTS received_notes_new (
// 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,
// is_change INTEGER NOT NULL, memo BLOB,
// spent INTEGER
// ); """.trimIndent()
// )
// database.execSQL("INSERT INTO received_notes_new SELECT * FROM received_notes;")
// database.execSQL("DROP TABLE received_notes;")
// database.execSQL("ALTER TABLE received_notes_new RENAME TO received_notes;")
// database.execSQL("PRAGMA foreign_keys = ON;")
// }
// }
val MIGRATION_4_3 = object : Migration(4, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("PRAGMA foreign_keys = OFF;")
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS received_notes_new (
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,
is_change INTEGER NOT NULL,
memo BLOB,
spent INTEGER,
FOREIGN KEY (tx) REFERENCES transactions(id_tx),
FOREIGN KEY (account) REFERENCES accounts(account),
FOREIGN KEY (spent) REFERENCES transactions(id_tx),
CONSTRAINT tx_output UNIQUE (tx, output_index)
); """.trimIndent()
)
database.execSQL("INSERT INTO received_notes_new SELECT * FROM received_notes;")
database.execSQL("DROP TABLE received_notes;")
database.execSQL("ALTER TABLE received_notes_new RENAME TO received_notes;")
database.execSQL("PRAGMA foreign_keys = ON;")
}
}
}
}

View File

@ -71,7 +71,7 @@ class WalletTransactionEncoder(
twigTask("creating transaction to spend $value zatoshi to" +
" ${toAddress.masked()} with memo $memo") {
try {
ensureParams((rustBackend as RustBackend).paramDestinationDir)
ensureParams((rustBackend as RustBackend).pathParamsDir)
twig("params exist! attempting to send...")
rustBackend.createToAddress(
fromAccountIndex,

View File

@ -22,22 +22,25 @@ use service_grpc::CompactTxStreamer;
const LIGHTWALLETD_HOST: &str = "lightwalletd.z.cash";
const LIGHTWALLETD_PORT: u16 = 9067;
const BATCH_SIZE: u64 = 10_000;
const TARGET_HEIGHT: u64 = 735000;
const NETWORK: &str = "mainnet";
fn print_sapling_tree(height: u64, mut hash: Vec<u8>, time: u32, tree: CommitmentTree<Node>) {
hash.reverse();
let mut tree_bytes = vec![];
tree.write(&mut tree_bytes).unwrap();
println!("{{");
println!(" \"network\": \"{}\",", NETWORK);
println!(" \"height\": {},", height);
println!(" \"hash\": {},", hex::encode(hash));
println!(" \"hash\": \"{}\",", hex::encode(hash));
println!(" \"time\": {},", time);
println!(" \"tree\": \"{}\",", hex::encode(tree_bytes));
println!(" \"tree\": \"{}\"", hex::encode(tree_bytes));
println!("}}");
}
fn main() {
// For now, start from Sapling activation height
let mut start_height = 280000;
let mut start_height = 419200;
let mut tree = CommitmentTree::new();
let client_conf = Default::default();
@ -50,11 +53,7 @@ fn main() {
loop {
// Get the latest height
let latest_height = client
.get_latest_block(grpc::RequestOptions::new(), service::ChainSpec::new())
.wait_drop_metadata()
.unwrap()
.height;
let latest_height = TARGET_HEIGHT;
let end_height = if latest_height - start_height < BATCH_SIZE {
latest_height
} else {

View File

@ -17,7 +17,7 @@ use std::ptr;
use zcash_client_backend::{
encoding::{
decode_extended_spending_key, encode_extended_full_viewing_key,
encode_extended_spending_key,
encode_extended_spending_key, encode_payment_address,
},
keys::spending_key,
};
@ -30,11 +30,10 @@ use zcash_client_sqlite::{
get_address, get_balance, get_received_memo_as_utf8, get_sent_memo_as_utf8,
get_verified_balance,
},
scan::{
scan_cached_blocks
},
scan::scan_cached_blocks,
transact::create_to_address,
};
use zcash_primitives::{
block::BlockHash,
consensus::BranchId,
@ -48,12 +47,15 @@ use crate::utils::exception::unwrap_exc_or;
#[cfg(feature = "mainnet")]
use zcash_client_backend::constants::mainnet::{
COIN_TYPE, HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, HRP_SAPLING_EXTENDED_SPENDING_KEY
COIN_TYPE, HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, HRP_SAPLING_EXTENDED_SPENDING_KEY,
HRP_SAPLING_PAYMENT_ADDRESS,
};
#[cfg(not(feature = "mainnet"))]
use zcash_client_backend::constants::testnet::{
COIN_TYPE, HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, HRP_SAPLING_EXTENDED_SPENDING_KEY
COIN_TYPE, HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, HRP_SAPLING_EXTENDED_SPENDING_KEY,
HRP_SAPLING_PAYMENT_ADDRESS,
};
use zcash_client_backend::encoding::decode_extended_full_viewing_key;
#[no_mangle]
pub unsafe extern "C" fn Java_cash_z_wallet_sdk_jni_RustBackend_initLogs(
@ -200,6 +202,68 @@ pub unsafe extern "C" fn Java_cash_z_wallet_sdk_jni_RustBackend_deriveExtendedFu
unwrap_exc_or(&env, res, ptr::null_mut())
}
#[no_mangle]
pub unsafe extern "C" fn Java_cash_z_wallet_sdk_jni_RustBackend_deriveAddressFromSeed(
env: JNIEnv<'_>,
_: JClass<'_>,
seed: jbyteArray,
account_index: jint,
) -> jstring {
let res = panic::catch_unwind(|| {
let seed = env.convert_byte_array(seed).unwrap();
let account_index = if account_index >= 0 {
account_index as u32
} else {
return Err(format_err!("accountIndex argument must be positive"));
};
let address = spending_key(&seed, COIN_TYPE, account_index)
.default_address()
.unwrap()
.1;
let address_str = encode_payment_address(HRP_SAPLING_PAYMENT_ADDRESS, &address);
let output = env
.new_string(address_str)
.expect("Couldn't create Java string!");
Ok(output.into_inner())
});
unwrap_exc_or(&env, res, ptr::null_mut())
}
#[no_mangle]
pub unsafe extern "C" fn Java_cash_z_wallet_sdk_jni_RustBackend_deriveAddressFromViewingKey(
env: JNIEnv<'_>,
_: JClass<'_>,
extfvk_string: JString<'_>,
) -> jstring {
let res = panic::catch_unwind(|| {
let extfvk_string = utils::java_string_to_rust(&env, extfvk_string);
let extfvk = match decode_extended_full_viewing_key(
HRP_SAPLING_EXTENDED_SPENDING_KEY,
&extfvk_string,
) {
Ok(Some(extfvk)) => extfvk,
Ok(None) => {
return Err(format_err!("Deriving viewing key from string returned no results. Encoding was valid but type was incorrect."));
}
Err(e) => {
return Err(format_err!(
"Error while deriving viewing key from string input: {}",
e
));
}
};
let address = extfvk.default_address().unwrap().1;
let address_str = encode_payment_address(HRP_SAPLING_PAYMENT_ADDRESS, &address);
let output = env
.new_string(address_str)
.expect("Couldn't create Java string!");
Ok(output.into_inner())
});
unwrap_exc_or(&env, res, ptr::null_mut())
}
#[no_mangle]
pub unsafe extern "C" fn Java_cash_z_wallet_sdk_jni_RustBackend_deriveExtendedFullViewingKey(
env: JNIEnv<'_>,
@ -481,7 +545,7 @@ pub unsafe extern "C" fn Java_cash_z_wallet_sdk_jni_RustBackend_scanBlocks(
let db_cache = utils::java_string_to_rust(&env, db_cache);
let db_data = utils::java_string_to_rust(&env, db_data);
match scan_cached_blocks(&db_cache, &db_data) {
match scan_cached_blocks(&db_cache, &db_data, None) {
Ok(()) => Ok(JNI_TRUE),
Err(e) => Err(format_err!("Error while scanning blocks: {}", e)),
}
@ -496,14 +560,13 @@ pub unsafe extern "C" fn Java_cash_z_wallet_sdk_jni_RustBackend_scanBlockBatch(
_: JClass<'_>,
db_cache: JString<'_>,
db_data: JString<'_>,
limit: jint
limit: jint,
) -> jboolean {
let res = panic::catch_unwind(|| {
let db_cache = utils::java_string_to_rust(&env, db_cache);
let db_data = utils::java_string_to_rust(&env, db_data);
// match scan_cached_block_batch(&db_cache, &db_data, limit) {
match scan_cached_blocks(&db_cache, &db_data) {
match scan_cached_blocks(&db_cache, &db_data, Some(limit)) {
Ok(()) => Ok(JNI_TRUE),
Err(e) => Err(format_err!("Error while scanning blocks: {}", e)),
}

View File

@ -1,4 +1,5 @@
{
"network": "mainnet",
"height": 419200,
"hash": "00000000025a57200d898ac7f21e26bf29028bbe96ec46e05b2c17cc9db9e4f3",
"time": 1540779337,

View File

@ -0,0 +1,7 @@
{
"network": "mainnet",
"height": 600000,
"hash": "00000000011502273e3726d1a229b69ae5088eeac650d787dcd5eabe1429ea38",
"time": 1568024995,
"tree": "017d2849ae4eca1bb7a1c78369373c3234b0b2205aeec7186b83da5970fe78100201f9375bb13cb285488c932b2dee1220589f490d4d83239371c260c80d5ffe1624100183daeacfa7985762de7e4442b854a07dab147fc2c8893ee986a2fb3db452c568019238d6a0c7a927deab0faee225cd2199c19a98a0dc29782ba6fd3213fed55031000130794486a8b9d78638a1688c520dbf70da1a912e94417fd8c8dd2d6d8363946b0001b6055deb04e1f5f4b9acc22f5ab2533e44d092f124cad08c7f4200d63dee666401427466a1604032d2080811e6a2a8b509d171fd9108bc24ec14f2b27c6155851c012bab0a6072d49eaa35808b886c0e5a0ab60e4bd554fff56c408dfed91b0d2e1301421e61e5b6edb6680d7868499753dd4b5bc8e6c4f61cb62b868836e8c105b13f00019549565919c2177d57bc5034bc222d75ec3bf56723ea7e1eb7c70dcf662f3d5b000188204c256935d05a22ccf0c273619854917c3af44f78d35c766f44570dfce65b01de9f824df05c82e5eb33ef429b4316605910a8a4aa28750440a379dc1593b2460001754bb593ea42d231a7ddf367640f09bbf59dc00f2c1d2003cc340e0c016b5b13"
}

View File

@ -0,0 +1,7 @@
{
"network": "mainnet",
"height": 610000,
"hash": "000000000218882f481e3b49ca3df819734b8d74aac91f69e848d7499b34b472",
"time": 1569533511,
"tree": "0192943f1eca6525cea7ea8e26b37c792593ed50cfe2be7a1ff551a08dc64b812f001000000001deef7ae5162a9942b4b9aa797137c5bdf60750e9548664127df99d1981dda66901747ad24d5daf294ce2a27aba923e16e52e7348eea3048c5b5654b99ab0a371200149d8aff830305beb3887529f6deb150ab012916c3ce88a6b47b78228f8bfeb3f01ff84a89890cfae65e0852bc44d9aa82be2c5d204f5aebf681c9e966aa46f540e000001d58f1dfaa9db0996996129f8c474acb813bfed452d347fb17ebac2e775e209120000000001319312241b0031e3a255b0d708750b4cb3f3fe79e3503fe488cc8db1dd00753801754bb593ea42d231a7ddf367640f09bbf59dc00f2c1d2003cc340e0c016b5b13"
}

View File

@ -0,0 +1,7 @@
{
"network": "mainnet",
"height": 620000,
"hash": "00000000007b7e2fa16efad760ae16557af158bcdeb417ec125c886e6078c5fe",
"time": 1571037453,
"tree": "016b4b46fc9729d4665a56bba74829ea0a5498eba0d38c11b7fd9404eb8995ae330148f9b161bb8bc18290fe51533fd8be5f48ebd3aca45f037f5fdf04e7ec01ec1e10018fbb58de47d9a32291e56b0a2dc02d6ef845740f57fc7df75586c44bce14652900000181f8e612e83ab54c9eef940320304912ad2166fe45794cb85331ab452964b32f00012ae13cf0549ccce250d6678e5fb73ebdbb830228460cb17379c6cf83da838335000178642a45bbccd4ad77b8bb6e2d51d9ba697950214aa1ce9f00284d2e79c92c020195ed66336cd2a891f7c48080327b8f19d8d1fdbde3882a90695b9336de3613090001c093e2698e2ec1f7ebe6ba9f7ed0db7a2d9d03e0dd8f31fd88f78bad958cfa2c010164ab7b1c3c695a73634e7e90231394ab56d318aefda07c166889ee0d265222000001319312241b0031e3a255b0d708750b4cb3f3fe79e3503fe488cc8db1dd00753801754bb593ea42d231a7ddf367640f09bbf59dc00f2c1d2003cc340e0c016b5b13"
}

View File

@ -0,0 +1,7 @@
{
"network": "mainnet",
"height": 630000,
"hash": "00000000015493abba3e3bb384562f09141548f60581e06d4056993388d2ea2f",
"time": 1572545431,
"tree": "019b01066bae720ce88b4252c3852b0160ec4c4dcd6110df92e76de5cb23ab2f540109c3001b823fc745328a89a47fc5ace701bbd4dc1e9692e918a125ca48960545100001b2ba91c0f96777e735ded1ba9671003a399d435db3a0746bef3b2c83ba4d953f01d4c31130d2013fb57440d21fba0a8af65e61cd1405a8e2d9b987c02df8fc6514011c44ba36710e293ddf95e6715594daa927883d48cda6a3a5ee4aa3ef141ec55b0001cd9540592d39094703664771e61ce69d5b08539812886e0b9df509c80f938f6601178b3d8f9e7f7af7a1f4a049289195001abd96bb41e15b4010cecc1468af4e4b01ffe988e63aba31819640175d3fbb8c91b3c42d2f5074b4c075411d3a5c28e62801cb2e8d7f7387a9d31ba38697a9564808c9aff7d018a4cbdcd1c635edc3ab3014000001060f0c26ee205d7344bda85024a9f9a3c3022d52ea30dfb6770f4acbe168406d0103a7a58b1d7caef1531d521cc85de6fcb18d3590f31ad4486ca1252dac2c96020001319312241b0031e3a255b0d708750b4cb3f3fe79e3503fe488cc8db1dd00753801754bb593ea42d231a7ddf367640f09bbf59dc00f2c1d2003cc340e0c016b5b13"
}

View File

@ -0,0 +1,7 @@
{
"network": "mainnet",
"height": 640000,
"hash": "00000000016cd930734753f5acce6274b391f14330c793e54e7bd9f942d17114",
"time": 1574051743,
"tree": "0165aed8451b6a6c0a66294267976be6d171f2acf83c2b5b94d976cb32062cfa6301cd5b1e5ce12e7d82d07c1b83f7746ef2be8d0c56f90f82b71a1e422a1ffb400710000142f5056f23557ba4cbc562067d43fdc07477fa740c6a13a4ed6d0667b7c1b5510000000001ee9dbe0b8d268efe7e8a88ae7b0ac91923bd71ee81bba0e35e3b9504be59aa250001a2178e94504352c0dd7d6f711b814f8a332239f688568f1719808fd1d385831e0001967ca804f328397d98bd5e1f36786a9d44b06192e70a38026909fb4ce251943e000001fa6980c053d84f809b6abcf35690f03a11f87b28e3240828e32e3f57af41e54e01319312241b0031e3a255b0d708750b4cb3f3fe79e3503fe488cc8db1dd00753801754bb593ea42d231a7ddf367640f09bbf59dc00f2c1d2003cc340e0c016b5b13"
}

View File

@ -1,4 +1,5 @@
{
"network": "mainnet",
"height": 643500,
"hash": "000000000041005fd724ff6e29bd1738bed69a4d9ca028e124029525350bd789",
"time": 1574579149,

View File

@ -0,0 +1,7 @@
{
"network": "mainnet",
"height": 650000,
"hash": "0000000000a0a3fbbd739fb4fcbbfefff44efffc2064ca69a59d5284a2da26e2",
"time": 1575558895,
"tree": "01a6224d30bd854bb14e06b650e887e9ee3a45067dde6af8fdbca004b416accf0b001000018363c4cef8b386c64e759aba8380e950cae17e839da07426966b74ba23b06c350001ba6759797b2db9fbb295a6443f66e85a8f7b2f5895a6b5f5c328858e0af3bd4e00013617c00a1e03fb16a22189949e4888d3f105d10d9a7fcc0542d7ff62d9883e490000000000000163ab01f46a3bb6ea46f5a19d5bdd59eb3f81e19cfa6d10ab0fd5566c7a16992601fa6980c053d84f809b6abcf35690f03a11f87b28e3240828e32e3f57af41e54e01319312241b0031e3a255b0d708750b4cb3f3fe79e3503fe488cc8db1dd00753801754bb593ea42d231a7ddf367640f09bbf59dc00f2c1d2003cc340e0c016b5b13"
}

View File

@ -0,0 +1,7 @@
{
"network": "mainnet",
"height": 660000,
"hash": "00000000022191adab7eeaf3037fef390a2475cd1e2048e93070ad2a0932fe34",
"time": 1576585014,
"tree": "0100847c8f69f5a56bdaf1c4fb22b36ca639d95dfb04f5bd8abf8963675b2e320c0010014bf99a76b0ab66342b62efac6b848777980111ef53121d95ade59846d70dfc0d000000019936a7273937231a02229867cabbf340388809da1d0f2dd10924e1abb788a6500001254c3303021e02ef6f4349326c211f731d5acfc190980e44f332f2a082d6706d000000000163f4eec5a2fe00a5f45e71e1542ff01e937d2210c99f03addcce5314a5278b2d0163ab01f46a3bb6ea46f5a19d5bdd59eb3f81e19cfa6d10ab0fd5566c7a16992601fa6980c053d84f809b6abcf35690f03a11f87b28e3240828e32e3f57af41e54e01319312241b0031e3a255b0d708750b4cb3f3fe79e3503fe488cc8db1dd00753801754bb593ea42d231a7ddf367640f09bbf59dc00f2c1d2003cc340e0c016b5b13"
}

View File

@ -0,0 +1,7 @@
{
"network": "mainnet",
"height": 670000,
"hash": "000000000086313791aca867bf60fc7434fe2b3fc56926ef46223c6b6b05b5fd",
"time": 1577338346,
"tree": "01e38b18dcf2de5e9ca5c12a1329176b37219546cfabd9333a9536d2d3ef6cba3e01c159856a741f9da500a9c83935ee7323d63f589a117e66874612e70cdf7a9f4b1001db5e89b8cbf677a87375395940f4715de1bb951f05efbbc4fb34bb1990dd80600155dbaaf5b93f338d1c629fe2a77328c7609c59c6a767a6ccfcc14d3c8c7d826100000001d9e9451fe610b3374b30c711f62a29700ecd2b02e096f02085b896d3fdc3886401006895fa87a8083ae5d0d38df876e764486c67a684706f7750ee19c872dc5d2e01f5cc54720296c3379ac6fb0aa3ed6824bcc40894b3f40d9d2e2d1ed3e6080c3501e9fc6273cadcc40df45ee63984330cfe702a1e7b4c324516d1a80ebcacc4d4170125719ebec43e9148ecc5cfdb2359074badb6fc7759817f6afab999570a75a2000171b36f07e48c45e39f1cc02a99023236f1df60ae924b5ef14ddacc7885994e2b0163f4eec5a2fe00a5f45e71e1542ff01e937d2210c99f03addcce5314a5278b2d0163ab01f46a3bb6ea46f5a19d5bdd59eb3f81e19cfa6d10ab0fd5566c7a16992601fa6980c053d84f809b6abcf35690f03a11f87b28e3240828e32e3f57af41e54e01319312241b0031e3a255b0d708750b4cb3f3fe79e3503fe488cc8db1dd00753801754bb593ea42d231a7ddf367640f09bbf59dc00f2c1d2003cc340e0c016b5b13"
}

View File

@ -0,0 +1,7 @@
{
"network": "mainnet",
"height": 680000,
"hash": "0000000001f2e08db1ea7ce567a5cd745de87c2eafbd769346b8212cc922d517",
"time": 1578091375,
"tree": "01bc7b45da508ff7e4b3dbc1184a42a646a18ad0c73907d9462199354f3490ec00001101cf1bc2f3ef2e491a4c04cede4efa561dcb4e9c56562adaa79b96ec8e54b43643000001419a6936943299e8d695fb98c78153499682d1c332efa1fbd19ce3c996be713b01e3743cb66129e262add8996fc588df0b1a33366df4e5d618ec14d0bc8129f537000000000141b1ff5b5fdad24aafa550d42cb9f99c85f6175b3d65060079bb9638cacf654e0141754203644e6f3d5faf15f16492efec723da55b2db473b34299c5582e883e46000000000001d2ea556f49fb934dc76f087935a5c07788000b4e3aae24883adfec51b5f4d260"
}

View File

@ -0,0 +1,7 @@
{
"network": "mainnet",
"height": 690000,
"hash": "0000000000b1e6422ecd9292951b36ebb94e8926bbd33df8445b574b4be14f79",
"time": 1578845180,
"tree": "0117ffc074ef0f54651b2bc78d594e5ff786d9828ae78b1db972cd479669e8dd2401cc1b37d13f3b7d1fa2ead08493d275bfca976dd482e8dd879bf62b987652f63811013d84614158c7810753cc663f7a3da757f84f77744a24490eb07ce07af1daa92e0000017472a22c4064648ff260cbec8d85c273c5cd190dab7800f4978d473322dab1200001c7a1fd3786de051015c90f39143f3cfb89f2ea8bb5155520547ecfbefcdc382a0000000001d0c515cd513b49e397bf96d895a941aed4869ff2ff925939a34572c078dc16470121c1efd29f85680334050ee2a7e0d09fde474f90e573d85b7c9d337a5465625a0000000001d2ea556f49fb934dc76f087935a5c07788000b4e3aae24883adfec51b5f4d260"
}

View File

@ -1,4 +1,5 @@
{
"network": "mainnet",
"height": 692345,
"hash": "0000000002584662ea3fb1969a65f05cf1e0c82581b885fbd723eed6ba818e99",
"time": 1579021581,

View File

@ -0,0 +1,7 @@
{
"network": "mainnet",
"height": 693400,
"hash": "0000000001708386101e361d211b2a14f3571d0b81f5962b452d563444c7f06a",
"time": 1579101218,
"tree": "0110939e236e3f13fbf9a044dc4e8d0094b777ee950dca49cb0722556b08fbef2f0197c4a6daa51f5c699ba5f0c3e53b657e54e3d728e60edaf76b4e2215d6aa2d661100000001ef66b21ca159b57a3d54147b0011c096d20cb3aa9590becf8f026c9edadba61a01750454b0edee9cc2f1eaf6d34cb8e495679048008d8cf6c1ab4321bddb828a2901f3dab23e140f2c400b4d4b5e6003ba2c7b316721b0d2858c8e0fcd1f5acbfd4b01eb786638efecd4413cfaadc48a0275035b2d484b92e305cb086c581a07390d21000001a4711f58e3fa6f5d38e2f54ab424c3014c119629fab5ee8a4ed2814d7b17036d01d0c515cd513b49e397bf96d895a941aed4869ff2ff925939a34572c078dc16470121c1efd29f85680334050ee2a7e0d09fde474f90e573d85b7c9d337a5465625a0000000001d2ea556f49fb934dc76f087935a5c07788000b4e3aae24883adfec51b5f4d260"
}

View File

@ -0,0 +1,7 @@
{
"network": "mainnet",
"height": 700000,
"hash": "0000000000c057d167a20ae61b1f77996bc72631dee6ffb11095f0d312230ddc",
"time": 1579598443,
"tree": "01c6b273aee226912526622b91e48a0ff5caf71f1f47569aff8a1c145102b02328012758ab750e1cb4f933ebca089d23ead6032151a38266aa020ae84557bb61844811016443e86acd06140aa932467bcc7235704cf95081e2e5faaf031112a9abd5f930016d1847eb52f8218773e3d2dd8eb19950dbe693484098d763010d7c338337cf68018117bb5e4ad68438572aaa55cb7d66b4b86b9d8310fbb4e36db7982dcc28591400012c4e84168b1c9a322f6035ddb5989fea843045d22182ee9ce45a6a8f6831954301abf6a411ff1708af6252bf921625f28931c567d92833d7ed2b2b14efd6b06e5001d1f934bce5476ef5d21b384c7dddfcbd8c1f630435acbf26a094bc46757f5d3501e6a69ddf114c92d39370a24e840c46ed42fc54a63986d3aa916a08c2a922c73b0001a626bb2ed07614f7228f79d5fbccf541699895842341602c639ab7516b1c9a1a0000019be74b905f0e99399af0fda6832324ceeeaf57551b11b42c73bcb7cd215ab91400000001d2ea556f49fb934dc76f087935a5c07788000b4e3aae24883adfec51b5f4d260"
}

View File

@ -0,0 +1,7 @@
{
"network": "mainnet",
"height": 710000,
"hash": "00000000003adcad055353d33a0962103e284bc47577c62580535a1dd6be7cf6",
"time": 1580351806,
"tree": "01bf61bd9326bba72206cc0ac82791fe316277907ba76773b5ae01ea7df948ae04001101200905d2485346e39f07fd989ba05211195251968d6bcb41b8280bc94733bc5d01e44932d89c5309ac906072235443f573a92dcda2acf608c1851af01dbeab19350000000001a73367559bf511fc5212ab3f0f6754a9b7ca59a3da68588e3763c801c031bd1501eb94b48e208bdbd42bb4815940b2f9f5187cc5e42c196f461f7bc6d020ed670f0001f00ddf03aca4e8c2620ff274939a1f1cd6a4eceb147e8aa6a8ba83717d60182700010576ef08575c3dd49296ba7c2ddd914715c4f9a7316da4ae8f5600dafa1b1c39019be74b905f0e99399af0fda6832324ceeeaf57551b11b42c73bcb7cd215ab91400000001d2ea556f49fb934dc76f087935a5c07788000b4e3aae24883adfec51b5f4d260"
}

View File

@ -0,0 +1,7 @@
{
"network": "mainnet",
"height": 720000,
"hash": "000000000225bab0e1491d6abfa4a41c174bc7d0167411e2cb2ebd960dae5158",
"time": 1581104786,
"tree": "0147c6af2c835328a4c17eff07f76102dc57716a13ce3d3a4f48367c3f2384fe2901e01e9b45be2ad8bedab63db1963c2b8d85e1ed20b6327cf2c55c211234e8a3351101134f80e61b548e384e87f823187d2734b07c516d48eea33a533c6cf7aa47052200000000000000017ce48111238d9e81b7e4147286578f2d686d71b1ec0cae668f567f3fef65bc0b017895f4c380f5169dcb84c7154fe6fcf72d694d30f0ba2535437b443a2cb5ae18000000013a1f7fb005388ac6f04099b647ed85d8b025d8ae4b178c2376b473b121b8c052000001d2ea556f49fb934dc76f087935a5c07788000b4e3aae24883adfec51b5f4d260"
}

View File

@ -0,0 +1,7 @@
{
"network": "mainnet",
"height": 730000,
"hash": "0000000002293b9e058e17fbc357c9b676d276eba338e033e357034c775ab320",
"time": 1581858634,
"tree": "019a59eba6efd060a61cde70daa7b34202e5fd55fcbf809eaf0aa3252e45810e48001101e106c6f8a17723af8793c1bd0f0e95dbcf5ef0bd80e20195422d8388db48cf24000001907e2c08367bfb45d196771ad267ec773c80ff4306aa7c4d2415ee22d211e90a00000001d72bda7061e4086bb885d6f26e39aa603a1f6db2e4fc71ae65a571c7a31ade27014df2a298ce5d7f8e88617b66ef7dc1fddd854e8dc623c3dc0faaa0eb93137d45017a48dab02dd9a014df0bd310657c3b8e854e24e1137f2ffcb1db693e38a4416d00011a5c078f7dd38704665b7270ebd90366fdd0edccdf284ca1f03c6d7e0536182800013a1f7fb005388ac6f04099b647ed85d8b025d8ae4b178c2376b473b121b8c052000001d2ea556f49fb934dc76f087935a5c07788000b4e3aae24883adfec51b5f4d260"
}

View File

@ -0,0 +1,7 @@
{
"network": "mainnet",
"height": 735000,
"hash": "00000000015c597fab53f58b9e1ededbe8bd83ca0203788e2039eceeb0d65ca6",
"time": 1582235356,
"tree": "0161f2ff97ff6ac6a90f9bce76c11710460f4944d8695aecc7dc99e34cad0131040011015325b185e23e82562db27817be996ffade9597181244f67efc40561aeb9dde1101daeffadc9e38f755bcb55a847a1278518a0ba4a2ef33b2fe01bbb3eb242ab0070000000000011c51f9077e3f7e28e8e337eaf4bb99b41acbc853a37dcc1e172467a1c919fe4100010bb1f55481b2268ef31997dc0fb6b48a530bc17870220f156d832326c433eb0a010b3768d3bf7868a67823e022f49be67982d0588e7041c498a756024750065a4a0001a9e1bf4bccb48b14b544e770f21d48f2d3ad8d6ca54eccc92f60634e3078eb48013a1f7fb005388ac6f04099b647ed85d8b025d8ae4b178c2376b473b121b8c052000001d2ea556f49fb934dc76f087935a5c07788000b4e3aae24883adfec51b5f4d260"
}

View File

@ -24,6 +24,6 @@ object ZcashSdk : ZcashSdkCommon() {
*/
override val DEFAULT_LIGHTWALLETD_HOST = "lightd-main.zecwallet.co"
override val DEFAULT_DB_NAME_PREFIX = "ZcashSdk_mainnet_"
override val DEFAULT_DB_NAME_PREFIX = "ZcashSdk_mainnet"
}

View File

@ -24,6 +24,6 @@ object ZcashSdk : ZcashSdkCommon() {
*/
override val DEFAULT_LIGHTWALLETD_HOST = "lightd-test.zecwallet.co"
override val DEFAULT_DB_NAME_PREFIX = "ZcashSdk_testnet_"
override val DEFAULT_DB_NAME_PREFIX = "ZcashSdk_testnet"
}