Merge pull request #105 from zcash/release/sprint-20-05
Release/sprint 20 05
This commit is contained in:
commit
109673fe97
51
build.gradle
51
build.gradle
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
// }
|
||||
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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")
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -45,4 +45,8 @@ class CompactBlockDbStore(
|
|||
override suspend fun rewindTo(height: Int) = withContext(IO) {
|
||||
cacheDao.rewindTo(height)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
cacheDb.close()
|
||||
}
|
||||
}
|
|
@ -37,5 +37,10 @@ open class CompactBlockDownloader(
|
|||
compactBlockStore.getLatestHeight()
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
lightwalletService.shutdown()
|
||||
compactBlockStore.close()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -60,7 +60,6 @@ class FlowPagedListBuilder<Key, Value>(
|
|||
}
|
||||
|
||||
do {
|
||||
twig("zzzzz do this while...")
|
||||
if (::dataSource.isInitialized) {
|
||||
dataSource.removeInvalidatedCallback(callback)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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;")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)),
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"network": "mainnet",
|
||||
"height": 419200,
|
||||
"hash": "00000000025a57200d898ac7f21e26bf29028bbe96ec46e05b2c17cc9db9e4f3",
|
||||
"time": 1540779337,
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"network": "mainnet",
|
||||
"height": 600000,
|
||||
"hash": "00000000011502273e3726d1a229b69ae5088eeac650d787dcd5eabe1429ea38",
|
||||
"time": 1568024995,
|
||||
"tree": "017d2849ae4eca1bb7a1c78369373c3234b0b2205aeec7186b83da5970fe78100201f9375bb13cb285488c932b2dee1220589f490d4d83239371c260c80d5ffe1624100183daeacfa7985762de7e4442b854a07dab147fc2c8893ee986a2fb3db452c568019238d6a0c7a927deab0faee225cd2199c19a98a0dc29782ba6fd3213fed55031000130794486a8b9d78638a1688c520dbf70da1a912e94417fd8c8dd2d6d8363946b0001b6055deb04e1f5f4b9acc22f5ab2533e44d092f124cad08c7f4200d63dee666401427466a1604032d2080811e6a2a8b509d171fd9108bc24ec14f2b27c6155851c012bab0a6072d49eaa35808b886c0e5a0ab60e4bd554fff56c408dfed91b0d2e1301421e61e5b6edb6680d7868499753dd4b5bc8e6c4f61cb62b868836e8c105b13f00019549565919c2177d57bc5034bc222d75ec3bf56723ea7e1eb7c70dcf662f3d5b000188204c256935d05a22ccf0c273619854917c3af44f78d35c766f44570dfce65b01de9f824df05c82e5eb33ef429b4316605910a8a4aa28750440a379dc1593b2460001754bb593ea42d231a7ddf367640f09bbf59dc00f2c1d2003cc340e0c016b5b13"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"network": "mainnet",
|
||||
"height": 610000,
|
||||
"hash": "000000000218882f481e3b49ca3df819734b8d74aac91f69e848d7499b34b472",
|
||||
"time": 1569533511,
|
||||
"tree": "0192943f1eca6525cea7ea8e26b37c792593ed50cfe2be7a1ff551a08dc64b812f001000000001deef7ae5162a9942b4b9aa797137c5bdf60750e9548664127df99d1981dda66901747ad24d5daf294ce2a27aba923e16e52e7348eea3048c5b5654b99ab0a371200149d8aff830305beb3887529f6deb150ab012916c3ce88a6b47b78228f8bfeb3f01ff84a89890cfae65e0852bc44d9aa82be2c5d204f5aebf681c9e966aa46f540e000001d58f1dfaa9db0996996129f8c474acb813bfed452d347fb17ebac2e775e209120000000001319312241b0031e3a255b0d708750b4cb3f3fe79e3503fe488cc8db1dd00753801754bb593ea42d231a7ddf367640f09bbf59dc00f2c1d2003cc340e0c016b5b13"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"network": "mainnet",
|
||||
"height": 620000,
|
||||
"hash": "00000000007b7e2fa16efad760ae16557af158bcdeb417ec125c886e6078c5fe",
|
||||
"time": 1571037453,
|
||||
"tree": "016b4b46fc9729d4665a56bba74829ea0a5498eba0d38c11b7fd9404eb8995ae330148f9b161bb8bc18290fe51533fd8be5f48ebd3aca45f037f5fdf04e7ec01ec1e10018fbb58de47d9a32291e56b0a2dc02d6ef845740f57fc7df75586c44bce14652900000181f8e612e83ab54c9eef940320304912ad2166fe45794cb85331ab452964b32f00012ae13cf0549ccce250d6678e5fb73ebdbb830228460cb17379c6cf83da838335000178642a45bbccd4ad77b8bb6e2d51d9ba697950214aa1ce9f00284d2e79c92c020195ed66336cd2a891f7c48080327b8f19d8d1fdbde3882a90695b9336de3613090001c093e2698e2ec1f7ebe6ba9f7ed0db7a2d9d03e0dd8f31fd88f78bad958cfa2c010164ab7b1c3c695a73634e7e90231394ab56d318aefda07c166889ee0d265222000001319312241b0031e3a255b0d708750b4cb3f3fe79e3503fe488cc8db1dd00753801754bb593ea42d231a7ddf367640f09bbf59dc00f2c1d2003cc340e0c016b5b13"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"network": "mainnet",
|
||||
"height": 630000,
|
||||
"hash": "00000000015493abba3e3bb384562f09141548f60581e06d4056993388d2ea2f",
|
||||
"time": 1572545431,
|
||||
"tree": "019b01066bae720ce88b4252c3852b0160ec4c4dcd6110df92e76de5cb23ab2f540109c3001b823fc745328a89a47fc5ace701bbd4dc1e9692e918a125ca48960545100001b2ba91c0f96777e735ded1ba9671003a399d435db3a0746bef3b2c83ba4d953f01d4c31130d2013fb57440d21fba0a8af65e61cd1405a8e2d9b987c02df8fc6514011c44ba36710e293ddf95e6715594daa927883d48cda6a3a5ee4aa3ef141ec55b0001cd9540592d39094703664771e61ce69d5b08539812886e0b9df509c80f938f6601178b3d8f9e7f7af7a1f4a049289195001abd96bb41e15b4010cecc1468af4e4b01ffe988e63aba31819640175d3fbb8c91b3c42d2f5074b4c075411d3a5c28e62801cb2e8d7f7387a9d31ba38697a9564808c9aff7d018a4cbdcd1c635edc3ab3014000001060f0c26ee205d7344bda85024a9f9a3c3022d52ea30dfb6770f4acbe168406d0103a7a58b1d7caef1531d521cc85de6fcb18d3590f31ad4486ca1252dac2c96020001319312241b0031e3a255b0d708750b4cb3f3fe79e3503fe488cc8db1dd00753801754bb593ea42d231a7ddf367640f09bbf59dc00f2c1d2003cc340e0c016b5b13"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"network": "mainnet",
|
||||
"height": 640000,
|
||||
"hash": "00000000016cd930734753f5acce6274b391f14330c793e54e7bd9f942d17114",
|
||||
"time": 1574051743,
|
||||
"tree": "0165aed8451b6a6c0a66294267976be6d171f2acf83c2b5b94d976cb32062cfa6301cd5b1e5ce12e7d82d07c1b83f7746ef2be8d0c56f90f82b71a1e422a1ffb400710000142f5056f23557ba4cbc562067d43fdc07477fa740c6a13a4ed6d0667b7c1b5510000000001ee9dbe0b8d268efe7e8a88ae7b0ac91923bd71ee81bba0e35e3b9504be59aa250001a2178e94504352c0dd7d6f711b814f8a332239f688568f1719808fd1d385831e0001967ca804f328397d98bd5e1f36786a9d44b06192e70a38026909fb4ce251943e000001fa6980c053d84f809b6abcf35690f03a11f87b28e3240828e32e3f57af41e54e01319312241b0031e3a255b0d708750b4cb3f3fe79e3503fe488cc8db1dd00753801754bb593ea42d231a7ddf367640f09bbf59dc00f2c1d2003cc340e0c016b5b13"
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"network": "mainnet",
|
||||
"height": 643500,
|
||||
"hash": "000000000041005fd724ff6e29bd1738bed69a4d9ca028e124029525350bd789",
|
||||
"time": 1574579149,
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"network": "mainnet",
|
||||
"height": 650000,
|
||||
"hash": "0000000000a0a3fbbd739fb4fcbbfefff44efffc2064ca69a59d5284a2da26e2",
|
||||
"time": 1575558895,
|
||||
"tree": "01a6224d30bd854bb14e06b650e887e9ee3a45067dde6af8fdbca004b416accf0b001000018363c4cef8b386c64e759aba8380e950cae17e839da07426966b74ba23b06c350001ba6759797b2db9fbb295a6443f66e85a8f7b2f5895a6b5f5c328858e0af3bd4e00013617c00a1e03fb16a22189949e4888d3f105d10d9a7fcc0542d7ff62d9883e490000000000000163ab01f46a3bb6ea46f5a19d5bdd59eb3f81e19cfa6d10ab0fd5566c7a16992601fa6980c053d84f809b6abcf35690f03a11f87b28e3240828e32e3f57af41e54e01319312241b0031e3a255b0d708750b4cb3f3fe79e3503fe488cc8db1dd00753801754bb593ea42d231a7ddf367640f09bbf59dc00f2c1d2003cc340e0c016b5b13"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"network": "mainnet",
|
||||
"height": 660000,
|
||||
"hash": "00000000022191adab7eeaf3037fef390a2475cd1e2048e93070ad2a0932fe34",
|
||||
"time": 1576585014,
|
||||
"tree": "0100847c8f69f5a56bdaf1c4fb22b36ca639d95dfb04f5bd8abf8963675b2e320c0010014bf99a76b0ab66342b62efac6b848777980111ef53121d95ade59846d70dfc0d000000019936a7273937231a02229867cabbf340388809da1d0f2dd10924e1abb788a6500001254c3303021e02ef6f4349326c211f731d5acfc190980e44f332f2a082d6706d000000000163f4eec5a2fe00a5f45e71e1542ff01e937d2210c99f03addcce5314a5278b2d0163ab01f46a3bb6ea46f5a19d5bdd59eb3f81e19cfa6d10ab0fd5566c7a16992601fa6980c053d84f809b6abcf35690f03a11f87b28e3240828e32e3f57af41e54e01319312241b0031e3a255b0d708750b4cb3f3fe79e3503fe488cc8db1dd00753801754bb593ea42d231a7ddf367640f09bbf59dc00f2c1d2003cc340e0c016b5b13"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"network": "mainnet",
|
||||
"height": 670000,
|
||||
"hash": "000000000086313791aca867bf60fc7434fe2b3fc56926ef46223c6b6b05b5fd",
|
||||
"time": 1577338346,
|
||||
"tree": "01e38b18dcf2de5e9ca5c12a1329176b37219546cfabd9333a9536d2d3ef6cba3e01c159856a741f9da500a9c83935ee7323d63f589a117e66874612e70cdf7a9f4b1001db5e89b8cbf677a87375395940f4715de1bb951f05efbbc4fb34bb1990dd80600155dbaaf5b93f338d1c629fe2a77328c7609c59c6a767a6ccfcc14d3c8c7d826100000001d9e9451fe610b3374b30c711f62a29700ecd2b02e096f02085b896d3fdc3886401006895fa87a8083ae5d0d38df876e764486c67a684706f7750ee19c872dc5d2e01f5cc54720296c3379ac6fb0aa3ed6824bcc40894b3f40d9d2e2d1ed3e6080c3501e9fc6273cadcc40df45ee63984330cfe702a1e7b4c324516d1a80ebcacc4d4170125719ebec43e9148ecc5cfdb2359074badb6fc7759817f6afab999570a75a2000171b36f07e48c45e39f1cc02a99023236f1df60ae924b5ef14ddacc7885994e2b0163f4eec5a2fe00a5f45e71e1542ff01e937d2210c99f03addcce5314a5278b2d0163ab01f46a3bb6ea46f5a19d5bdd59eb3f81e19cfa6d10ab0fd5566c7a16992601fa6980c053d84f809b6abcf35690f03a11f87b28e3240828e32e3f57af41e54e01319312241b0031e3a255b0d708750b4cb3f3fe79e3503fe488cc8db1dd00753801754bb593ea42d231a7ddf367640f09bbf59dc00f2c1d2003cc340e0c016b5b13"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"network": "mainnet",
|
||||
"height": 680000,
|
||||
"hash": "0000000001f2e08db1ea7ce567a5cd745de87c2eafbd769346b8212cc922d517",
|
||||
"time": 1578091375,
|
||||
"tree": "01bc7b45da508ff7e4b3dbc1184a42a646a18ad0c73907d9462199354f3490ec00001101cf1bc2f3ef2e491a4c04cede4efa561dcb4e9c56562adaa79b96ec8e54b43643000001419a6936943299e8d695fb98c78153499682d1c332efa1fbd19ce3c996be713b01e3743cb66129e262add8996fc588df0b1a33366df4e5d618ec14d0bc8129f537000000000141b1ff5b5fdad24aafa550d42cb9f99c85f6175b3d65060079bb9638cacf654e0141754203644e6f3d5faf15f16492efec723da55b2db473b34299c5582e883e46000000000001d2ea556f49fb934dc76f087935a5c07788000b4e3aae24883adfec51b5f4d260"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"network": "mainnet",
|
||||
"height": 690000,
|
||||
"hash": "0000000000b1e6422ecd9292951b36ebb94e8926bbd33df8445b574b4be14f79",
|
||||
"time": 1578845180,
|
||||
"tree": "0117ffc074ef0f54651b2bc78d594e5ff786d9828ae78b1db972cd479669e8dd2401cc1b37d13f3b7d1fa2ead08493d275bfca976dd482e8dd879bf62b987652f63811013d84614158c7810753cc663f7a3da757f84f77744a24490eb07ce07af1daa92e0000017472a22c4064648ff260cbec8d85c273c5cd190dab7800f4978d473322dab1200001c7a1fd3786de051015c90f39143f3cfb89f2ea8bb5155520547ecfbefcdc382a0000000001d0c515cd513b49e397bf96d895a941aed4869ff2ff925939a34572c078dc16470121c1efd29f85680334050ee2a7e0d09fde474f90e573d85b7c9d337a5465625a0000000001d2ea556f49fb934dc76f087935a5c07788000b4e3aae24883adfec51b5f4d260"
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"network": "mainnet",
|
||||
"height": 692345,
|
||||
"hash": "0000000002584662ea3fb1969a65f05cf1e0c82581b885fbd723eed6ba818e99",
|
||||
"time": 1579021581,
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"network": "mainnet",
|
||||
"height": 693400,
|
||||
"hash": "0000000001708386101e361d211b2a14f3571d0b81f5962b452d563444c7f06a",
|
||||
"time": 1579101218,
|
||||
"tree": "0110939e236e3f13fbf9a044dc4e8d0094b777ee950dca49cb0722556b08fbef2f0197c4a6daa51f5c699ba5f0c3e53b657e54e3d728e60edaf76b4e2215d6aa2d661100000001ef66b21ca159b57a3d54147b0011c096d20cb3aa9590becf8f026c9edadba61a01750454b0edee9cc2f1eaf6d34cb8e495679048008d8cf6c1ab4321bddb828a2901f3dab23e140f2c400b4d4b5e6003ba2c7b316721b0d2858c8e0fcd1f5acbfd4b01eb786638efecd4413cfaadc48a0275035b2d484b92e305cb086c581a07390d21000001a4711f58e3fa6f5d38e2f54ab424c3014c119629fab5ee8a4ed2814d7b17036d01d0c515cd513b49e397bf96d895a941aed4869ff2ff925939a34572c078dc16470121c1efd29f85680334050ee2a7e0d09fde474f90e573d85b7c9d337a5465625a0000000001d2ea556f49fb934dc76f087935a5c07788000b4e3aae24883adfec51b5f4d260"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"network": "mainnet",
|
||||
"height": 700000,
|
||||
"hash": "0000000000c057d167a20ae61b1f77996bc72631dee6ffb11095f0d312230ddc",
|
||||
"time": 1579598443,
|
||||
"tree": "01c6b273aee226912526622b91e48a0ff5caf71f1f47569aff8a1c145102b02328012758ab750e1cb4f933ebca089d23ead6032151a38266aa020ae84557bb61844811016443e86acd06140aa932467bcc7235704cf95081e2e5faaf031112a9abd5f930016d1847eb52f8218773e3d2dd8eb19950dbe693484098d763010d7c338337cf68018117bb5e4ad68438572aaa55cb7d66b4b86b9d8310fbb4e36db7982dcc28591400012c4e84168b1c9a322f6035ddb5989fea843045d22182ee9ce45a6a8f6831954301abf6a411ff1708af6252bf921625f28931c567d92833d7ed2b2b14efd6b06e5001d1f934bce5476ef5d21b384c7dddfcbd8c1f630435acbf26a094bc46757f5d3501e6a69ddf114c92d39370a24e840c46ed42fc54a63986d3aa916a08c2a922c73b0001a626bb2ed07614f7228f79d5fbccf541699895842341602c639ab7516b1c9a1a0000019be74b905f0e99399af0fda6832324ceeeaf57551b11b42c73bcb7cd215ab91400000001d2ea556f49fb934dc76f087935a5c07788000b4e3aae24883adfec51b5f4d260"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"network": "mainnet",
|
||||
"height": 710000,
|
||||
"hash": "00000000003adcad055353d33a0962103e284bc47577c62580535a1dd6be7cf6",
|
||||
"time": 1580351806,
|
||||
"tree": "01bf61bd9326bba72206cc0ac82791fe316277907ba76773b5ae01ea7df948ae04001101200905d2485346e39f07fd989ba05211195251968d6bcb41b8280bc94733bc5d01e44932d89c5309ac906072235443f573a92dcda2acf608c1851af01dbeab19350000000001a73367559bf511fc5212ab3f0f6754a9b7ca59a3da68588e3763c801c031bd1501eb94b48e208bdbd42bb4815940b2f9f5187cc5e42c196f461f7bc6d020ed670f0001f00ddf03aca4e8c2620ff274939a1f1cd6a4eceb147e8aa6a8ba83717d60182700010576ef08575c3dd49296ba7c2ddd914715c4f9a7316da4ae8f5600dafa1b1c39019be74b905f0e99399af0fda6832324ceeeaf57551b11b42c73bcb7cd215ab91400000001d2ea556f49fb934dc76f087935a5c07788000b4e3aae24883adfec51b5f4d260"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"network": "mainnet",
|
||||
"height": 720000,
|
||||
"hash": "000000000225bab0e1491d6abfa4a41c174bc7d0167411e2cb2ebd960dae5158",
|
||||
"time": 1581104786,
|
||||
"tree": "0147c6af2c835328a4c17eff07f76102dc57716a13ce3d3a4f48367c3f2384fe2901e01e9b45be2ad8bedab63db1963c2b8d85e1ed20b6327cf2c55c211234e8a3351101134f80e61b548e384e87f823187d2734b07c516d48eea33a533c6cf7aa47052200000000000000017ce48111238d9e81b7e4147286578f2d686d71b1ec0cae668f567f3fef65bc0b017895f4c380f5169dcb84c7154fe6fcf72d694d30f0ba2535437b443a2cb5ae18000000013a1f7fb005388ac6f04099b647ed85d8b025d8ae4b178c2376b473b121b8c052000001d2ea556f49fb934dc76f087935a5c07788000b4e3aae24883adfec51b5f4d260"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"network": "mainnet",
|
||||
"height": 730000,
|
||||
"hash": "0000000002293b9e058e17fbc357c9b676d276eba338e033e357034c775ab320",
|
||||
"time": 1581858634,
|
||||
"tree": "019a59eba6efd060a61cde70daa7b34202e5fd55fcbf809eaf0aa3252e45810e48001101e106c6f8a17723af8793c1bd0f0e95dbcf5ef0bd80e20195422d8388db48cf24000001907e2c08367bfb45d196771ad267ec773c80ff4306aa7c4d2415ee22d211e90a00000001d72bda7061e4086bb885d6f26e39aa603a1f6db2e4fc71ae65a571c7a31ade27014df2a298ce5d7f8e88617b66ef7dc1fddd854e8dc623c3dc0faaa0eb93137d45017a48dab02dd9a014df0bd310657c3b8e854e24e1137f2ffcb1db693e38a4416d00011a5c078f7dd38704665b7270ebd90366fdd0edccdf284ca1f03c6d7e0536182800013a1f7fb005388ac6f04099b647ed85d8b025d8ae4b178c2376b473b121b8c052000001d2ea556f49fb934dc76f087935a5c07788000b4e3aae24883adfec51b5f4d260"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"network": "mainnet",
|
||||
"height": 735000,
|
||||
"hash": "00000000015c597fab53f58b9e1ededbe8bd83ca0203788e2039eceeb0d65ca6",
|
||||
"time": 1582235356,
|
||||
"tree": "0161f2ff97ff6ac6a90f9bce76c11710460f4944d8695aecc7dc99e34cad0131040011015325b185e23e82562db27817be996ffade9597181244f67efc40561aeb9dde1101daeffadc9e38f755bcb55a847a1278518a0ba4a2ef33b2fe01bbb3eb242ab0070000000000011c51f9077e3f7e28e8e337eaf4bb99b41acbc853a37dcc1e172467a1c919fe4100010bb1f55481b2268ef31997dc0fb6b48a530bc17870220f156d832326c433eb0a010b3768d3bf7868a67823e022f49be67982d0588e7041c498a756024750065a4a0001a9e1bf4bccb48b14b544e770f21d48f2d3ad8d6ca54eccc92f60634e3078eb48013a1f7fb005388ac6f04099b647ed85d8b025d8ae4b178c2376b473b121b8c052000001d2ea556f49fb934dc76f087935a5c07788000b4e3aae24883adfec51b5f4d260"
|
||||
}
|
|
@ -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"
|
||||
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue