Merge pull request #216 from zcash/integration/auto-shielding-poc

Integration of all auto-shielding PoC changes
This commit is contained in:
Kevin Gorham 2021-03-10 18:24:31 -05:00 committed by GitHub
commit 9dfc51967f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1779 additions and 466 deletions

394
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -22,13 +22,14 @@ zcash_primitives = "0.4"
zcash_proofs = "0.4"
#### Temporary additions: ####################################
base58 = "0.1.0"
protobuf = "2"
sha2 = "0.9"
bs58 = { version = "0.3", features = ["check"] }
hdwallet = "0.2.2"
ripemd160 = "0.9"
secp256k1 = "0.17.2"
#base58 = "0.1.0"
#protobuf = "2"
#sha2 = "0.9"
#bs58 = { version = "0.3", features = ["check"] }
#hdwallet = "0.2.2"
#ripemd160 = "0.9"
hdwallet = { git = "https://github.com/nuttycom/hdwallet", rev = "72f1f7a56c114eed484cefd6d402b7ef28158712"}
secp256k1 = "0.19"
##############################################################
# update-sapling-tree dependencies
@ -43,11 +44,11 @@ tls-api-rustls = { version = "0.4", optional = true }
[build-dependencies]
protobuf-codegen-pure = "2.14"
[patch.crates-io]
zcash_client_backend = { git = 'https://github.com/zcash/librustzcash.git', rev='04a2bd4ad86980e0c2862706bd402b85b9dd1965' }
zcash_client_sqlite = { git = 'https://github.com/zcash/librustzcash.git', rev='04a2bd4ad86980e0c2862706bd402b85b9dd1965' }
zcash_primitives = { git = 'https://github.com/zcash/librustzcash.git', rev='04a2bd4ad86980e0c2862706bd402b85b9dd1965' }
zcash_proofs = { git = 'https://github.com/zcash/librustzcash.git', rev='04a2bd4ad86980e0c2862706bd402b85b9dd1965' }
#[patch.crates-io]
#zcash_client_backend = { git = 'https://github.com/zcash/librustzcash.git', rev='04a2bd4ad86980e0c2862706bd402b85b9dd1965' }
#zcash_client_sqlite = { git = 'https://github.com/zcash/librustzcash.git', rev='04a2bd4ad86980e0c2862706bd402b85b9dd1965' }
#zcash_primitives = { git = 'https://github.com/zcash/librustzcash.git', rev='04a2bd4ad86980e0c2862706bd402b85b9dd1965' }
#zcash_proofs = { git = 'https://github.com/zcash/librustzcash.git', rev='04a2bd4ad86980e0c2862706bd402b85b9dd1965' }
# Uncomment this to test librustzcash changes locally
#[patch.crates-io]
@ -56,14 +57,16 @@ zcash_proofs = { git = 'https://github.com/zcash/librustzcash.git', rev='04a2bd4
#zcash_primitives = { path = '../../clones/librustzcash/zcash_primitives' }
#zcash_proofs = { path = '../../clones/librustzcash/zcash_proofs' }
# Uncomment this to test someone else's librustzcash changes in a branch
[patch.crates-io]
zcash_client_backend = {git = "https://github.com/nuttycom/librustzcash", branch = "data_access_api"}
zcash_client_sqlite = {git = "https://github.com/nuttycom/librustzcash", branch = "data_access_api"}
zcash_primitives = {git = "https://github.com/nuttycom/librustzcash", branch = "data_access_api"}
zcash_proofs = {git = "https://github.com/nuttycom/librustzcash", branch = "data_access_api"}
zcash_client_backend = {git = "https://github.com/nuttycom/librustzcash", branch = "autoshield-poc-daa"}
zcash_client_sqlite = {git = "https://github.com/nuttycom/librustzcash", branch = "autoshield-poc-daa"}
zcash_primitives = {git = "https://github.com/nuttycom/librustzcash", branch = "autoshield-poc-daa"}
zcash_proofs = {git = "https://github.com/nuttycom/librustzcash", branch = "autoshield-poc-daa"}
[features]
mainnet = ["zcash_client_sqlite/mainnet"]
mainnet = ["zcash_client_sqlite/mainnet", "zcash_client_sqlite/transparent-inputs", "zcash_client_backend/transparent-inputs", "zcash_primitives/transparent-inputs"]
testnet = ["zcash_client_backend/transparent-inputs", "zcash_client_sqlite/transparent-inputs", "zcash_primitives/transparent-inputs"]
updater = ["bls12_381", "futures", "grpc", "grpc-protobuf", "httpbis", "tls-api", "tls-api-rustls"]
[lib]

View File

@ -214,8 +214,10 @@ cargo {
defaultFeaturesAnd("mainnet")
}
zcashtestnetDebug {
defaultFeaturesAnd("testnet")
}
zcashtestnetRelease {
defaultFeaturesAnd("testnet")
}
}
}

View File

@ -1,10 +0,0 @@
use protobuf_codegen_pure;
fn main() {
protobuf_codegen_pure::Codegen::new()
.out_dir("src/main/rust")
.inputs(&["src/main/proto/local_rpc_types.proto"])
.includes(&["src/main/proto"])
.run()
.expect("Protobuf codegen failed");
}

View File

@ -6,8 +6,8 @@ object Deps {
const val kotlinVersion = "1.4.21"
const val group = "cash.z.ecc.android"
const val artifactName = "zcash-android-sdk"
const val versionName = "1.2.1-beta04"
const val versionCode = 1_02_01_204 // 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.
const val versionName = "1.2.1-beta05"
const val versionCode = 1_02_01_105 // 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.
const val description = "This lightweight SDK connects Android to Zcash. It welds together Rust and Kotlin in a minimal way, allowing third-party Android apps to send and receive shielded transactions easily, securely and privately."
const val githubUrl = "https://github.com/zcash/zcash-android-wallet-sdk"
@ -16,7 +16,7 @@ object Deps {
// to publish for local development run: ./gradlew publishToMavenLocal
// Remember: publish both mainnet and testnet!
const val publishingDryRun = true
val publishingTarget = Publication.Mainnet
val publishingTarget = Publication.Testnet
object Publication {
object Mainnet {

View File

@ -8,7 +8,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.0.1'
classpath 'com.android.tools.build:gradle:7.0.0-alpha04'
classpath"org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files

View File

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

View File

@ -0,0 +1,96 @@
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "11cfa01fe0b00e5d1e61a46e78f68ee2",
"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": []
},
{
"tableName": "utxos",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `address` TEXT NOT NULL, `txid` BLOB, `tx_index` INTEGER, `script` BLOB, `value` INTEGER NOT NULL, `height` INTEGER)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "address",
"columnName": "address",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "txid",
"columnName": "txid",
"affinity": "BLOB",
"notNull": false
},
{
"fieldPath": "transactionIndex",
"columnName": "tx_index",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "script",
"columnName": "script",
"affinity": "BLOB",
"notNull": false
},
{
"fieldPath": "value",
"columnName": "value",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "height",
"columnName": "height",
"affinity": "INTEGER",
"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, '11cfa01fe0b00e5d1e61a46e78f68ee2')"
]
}
}

View File

@ -2,7 +2,7 @@
"formatVersion": 1,
"database": {
"version": 5,
"identityHash": "d6e9b05e0607d399f821058adb43dc15",
"identityHash": "9431cf7a9bc49395e07834e4c81c5ed1",
"entities": [
{
"tableName": "transactions",
@ -334,12 +334,74 @@
]
}
]
},
{
"tableName": "utxos",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `address` TEXT NOT NULL, `prevout_txid` BLOB, `prevout_idx` INTEGER, `script` BLOB, `value_zat` INTEGER NOT NULL, `height` INTEGER, `spent_in_tx` INTEGER)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "address",
"columnName": "address",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "txid",
"columnName": "prevout_txid",
"affinity": "BLOB",
"notNull": false
},
{
"fieldPath": "transactionIndex",
"columnName": "prevout_idx",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "script",
"columnName": "script",
"affinity": "BLOB",
"notNull": false
},
{
"fieldPath": "value",
"columnName": "value_zat",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "height",
"columnName": "height",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "spent",
"columnName": "spent_in_tx",
"affinity": "INTEGER",
"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, 'd6e9b05e0607d399f821058adb43dc15')"
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9431cf7a9bc49395e07834e4c81c5ed1')"
]
}
}

View File

@ -0,0 +1,419 @@
{
"formatVersion": 1,
"database": {
"version": 6,
"identityHash": "fa97f2995039ee4a382a54d224f4d8b9",
"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"
]
}
]
},
{
"tableName": "utxos",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id_utxo` INTEGER, `address` TEXT NOT NULL, `prevout_txid` BLOB NOT NULL, `prevout_idx` INTEGER NOT NULL, `script` BLOB NOT NULL, `value_zat` INTEGER NOT NULL, `height` INTEGER NOT NULL, `spent_in_tx` INTEGER, PRIMARY KEY(`id_utxo`), FOREIGN KEY(`spent_in_tx`) REFERENCES `transactions`(`id_tx`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
"fields": [
{
"fieldPath": "id",
"columnName": "id_utxo",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "address",
"columnName": "address",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "txid",
"columnName": "prevout_txid",
"affinity": "BLOB",
"notNull": true
},
{
"fieldPath": "transactionIndex",
"columnName": "prevout_idx",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "script",
"columnName": "script",
"affinity": "BLOB",
"notNull": true
},
{
"fieldPath": "value",
"columnName": "value_zat",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "height",
"columnName": "height",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "spent",
"columnName": "spent_in_tx",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id_utxo"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": [
{
"table": "transactions",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"spent_in_tx"
],
"referencedColumns": [
"id_tx"
]
}
]
}
],
"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, 'fa97f2995039ee4a382a54d224f4d8b9')"
]
}
}

View File

@ -0,0 +1,69 @@
package cash.z.ecc.android.sdk.jni
import androidx.test.ext.junit.runners.AndroidJUnit4
import cash.z.ecc.android.bip39.Mnemonics.MnemonicCode
import cash.z.ecc.android.bip39.Mnemonics.WordCount.COUNT_24
import cash.z.ecc.android.bip39.toSeed
import cash.z.ecc.android.sdk.ext.TroubleshootingTwig
import cash.z.ecc.android.sdk.ext.Twig
import cash.z.ecc.android.sdk.ext.twig
import cash.z.ecc.android.sdk.tool.DerivationTool
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class TransparentTest {
@Before
fun setup() {
}
@Test
fun deriveTransparentSecretKeyTest() {
assertEquals(Expected.tskCompressed, DerivationTool.deriveTransparentSecretKey(SEED))
}
@Test
fun deriveTransparentAddressTest() {
assertEquals(Expected.tAddr, DerivationTool.deriveTransparentAddress(SEED))
}
@Test
fun deriveTransparentAddressFromSecretKeyTest() {
assertEquals(Expected.tAddr, DerivationTool.deriveTransparentAddress(Expected.tskCompressed))
}
// @Test
// fun deriveTransparentAddressFromSecretKeyTest2() {
// while(false) {
// MnemonicCode(COUNT_24).let { phrase ->
// val addr = DerivationTool.deriveShieldedAddress(phrase.toSeed())
// twig("$addr${String(phrase.chars)}\t")
// }
// }
// }
companion object {
const val PHRASE = "deputy visa gentle among clean scout farm drive comfort patch skin salt ranch cool ramp warrior drink narrow normal lunch behind salt deal person"
val MNEMONIC = MnemonicCode(PHRASE)
val SEED = MNEMONIC.toSeed()
object Expected {
val tAddr = "t1PKtYdJJHhc3Pxowmznkg7vdTwnhEsCvR4"
// private key in compressed Wallet Import Format (WIF)
val tskCompressed = "L4BvDC33yLjMRxipZvdiUmdYeRfZmR8viziwsVwe72zJdGbiJPv2"
}
@BeforeClass
@JvmStatic
fun startup() {
Twig.plant(TroubleshootingTwig(formatter = {"@TWIG $it"}))
}
}
}

View File

@ -0,0 +1,110 @@
package cash.z.ecc.android.sdk.sample
import androidx.test.platform.app.InstrumentationRegistry
import cash.z.ecc.android.bip39.Mnemonics
import cash.z.ecc.android.bip39.toSeed
import cash.z.ecc.android.sdk.Initializer
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.Synchronizer.Status.SYNCED
import cash.z.ecc.android.sdk.ext.TroubleshootingTwig
import cash.z.ecc.android.sdk.ext.Twig
import cash.z.ecc.android.sdk.ext.twig
import cash.z.ecc.android.sdk.tool.DerivationTool
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.newFixedThreadPoolContext
import kotlinx.coroutines.runBlocking
import org.junit.Assert
import org.junit.Test
/**
* Samples related to shielding funds.
*/
class ShieldFundsSample {
val SEED_PHRASE = "wish puppy smile loan doll curve hole maze file ginger hair nose key relax knife witness cannon grab despair throw review deal slush frame"//\"still champion voice habit trend flight survey between bitter process artefact blind carbon truly provide dizzy crush flush breeze blouse charge solid fish spread\"//\"deputy visa gentle among clean scout farm drive comfort patch skin salt ranch cool ramp warrior drink narrow normal lunch behind salt deal person"//"deputy visa gentle among clean scout farm drive comfort patch skin salt ranch cool ramp warrior drink narrow normal lunch behind salt deal person"
// simple flag to turn off actually spending funds
val IS_DRY_RUN = true
/**
* This test will construct a t2z transaction. It is safe to run this repeatedly, because
* nothing is submitted to the network (because the keys don't match the address so the encoding
* fails). Originally, it's intent is just to exercise the code and troubleshoot any issues but
* then it became clear that this would be a cool Sample Test and PoC for writing a SimpleWallet
* class.
*/
@Test
fun constructT2Z() = runBlocking {
Twig.sprout("ShieldFundsSample")
val wallet = SimpleWallet(SEED_PHRASE).sync()
wallet.shieldFunds()
Twig.clip("ShieldFundsSample")
Assert.assertEquals(5, wallet.synchronizer.latestBalance.availableZatoshi)
}
// when startHeight is null, it will use the latest checkpoint
class SimpleWallet(seedPhrase: String, startHeight: Int? = null) {
val walletScope = CoroutineScope(
SupervisorJob() + newFixedThreadPoolContext(3, this.javaClass.simpleName)
)
private val context = InstrumentationRegistry.getInstrumentation().context
private val seed: ByteArray = Mnemonics.MnemonicCode(seedPhrase).toSeed()
private val shieldedSpendingKey = DerivationTool.deriveSpendingKeys(seed)[0]
private val transparentSecretKey = DerivationTool.deriveTransparentSecretKey(seed)
private val shieldedAddress = DerivationTool.deriveShieldedAddress(seed)
// t1b9Y6PESSGavavgge3ruTtX9X83817V29s
private val transparentAddress = DerivationTool.deriveTransparentAddress(seed)
private val config = Initializer.Config {
it.setSeed(seed)
it.setBirthdayHeight(startHeight, false)
it.server("lightwalletd.electriccoin.co", 9067)
}
val synchronizer = Synchronizer(Initializer(context, config))
suspend fun sync(): SimpleWallet {
twig("Starting sync")
synchronizer.start(walletScope)
// block until synced
synchronizer.status.first { it == SYNCED }
twig("Synced!")
return this
}
suspend fun shieldFunds(): SimpleWallet {
twig("checking $transparentAddress for transactions!")
synchronizer.refreshUtxos(transparentAddress, 935000).let { count ->
twig("FOUND $count new UTXOs")
}
synchronizer.getTransparentBalance(transparentAddress).let { walletBalance ->
twig("FOUND utxo balance of total: ${walletBalance.totalZatoshi} available: ${walletBalance.availableZatoshi}")
if (walletBalance.availableZatoshi > 0L && !IS_DRY_RUN) {
synchronizer.shieldFunds(shieldedSpendingKey, transparentSecretKey)
.onCompletion { twig("done shielding funds") }
.catch { twig("Failed with $it") }
.collect()
}
}
return this
}
companion object {
init {
Twig.plant(TroubleshootingTwig())
}
}
}
}

View File

@ -40,6 +40,7 @@ import cash.z.ecc.android.sdk.ext.twigTask
import cash.z.ecc.android.sdk.jni.RustBackend
import cash.z.ecc.android.sdk.service.LightWalletGrpcService
import cash.z.ecc.android.sdk.service.LightWalletService
import cash.z.ecc.android.sdk.tool.DerivationTool
import cash.z.ecc.android.sdk.transaction.OutboundTransactionManager
import cash.z.ecc.android.sdk.transaction.PagedTransactionRepository
import cash.z.ecc.android.sdk.transaction.PersistentTransactionManager
@ -460,6 +461,15 @@ class SdkSynchronizer internal constructor(
}
}
// Experimental: cleanup failed transactions
allPendingTxs.filter { it.isSubmitted() && it.isFailedSubmit() && !it.isMarkedForDeletion()}.let { failed ->
failed.forEachIndexed { index, pendingTx ->
twig("[cleanup] FOUND (${index + 1} of ${failed.size})" +
" FAILED pendingTxId: ${pendingTx.id}")
cleanupCancelledTx(pendingTx)
}
}
twig("[cleanup] beginning to cleanup expired transactions")
// Experimental: cleanup expired transactions
// note: don't delete the pendingTx until the related data has been scrubbed, or else you
@ -500,7 +510,13 @@ class SdkSynchronizer internal constructor(
override suspend fun cancelSpend(pendingId: Long) = txManager.cancel(pendingId)
override suspend fun getAddress(accountId: Int): String = processor.getAddress(accountId)
override suspend fun getAddress(accountId: Int): String = getShieldedAddress(accountId)
override suspend fun getShieldedAddress(accountId: Int): String = processor.getShieldedAddress(accountId)
override suspend fun getTransparentAddress(seed: ByteArray, accountId: Int, index: Int): String {
return DerivationTool.deriveTransparentAddress(seed, accountId, index)
}
override fun sendToAddress(
spendingKey: String,
@ -529,6 +545,45 @@ class SdkSynchronizer internal constructor(
txManager.monitorById(it.id)
}.distinctUntilChanged()
override fun shieldFunds(
spendingKey: String,
transparentSecretKey: String,
memo: String
): Flow<PendingTransaction> = flow {
twig("Initializing shielding transaction")
val tAddr = DerivationTool.deriveTransparentAddress(transparentSecretKey)
val tBalance = processor.getUtxoCacheBalance(tAddr)
val zAddr = getAddress(0)
// Emit the placeholder transaction, then switch to monitoring the database
txManager.initSpend(tBalance.availableZatoshi, zAddr, memo, 0).let { placeHolderTx ->
emit(placeHolderTx)
txManager.encode(spendingKey, transparentSecretKey, placeHolderTx).let { encodedTx ->
// only submit if it wasn't cancelled. Otherwise cleanup, immediately for best UX.
if (encodedTx.isCancelled()) {
twig("[cleanup] this shielding tx has been cancelled so we will cleanup instead of submitting")
if (cleanupCancelledTx(encodedTx)) refreshBalance()
encodedTx
} else {
txManager.submit(encodedTx)
}
}
}
}.flatMapLatest {
twig("Monitoring shielding transaction (id: ${it.id}) for updates...")
txManager.monitorById(it.id)
}.distinctUntilChanged()
override suspend fun refreshUtxos(address: String, sinceHeight: Int): Int {
// TODO: we need to think about how we restrict this to only our taddr
return processor.downloadUtxos(address, sinceHeight)
}
override suspend fun getTransparentBalance(tAddr: String): WalletBalance {
return processor.getUtxoCacheBalance(tAddr)
}
override suspend fun isValidShieldedAddr(address: String) =
txManager.isValidShieldedAddress(address)

View File

@ -120,14 +120,38 @@ interface Synchronizer {
//
/**
* Gets the address for the given account.
* Gets the shielded address for the given account. This is syntactic sugar for
* [getShieldedAddress] because we use z-addrs by default.
*
* @param accountId the optional accountId whose address is of interest. By default, the first
* account is used.
*
* @return the address for the given account.
* @return the shielded address for the given account.
*/
suspend fun getAddress(accountId: Int = 0): String
suspend fun getAddress(accountId: Int = 0) = getShieldedAddress(accountId)
/**
* Gets the shielded address for the given account.
*
* @param accountId the optional accountId whose address is of interest. By default, the first
* account is used.
*
* @return the shielded address for the given account.
*/
suspend fun getShieldedAddress(accountId: Int = 0): String
/**
* Gets the transparent address for the given account and index.
*
* @param accountId the optional accountId whose address is of interest. By default, the first
* account is used.
* @param index the optional index whose address is of interest. By default, the first index is
* used.
*
* @return the address for the given account and index.
*/
suspend fun getTransparentAddress(seed: ByteArray, accountId: Int = 0, index: Int = 0): String
/**
* Sends zatoshi.
@ -151,6 +175,12 @@ interface Synchronizer {
fromAccountIndex: Int = 0
): Flow<PendingTransaction>
fun shieldFunds(
spendingKey: String,
transparentSecretKey: String,
memo: String = ZcashSdk.DEFAULT_SHIELD_FUNDS_MEMO_PREFIX
): Flow<PendingTransaction>
/**
* Returns true when the given address is a valid z-addr. Invalid addresses will throw an
* exception. Valid z-addresses have these characteristics: //TODO copy info from related ZIP
@ -230,6 +260,13 @@ interface Synchronizer {
errorHandler: (Throwable) -> Unit = { throw it }
)
suspend fun refreshUtxos(tAddr: String, sinceHeight: Int): Int
/**
* Returns the balance that the wallet knows about. This should be called after [refreshUtxos].
*/
suspend fun getTransparentBalance(tAddr: String): WalletBalance
//
// Error Handling
//

View File

@ -34,6 +34,7 @@ import cash.z.ecc.android.sdk.jni.RustBackend
import cash.z.ecc.android.sdk.jni.RustBackendWelding
import cash.z.ecc.android.sdk.transaction.PagedTransactionRepository
import cash.z.ecc.android.sdk.transaction.TransactionRepository
import cash.z.wallet.sdk.rpc.Service
import io.grpc.StatusRuntimeException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.IO
@ -313,6 +314,33 @@ class CompactBlockProcessor(
if (!repository.isInitialized()) throw CompactBlockProcessorException.Uninitialized
}
internal suspend fun downloadUtxos(tAddress: String, startHeight: Int): Int = withContext(IO) {
var skipped = 0
twig("Downloading utxos starting at height $startHeight")
downloader.lightWalletService.fetchUtxos(tAddress, startHeight).let { result ->
result.forEach { utxo: Service.GetAddressUtxosReply ->
twig("Found UTXO at height ${utxo.height.toInt()}")
try {
rustBackend.putUtxo(
tAddress,
utxo.txid.toByteArray(),
utxo.index,
utxo.script.toByteArray(),
utxo.valueZat,
utxo.height.toInt()
)
} catch (t: Throwable) {
// TODO: more accurately track the utxos that were skipped (in theory, this could fail for other reasons)
skipped++
twig("Warning: Ignoring transaction at height ${utxo.height} @ index ${utxo.index} because it already exists")
}
}
// return the number of UTXOs that were downloaded
result.size - skipped
}
}
/**
* Request all blocks in the given range and persist them locally for processing, later.
*
@ -546,8 +574,8 @@ class CompactBlockProcessor(
*
* @return the address of this wallet.
*/
suspend fun getAddress(accountId: Int) = withContext(IO) {
rustBackend.getAddress(accountId)
suspend fun getShieldedAddress(accountId: Int) = withContext(IO) {
rustBackend.getShieldedAddress(accountId)
}
/**
@ -572,6 +600,11 @@ class CompactBlockProcessor(
}
}
suspend fun getUtxoCacheBalance(address: String): WalletBalance = withContext(IO) {
rustBackend.getDownloadedUtxoBalance(address)
}
/**
* Transmits the given state for this processor.
*/

View File

@ -34,9 +34,10 @@ import cash.z.ecc.android.sdk.ext.twig
Block::class,
Received::class,
Account::class,
Sent::class
Sent::class,
Utxo::class
],
version = 5,
version = 6,
exportSchema = true
)
abstract class DerivedDataDb : RoomDatabase() {
@ -138,6 +139,26 @@ abstract class DerivedDataDb : RoomDatabase() {
database.execSQL("PRAGMA foreign_keys = ON;")
}
}
val MIGRATION_5_6 = object : Migration(5, 6) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS utxos (
id_utxo INTEGER PRIMARY KEY,
address TEXT NOT NULL,
prevout_txid BLOB NOT NULL,
prevout_idx INTEGER NOT NULL,
script BLOB NOT NULL,
value_zat INTEGER NOT NULL,
height INTEGER NOT NULL,
spent_in_tx INTEGER,
FOREIGN KEY (spent_in_tx) REFERENCES transactions(id_tx),
CONSTRAINT tx_outpoint UNIQUE (prevout_txid, prevout_idx)
); """.trimIndent()
)
}
}
}
}
@ -394,6 +415,12 @@ interface TransactionDao {
twig("[cleanup] WARNING: deleting invalid sent noteId:$noteId")
deleteSentNote(noteId)
}
// delete the UTXOs because these are effectively cached and we don't have a good way of knowing whether they're spent
deleteUtxos(transactionId).let { count ->
twig("[cleanup] removed $count UTXOs matching transactionId $transactionId")
}
twig("[cleanup] WARNING: deleting invalid transactionId $transactionId")
success = deleteTransaction(transactionId) != 0
twig("[cleanup] removeInvalidTransaction Done. success? $success")
@ -454,6 +481,9 @@ interface TransactionDao {
@Query("UPDATE received_notes SET spent = null WHERE spent = :transactionId")
fun unspendTransactionNotes(transactionId: Long): Int
@Query("DELETE FROM utxos WHERE spent_in_tx = :utxoId")
fun deleteUtxos(utxoId: Long): Int
@Query(
"""
SELECT transactions.id_tx

View File

@ -0,0 +1,72 @@
package cash.z.ecc.android.sdk.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
import org.jetbrains.annotations.NotNull
@Entity(tableName = "utxos",
primaryKeys = ["id_utxo"],
foreignKeys = [ForeignKey(
entity = TransactionEntity::class,
parentColumns = ["id_tx"],
childColumns = ["spent_in_tx"]
)]
)
data class Utxo(
@ColumnInfo(name = "id_utxo")
val id: Long? = 0L,
val address: String ="",
@ColumnInfo(name = "prevout_txid", typeAffinity = ColumnInfo.BLOB)
val txid: ByteArray = byteArrayOf(),
@ColumnInfo(name = "prevout_idx")
val transactionIndex: Int = -1,
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
val script: ByteArray = byteArrayOf(),
@ColumnInfo(name = "value_zat")
val value: Long = 0L,
val height: Int = -1,
/**
* A reference to the transaction this note was later spent in
*/
@ColumnInfo(name = "spent_in_tx")
val spent: Int? = 0,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Utxo) return false
if (id != other.id) return false
if (address != other.address) return false
if (!txid.contentEquals(other.txid)) return false
if (transactionIndex != other.transactionIndex) return false
if (!script.contentEquals(other.script)) return false
if (value != other.value) return false
if (height != other.height) return false
if (spent != other.spent) return false
return true
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + address.hashCode()
result = 31 * result + txid.contentHashCode()
result = 31 * result + transactionIndex
result = 31 * result + script.contentHashCode()
result = 31 * result + value.hashCode()
result = 31 * result + height
result = 31 * result + (spent ?: 0)
return result
}
}

View File

@ -110,4 +110,8 @@ open class ZcashSdkCommon {
* this will do for now, since we're using a cloudfront URL that already redirects.
*/
val CLOUD_PARAM_DIR_URL = "https://z.cash/downloads/"
/**
* The default memo to use when shielding transparent funds.
*/
open val DEFAULT_SHIELD_FUNDS_MEMO_PREFIX = "shielding:"
}

View File

@ -1,10 +1,10 @@
package cash.z.ecc.android.sdk.jni
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
import cash.z.ecc.android.sdk.exception.BirthdayException
import cash.z.ecc.android.sdk.ext.ZcashSdk.OUTPUT_PARAM_FILE_NAME
import cash.z.ecc.android.sdk.ext.ZcashSdk.SPEND_PARAM_FILE_NAME
import cash.z.ecc.android.sdk.ext.twig
import cash.z.ecc.android.sdk.rpc.LocalRpcTypes
import java.io.File
/**
@ -64,7 +64,11 @@ class RustBackend private constructor() : RustBackendWelding {
return initBlocksTable(pathDataDb, height, hash, time, saplingTree)
}
override fun getAddress(account: Int) = getAddress(pathDataDb, account)
override fun getShieldedAddress(account: Int) = getShieldedAddress(pathDataDb, account)
override fun getTransparentAddress(account: Int, index: Int): String {
throw NotImplementedError("TODO: implement this at the zcash_client_sqlite level. But for now, use DerivationTool, instead to derive addresses from seeds")
}
override fun getBalance(account: Int) = getBalance(pathDataDb, account)
@ -108,30 +112,62 @@ class RustBackend private constructor() : RustBackendWelding {
"$pathParamsDir/$OUTPUT_PARAM_FILE_NAME"
)
override fun shieldToAddress(
extsk: String,
tsk: String,
memo: ByteArray?
): Long {
twig("TMP: shieldToAddress with db path: $pathDataDb, ${memo?.size}")
return shieldToAddress(
pathDataDb,
0,
extsk,
tsk,
memo ?: ByteArray(0),
"${pathParamsDir}/$SPEND_PARAM_FILE_NAME",
"${pathParamsDir}/$OUTPUT_PARAM_FILE_NAME"
)
}
override fun putUtxo(
tAddress: String,
txId: ByteArray,
index: Int,
script: ByteArray,
value: Long,
height: Int
): Boolean = putUtxo(pathDataDb, tAddress, txId, index, script, value, height)
override fun getDownloadedUtxoBalance(address: String): CompactBlockProcessor.WalletBalance {
val verified = getVerifiedTransparentBalance(pathDataDb, address)
val total = getTotalTransparentBalance(pathDataDb, address)
return CompactBlockProcessor.WalletBalance(total, verified)
}
override fun isValidShieldedAddr(addr: String) = isValidShieldedAddress(addr)
override fun isValidTransparentAddr(addr: String) = isValidTransparentAddress(addr)
override fun getBranchIdForHeight(height: Int): Long = branchIdForHeight(height)
/**
* This is a proof-of-concept for doing Local RPC, where we are effectively using the JNI
* boundary as a grpc server. It is slightly inefficient in terms of both space and time but
* given that it is all done locally, on the heap, it seems to be a worthwhile tradeoff because
* it reduces the complexity and expands the capacity for the two layers to communicate.
*
* We're able to keep the "unsafe" byteArray functions private and wrap them in typeSafe
* equivalents and, eventually, surface any parse errors (for now, errors are only logged).
*/
override fun parseTransactionDataList(tdl: LocalRpcTypes.TransactionDataList): LocalRpcTypes.TransparentTransactionList {
return try {
// serialize the list, send it over to rust and get back a serialized set of results that we parse out and return
return LocalRpcTypes.TransparentTransactionList.parseFrom(parseTransactionDataList(tdl.toByteArray()))
} catch (t: Throwable) {
twig("ERROR: failed to parse transaction data list due to: $t caused by: ${t.cause}")
LocalRpcTypes.TransparentTransactionList.newBuilder().build()
}
}
// /**
// * This is a proof-of-concept for doing Local RPC, where we are effectively using the JNI
// * boundary as a grpc server. It is slightly inefficient in terms of both space and time but
// * given that it is all done locally, on the heap, it seems to be a worthwhile tradeoff because
// * it reduces the complexity and expands the capacity for the two layers to communicate.
// *
// * We're able to keep the "unsafe" byteArray functions private and wrap them in typeSafe
// * equivalents and, eventually, surface any parse errors (for now, errors are only logged).
// */
// override fun parseTransactionDataList(tdl: LocalRpcTypes.TransactionDataList): LocalRpcTypes.TransparentTransactionList {
// return try {
// // serialize the list, send it over to rust and get back a serialized set of results that we parse out and return
// return LocalRpcTypes.TransparentTransactionList.parseFrom(parseTransactionDataList(tdl.toByteArray()))
// } catch (t: Throwable) {
// twig("ERROR: failed to parse transaction data list due to: $t caused by: ${t.cause}")
// LocalRpcTypes.TransparentTransactionList.newBuilder().build()
// }
// }
/**
* Exposes all of the librustzcash functions along with helpers for loading the static library.
@ -208,7 +244,9 @@ class RustBackend private constructor() : RustBackendWelding {
saplingTree: String
): Boolean
@JvmStatic private external fun getAddress(dbDataPath: String, account: Int): String
@JvmStatic private external fun getShieldedAddress(dbDataPath: String, account: Int): String
// TODO: implement this in the zcash_client_sqlite layer. For now, use DerivationTool, instead.
// @JvmStatic private external fun getTransparentAddress(dbDataPath: String, account: Int): String
@JvmStatic private external fun isValidShieldedAddress(addr: String): Boolean
@ -244,10 +282,38 @@ class RustBackend private constructor() : RustBackendWelding {
outputParamsPath: String
): Long
@JvmStatic private external fun shieldToAddress(
dbDataPath: String,
account: Int,
extsk: String,
tsk: String,
memo: ByteArray,
spendParamsPath: String,
outputParamsPath: String
): Long
@JvmStatic private external fun initLogs()
@JvmStatic private external fun branchIdForHeight(height: Int): Long
@JvmStatic private external fun parseTransactionDataList(serializedList: ByteArray): ByteArray
@JvmStatic private external fun putUtxo(
dbDataPath: String,
tAddress: String,
txId: ByteArray,
index: Int,
script: ByteArray,
value: Long,
height: Int
): Boolean
@JvmStatic private external fun getVerifiedTransparentBalance(
pathDataDb: String,
taddr: String
): Long
@JvmStatic private external fun getTotalTransparentBalance(
pathDataDb: String,
taddr: String
): Long
}
}

View File

@ -1,6 +1,6 @@
package cash.z.ecc.android.sdk.jni
import cash.z.ecc.android.sdk.rpc.LocalRpcTypes
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
/**
* Contract defining the exposed capabilities of the Rust backend.
@ -19,6 +19,12 @@ interface RustBackendWelding {
memo: ByteArray? = byteArrayOf()
): Long
fun shieldToAddress(
extsk: String,
tsk: String,
memo: ByteArray? = byteArrayOf()
): Long
fun decryptAndStoreTransaction(tx: ByteArray)
fun initAccountsTable(seed: ByteArray, numberOfAccounts: Int): Array<String>
@ -33,7 +39,9 @@ interface RustBackendWelding {
fun isValidTransparentAddr(addr: String): Boolean
fun getAddress(account: Int = 0): String
fun getShieldedAddress(account: Int = 0): String
fun getTransparentAddress(account: Int = 0, index: Int = 0): String
fun getBalance(account: Int = 0): Long
@ -45,7 +53,7 @@ interface RustBackendWelding {
fun getVerifiedBalance(account: Int = 0): Long
fun parseTransactionDataList(tdl: LocalRpcTypes.TransactionDataList): LocalRpcTypes.TransparentTransactionList
// fun parseTransactionDataList(tdl: LocalRpcTypes.TransactionDataList): LocalRpcTypes.TransparentTransactionList
fun rewindToHeight(height: Int): Boolean
@ -53,6 +61,17 @@ interface RustBackendWelding {
fun validateCombinedChain(): Int
fun putUtxo(
tAddress: String,
txId: ByteArray,
index: Int,
script: ByteArray,
value: Long,
height: Int
): Boolean
fun getDownloadedUtxoBalance(address: String): CompactBlockProcessor.WalletBalance
// Implemented by `DerivationTool`
interface Derivation {
fun deriveShieldedAddress(viewingKey: String): String
@ -61,7 +80,11 @@ interface RustBackendWelding {
fun deriveSpendingKeys(seed: ByteArray, numberOfAccounts: Int = 1): Array<String>
fun deriveTransparentAddress(seed: ByteArray): String
fun deriveTransparentAddress(seed: ByteArray, account: Int = 0, index: Int = 0): String
fun deriveTransparentAddress(transparentSecretKey: String): String
fun deriveTransparentSecretKey(seed: ByteArray, account: Int = 0, index: Int = 0): String
fun deriveViewingKey(spendingKey: String): String

View File

@ -106,6 +106,18 @@ class LightWalletGrpcService private constructor(
)
}
override fun fetchUtxos(
tAddress: String,
startHeight: Int
): List<Service.GetAddressUtxosReply> {
channel.resetConnectBackoff()
val result = channel.createStub().getAddressUtxos(
Service.GetAddressUtxosArg.newBuilder().setAddress(tAddress)
.setStartHeight(startHeight.toLong()).build()
)
return result.addressUtxosList
}
override fun getTAddressTransactions(
tAddress: String,
blockHeightRange: IntRange

View File

@ -16,6 +16,16 @@ interface LightWalletService {
*/
fun fetchTransaction(txId: ByteArray): Service.RawTransaction?
/**
* Fetch all UTXOs for the given address, going back to the start height.
*
* @param tAddress the transparent address to use.
* @param startHeight the starting height to use.
*
* @return the UTXOs for the given address from the startHeight.
*/
fun fetchUtxos(tAddress: String, startHeight: Int): List<Service.GetAddressUtxosReply>
/**
* Return the given range of blocks.
*

View File

@ -75,8 +75,16 @@ class DerivationTool {
// WIP probably shouldn't be used just yet. Why?
// - because we need the private key associated with this seed and this function doesn't return it.
// - the underlying implementation needs to be split out into a few lower-level calls
override fun deriveTransparentAddress(seed: ByteArray): String = withRustBackendLoaded {
deriveTransparentAddressFromSeed(seed)
override fun deriveTransparentAddress(seed: ByteArray, account: Int, index: Int): String = withRustBackendLoaded {
deriveTransparentAddressFromSeed(seed, account, index)
}
override fun deriveTransparentAddress(transparentSecretKey: String): String = withRustBackendLoaded {
deriveTransparentAddressFromSecretKey(transparentSecretKey)
}
override fun deriveTransparentSecretKey(seed: ByteArray, account: Int, index: Int): String = withRustBackendLoaded {
deriveTransparentSecretKeyFromSeed(seed, account, index)
}
fun validateViewingKey(viewingKey: String) {
@ -122,6 +130,13 @@ class DerivationTool {
private external fun deriveShieldedAddressFromViewingKey(key: String): String
@JvmStatic
private external fun deriveTransparentAddressFromSeed(seed: ByteArray): String
private external fun deriveTransparentAddressFromSeed(seed: ByteArray, account: Int, index: Int): String
@JvmStatic
private external fun deriveTransparentAddressFromSecretKey(tsk: String): String
@JvmStatic
private external fun deriveTransparentSecretKeyFromSeed(seed: ByteArray, account: Int, index: Int): String
}
}

View File

@ -70,10 +70,16 @@ class SaplingParamTool {
twig("directory did not exist attempting to make it")
file.parentFile.mkdirs()
}
Okio.buffer(Okio.sink(file)).use {
twig("writing to $file")
it.writeAll(response.body().source())
}
} else {
failureMessage += "Error while fetching $paramFileName : $response\n"
twig(failureMessage)
}
twig("fetch succeeded, done writing $paramFileName")
}
if (failureMessage.isNotEmpty()) throw TransactionEncoderException.FetchParamsException(
failureMessage

View File

@ -39,6 +39,7 @@ open class PagedTransactionRepository(
.addMigrations(DerivedDataDb.MIGRATION_3_4)
.addMigrations(DerivedDataDb.MIGRATION_4_3)
.addMigrations(DerivedDataDb.MIGRATION_4_5)
.addMigrations(DerivedDataDb.MIGRATION_5_6)
.build(),
pageSize
)

View File

@ -139,6 +139,40 @@ class PersistentTransactionManager(
tx
}
override suspend fun encode(
spendingKey: String,
transparentSecretKey: String,
pendingTx: PendingTransaction
): PendingTransaction {
twig("managing the creation of a shielding transaction")
var tx = pendingTx as PendingTransactionEntity
try {
twig("beginning to encode shielding transaction with : $encoder")
val encodedTx = encoder.createShieldingTransaction(
spendingKey,
transparentSecretKey,
tx.memo
)
twig("successfully encoded shielding transaction!")
safeUpdate("updating shielding transaction encoding") {
updateEncoding(tx.id, encodedTx.raw, encodedTx.txId, encodedTx.expiryHeight)
}
} catch (t: Throwable) {
val message = "failed to encode shielding transaction due to : ${t.message} caused by: ${t.cause}"
twig(message)
safeUpdate("updating shielding transaction error info") {
updateError(tx.id, message, ERROR_ENCODING)
}
} finally {
safeUpdate("incrementing shielding transaction encodeAttempts (from: ${tx.encodeAttempts})") {
updateEncodeAttempts(tx.id, max(1, tx.encodeAttempts + 1))
tx = findById(tx.id)!!
}
}
return tx
}
override suspend fun submit(pendingTx: PendingTransaction): PendingTransaction = withContext(Dispatchers.IO) {
// reload the tx to check for cancellation
var tx = pendingTransactionDao { findById(pendingTx.id) }

View File

@ -24,6 +24,12 @@ interface TransactionEncoder {
fromAccountIndex: Int = 0
): EncodedTransaction
suspend fun createShieldingTransaction(
spendingKey: String,
transparentSecretKey: String,
memo: ByteArray? = byteArrayOf()
): EncodedTransaction
/**
* Utility function to help with validation. This is not called during [createTransaction]
* because this class asserts that all validation is done externally by the UI, for now.

View File

@ -39,6 +39,12 @@ interface OutboundTransactionManager {
*/
suspend fun encode(spendingKey: String, pendingTx: PendingTransaction): PendingTransaction
suspend fun encode(
spendingKey: String,
transparentSecretKey: String,
pendingTx: PendingTransaction
): PendingTransaction
/**
* Submits the transaction represented by [pendingTx] to lightwalletd to broadcast to the
* network and, hopefully, include in the next block.

View File

@ -51,6 +51,17 @@ class WalletTransactionEncoder(
?: throw TransactionEncoderException.TransactionNotFoundException(transactionId)
}
override suspend fun createShieldingTransaction(
spendingKey: String,
transparentSecretKey: String,
memo: ByteArray?
): EncodedTransaction = withContext(IO) {
twig("TMP: createShieldingTransaction with $spendingKey and $transparentSecretKey and ${memo?.size}")
val transactionId = createShieldingSpend(spendingKey, transparentSecretKey, memo)
repository.findEncodedTransactionById(transactionId)
?: throw TransactionEncoderException.TransactionNotFoundException(transactionId)
}
/**
* Utility function to help with validation. This is not called during [createTransaction]
* because this class asserts that all validation is done externally by the UI, for now.
@ -126,4 +137,29 @@ class WalletTransactionEncoder(
twig("result of sendToAddress: $result")
}
}
private suspend fun createShieldingSpend(
spendingKey: String,
transparentSecretKey: String,
memo: ByteArray? = byteArrayOf()
): Long = withContext(IO) {
twigTask("creating transaction to shield all UTXOs") {
try {
SaplingParamTool.ensureParams((rustBackend as RustBackend).pathParamsDir)
twig("params exist! attempting to shield...")
rustBackend.shieldToAddress(
spendingKey,
transparentSecretKey,
memo
)
} catch (t: Throwable) {
// TODO: if this error matches: Insufficient balance (have 0, need 1000 including fee)
// then consider custom error that says no UTXOs existed to shield
twig("Shield failed due to: ${t.message}")
throw t
}
}.also { result ->
twig("result of shieldToAddress: $result")
}
}
}

View File

@ -1,22 +0,0 @@
syntax = "proto3";
package cash.z.ecc.android.sdk.rpc;
option go_package = "walletrpc";
message TransactionDataList {
repeated bytes data = 1;
}
message TransparentTransactionList {
repeated TransparentTransaction transactions = 1;
}
message TransparentTransaction {
uint32 protoVersion = 1; // the version of this wire format, for storage
uint32 expiryHeight = 2;
bool hasShieldedOutputs = 3;
bool hasShieldedSpends = 4;
uint32 height = 5;
int64 value = 6;
string toAddress = 7;
string fromAddress = 8;
}

View File

@ -34,7 +34,7 @@ message TxFilter {
// RawTransaction contains the complete transaction data. It also optionally includes
// the block height in which the transaction was included.
message RawTransaction {
bytes data = 1; // exact data returned by zcash 'getrawtransaction'
bytes data = 1; // exact data returned by Zcash 'getrawtransaction'
uint64 height = 2; // height that the transaction was mined (or -1)
}
@ -66,6 +66,9 @@ message LightdInfo {
string branch = 9;
string buildDate = 10;
string buildUser = 11;
uint64 estimatedHeight = 12; // less than tip height if zcashd is syncing
string zcashdBuild = 13; // example: "v4.1.1-877212414"
string zcashdSubversion = 14; // example: "/MagicBean:4.1.1/"
}
// TransparentAddressBlockFilter restricts the results to the given address
@ -104,7 +107,7 @@ message Exclude {
repeated bytes txid = 1;
}
// The TreeState is derived from the zcash z_gettreestate rpc.
// The TreeState is derived from the Zcash z_gettreestate rpc.
message TreeState {
string network = 1; // "main" or "test"
uint64 height = 2;
@ -139,7 +142,7 @@ service CompactTxStreamer {
// Return the requested full (not compact) transaction (as from zcashd)
rpc GetTransaction(TxFilter) returns (RawTransaction) {}
// Submit the given transaction to the zcash network
// Submit the given transaction to the Zcash network
rpc SendTransaction(RawTransaction) returns (SendResponse) {}
// Return the txids corresponding to the given t-address within the given block range
@ -159,7 +162,7 @@ service CompactTxStreamer {
rpc GetMempoolTx(Exclude) returns (stream CompactTx) {}
// GetTreeState returns the note commitment tree state corresponding to the given block.
// See section 3.7 of the zcash protocol specification. It returns several other useful
// See section 3.7 of the Zcash protocol specification. It returns several other useful
// values also (even though they can be obtained using GetBlock).
// The block can be specified by either height or hash.
rpc GetTreeState(BlockID) returns (TreeState) {}

View File

@ -1,20 +1,19 @@
#[macro_use]
extern crate log;
use std::convert::{TryFrom, TryInto};
use std::panic;
use std::path::Path;
use std::ptr;
use android_logger::Config;
use base58::ToBase58;
use failure::format_err;
use hdwallet::{ExtendedPrivKey, KeyIndex};
use jni::{
JNIEnv,
objects::{JClass, JString},
sys::{jboolean, jbyteArray, jint, jlong, JNI_FALSE, JNI_TRUE, jobjectArray, jstring},
};
use log::Level;
use std::convert::{TryFrom, TryInto};
use std::panic;
use std::path::Path;
use std::ptr;
use zcash_client_backend::{
address::RecipientAddress,
data_api::{
@ -24,45 +23,46 @@ use zcash_client_backend::{
WalletRead, WalletWrite,
},
encoding::{
decode_extended_full_viewing_key, decode_extended_spending_key,
encode_extended_full_viewing_key, encode_extended_spending_key, encode_payment_address,
AddressCodec, decode_extended_full_viewing_key,
decode_extended_spending_key, encode_extended_full_viewing_key, encode_extended_spending_key,
encode_payment_address,
},
keys::spending_key,
wallet::{AccountId, OvkPolicy},
keys::{
derive_secret_key_from_seed, derive_transparent_address_from_secret_key,
spending_key, Wif,
},
wallet::{AccountId, OvkPolicy, WalletTransparentOutput},
};
use zcash_client_backend::data_api::wallet::{shield_funds, ANCHOR_OFFSET};
use zcash_client_sqlite::{
wallet::init::{init_accounts_table, init_blocks_table, init_data_database},
BlockDB, NoteId, WalletDB,
BlockDB,
error::SqliteClientError,
NoteId,
wallet::init::{init_accounts_table, init_blocks_table, init_wallet_db}, wallet::put_received_transparent_utxo, WalletDB,
};
use zcash_primitives::{
block::BlockHash,
consensus::{BlockHeight, BranchId, Parameters},
legacy::TransparentAddress,
note_encryption::Memo,
transaction::{components::Amount, Transaction},
transaction::{
components::{Amount, OutPoint},
Transaction
},
zip32::ExtendedFullViewingKey,
};
#[cfg(feature = "mainnet")]
use zcash_primitives::consensus::{MainNetwork, MAIN_NETWORK};
use zcash_primitives::consensus::{MAIN_NETWORK, MainNetwork};
#[cfg(not(feature = "mainnet"))]
use zcash_primitives::consensus::{TestNetwork, TEST_NETWORK};
use zcash_primitives::consensus::{TEST_NETWORK, TestNetwork};
use zcash_proofs::prover::LocalTxProver;
use secp256k1::key::SecretKey;
use local_rpc_types::{TransactionDataList, TransparentTransaction, TransparentTransactionList};
use protobuf::{parse_from_bytes, Message};
use sha2::{Digest, Sha256};
use hdwallet::{ExtendedPrivKey, KeyIndex};
use secp256k1::{PublicKey, Secp256k1};
use crate::utils::exception::unwrap_exc_or;
use zcash_client_sqlite::wallet::get_unspent_transparent_utxos;
mod utils;
// /////////////////////////////////////////////////////////////////////////////////////////////////
// Temporary Imports
mod local_rpc_types;
// use crate::extended_key::{key_index::KeyIndex, ExtendedPrivKey, ExtendedPubKey, KeySeed};
// /////////////////////////////////////////////////////////////////////////////////////////////////
#[cfg(debug_assertions)]
fn print_debug_state() {
debug!("WARNING! Debugging enabled! This will likely slow things down 10X!");
@ -79,8 +79,8 @@ pub const NETWORK: MainNetwork = MAIN_NETWORK;
#[cfg(not(feature = "mainnet"))]
pub const NETWORK: TestNetwork = TEST_NETWORK;
fn wallet_db(env: &JNIEnv<'_>, db_data: JString<'_>) -> Result<WalletDB, failure::Error> {
WalletDB::for_path(utils::java_string_to_rust(&env, db_data))
fn wallet_db<P: Parameters>(env: &JNIEnv<'_>, params: P, db_data: JString<'_>) -> Result<WalletDB<P>, failure::Error> {
WalletDB::for_path(utils::java_string_to_rust(&env, db_data), params)
.map_err(|e| format_err!("Error opening wallet database connection: {}", e))
}
@ -114,8 +114,8 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_initDataDb(
) -> jboolean {
let res = panic::catch_unwind(|| {
let db_path = utils::java_string_to_rust(&env, db_data);
WalletDB::for_path(db_path)
.and_then(|db| init_data_database(&db))
WalletDB::for_path(db_path, NETWORK)
.and_then(|db| init_wallet_db(&db))
.map(|()| JNI_TRUE)
.map_err(|e| format_err!("Error while initializing data DB: {}", e))
});
@ -131,7 +131,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_initAccount
accounts: jint,
) -> jobjectArray {
let res = panic::catch_unwind(|| {
let db_data = wallet_db(&env, db_data)?;
let db_data = wallet_db(&env, NETWORK, db_data)?;
let seed = env.convert_byte_array(seed).unwrap();
let accounts = if accounts >= 0 {
accounts as u32
@ -144,7 +144,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_initAccount
.collect();
let extfvks: Vec<_> = extsks.iter().map(ExtendedFullViewingKey::from).collect();
init_accounts_table(&db_data, &NETWORK, &extfvks)
init_accounts_table(&db_data, &extfvks)
.map(|_| {
// Return the ExtendedSpendingKeys for the created accounts
utils::rust_vec_to_java(
@ -174,7 +174,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_initAccount
extfvks_arr: jobjectArray,
) -> jboolean {
let res = panic::catch_unwind(|| {
let db_data = wallet_db(&env, db_data)?;
let db_data = wallet_db(&env, NETWORK, db_data)?;
// TODO: avoid all this unwrapping and also surface errors, better
let count = env.get_array_length(extfvks_arr).unwrap();
let extfvks = (0..count)
@ -190,7 +190,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_initAccount
})
.collect::<Vec<_>>();
match init_accounts_table(&db_data, &NETWORK, &extfvks) {
match init_accounts_table(&db_data, &extfvks) {
Ok(()) => Ok(JNI_TRUE),
Err(e) => Err(format_err!("Error while initializing accounts: {}", e)),
}
@ -379,7 +379,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_initBlocksT
sapling_tree_string: JString<'_>,
) -> jboolean {
let res = panic::catch_unwind(|| {
let db_data = wallet_db(&env, db_data)?;
let db_data = wallet_db(&env, NETWORK, db_data)?;
let hash = {
let mut hash = hex::decode(utils::java_string_to_rust(&env, hash_string)).unwrap();
hash.reverse();
@ -403,17 +403,17 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_initBlocksT
}
#[no_mangle]
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_getAddress(
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_getShieldedAddress(
env: JNIEnv<'_>,
_: JClass<'_>,
db_data: JString<'_>,
account: jint,
) -> jstring {
let res = panic::catch_unwind(|| {
let db_data = wallet_db(&env, db_data)?;
let db_data = wallet_db(&env, NETWORK, db_data)?;
let account = AccountId(account.try_into()?);
match (&db_data).get_address(&NETWORK, account) {
match (&db_data).get_address(account) {
Ok(Some(addr)) => {
let addr_str = encode_payment_address(NETWORK.hrp_sapling_payment_address(), &addr);
let output = env
@ -480,17 +480,97 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_getBalance(
account: jint,
) -> jlong {
let res = panic::catch_unwind(|| {
let db_data = wallet_db(&env, db_data)?;
let db_data = wallet_db(&env, NETWORK, db_data)?;
let account = AccountId(account.try_into()?);
match (&db_data).get_balance(account) {
Ok(balance) => Ok(balance.into()),
Err(e) => Err(format_err!("Error while fetching balance: {}", e)),
}
(&db_data)
.get_target_and_anchor_heights()
.map_err(|e| format_err!("Error while fetching anchor height: {}", e))
.and_then(|opt_anchor| {
opt_anchor
.map(|(h, _)| h)
.ok_or(format_err!("height not available; scan required."))
})
.and_then(|anchor| {
(&db_data)
.get_balance_at(account, anchor)
.map_err(|e| format_err!("Error while fetching verified balance: {}", e))
})
.map(|amount| amount.into())
});
unwrap_exc_or(&env, res, -1)
}
#[no_mangle]
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_getVerifiedTransparentBalance(
env: JNIEnv<'_>,
_: JClass<'_>,
db_data: JString<'_>,
address: JString<'_>,
) -> jlong {
let res = panic::catch_unwind(|| {
let db_data = wallet_db(&env, NETWORK, db_data)?;
let addr = utils::java_string_to_rust(&env, address);
let taddr = TransparentAddress::decode(&NETWORK, &addr).unwrap();
let amount = (&db_data)
.get_target_and_anchor_heights()
.map_err(|e| format_err!("Error while fetching anchor height: {}", e))
.and_then(|opt_anchor| {
opt_anchor
.map(|(h, _)| h)
.ok_or(format_err!("height not available; scan required."))
})
.and_then(|anchor| {
(&db_data)
.get_unspent_transparent_utxos(&taddr, anchor - ANCHOR_OFFSET)
.map_err(|e| format_err!("Error while fetching verified balance: {}", e))
})?
.iter()
.map(|utxo| utxo.value)
.sum::<Amount>();
Ok(amount.into())
});
unwrap_exc_or(&env, res, -1)
}
#[no_mangle]
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_getTotalTransparentBalance(
env: JNIEnv<'_>,
_: JClass<'_>,
db_data: JString<'_>,
address: JString<'_>,
) -> jlong {
let res = panic::catch_unwind(|| {
let db_data = wallet_db(&env, NETWORK, db_data)?;
let addr = utils::java_string_to_rust(&env, address);
let taddr = TransparentAddress::decode(&NETWORK, &addr).unwrap();
let amount = (&db_data)
.get_target_and_anchor_heights()
.map_err(|e| format_err!("Error while fetching anchor height: {}", e))
.and_then(|opt_anchor| {
opt_anchor
.map(|(h, _)| h)
.ok_or(format_err!("height not available; scan required."))
})
.and_then(|anchor| {
(&db_data)
.get_unspent_transparent_utxos(&taddr, anchor)
.map_err(|e| format_err!("Error while fetching verified balance: {}", e))
})?
.iter()
.map(|utxo| utxo.value)
.sum::<Amount>();
Ok(amount.into())
});
unwrap_exc_or(&env, res, -1)
}
#[no_mangle]
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_getVerifiedBalance(
env: JNIEnv<'_>,
@ -499,7 +579,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_getVerified
account: jint,
) -> jlong {
let res = panic::catch_unwind(|| {
let db_data = wallet_db(&env, db_data)?;
let db_data = wallet_db(&env, NETWORK, db_data)?;
let account = AccountId(account.try_into()?);
(&db_data)
@ -512,7 +592,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_getVerified
})
.and_then(|anchor| {
(&db_data)
.get_verified_balance(account, anchor)
.get_balance_at(account, anchor)
.map_err(|e| format_err!("Error while fetching verified balance: {}", e))
})
.map(|amount| amount.into())
@ -529,9 +609,9 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_getReceived
id_note: jlong,
) -> jstring {
let res = panic::catch_unwind(|| {
let db_data = wallet_db(&env, db_data)?;
let db_data = wallet_db(&env, NETWORK, db_data)?;
let memo = match (&db_data).get_received_memo_as_utf8(NoteId(id_note)) {
let memo = match (&db_data).get_memo_as_utf8(NoteId::ReceivedNoteId(id_note)) {
Ok(memo) => memo.unwrap_or_default(),
Err(e) => return Err(format_err!("Error while fetching memo: {}", e)),
};
@ -550,10 +630,10 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_getSentMemo
id_note: jlong,
) -> jstring {
let res = panic::catch_unwind(|| {
let db_data = wallet_db(&env, db_data)?;
let db_data = wallet_db(&env, NETWORK, db_data)?;
let memo = (&db_data)
.get_sent_memo_as_utf8(NoteId(id_note))
.get_memo_as_utf8(NoteId::SentNoteId(id_note))
.map(|memo| memo.unwrap_or_default())
.map_err(|e| format_err!("Error while fetching memo: {}", e))?;
@ -573,7 +653,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_validateCom
) -> jint {
let res = panic::catch_unwind(|| {
let block_db = block_db(&env, db_cache)?;
let db_data = wallet_db(&env, db_data)?;
let db_data = wallet_db(&env, NETWORK, db_data)?;
let validate_from = (&db_data)
.get_max_height_hash()
@ -582,8 +662,8 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_validateCom
let val_res = validate_chain(&NETWORK, &block_db, validate_from);
if let Err(e) = val_res {
match e.0 {
Error::InvalidChain(upper_bound, _) => {
match e {
SqliteClientError::BackendError(Error::InvalidChain(upper_bound, _)) => {
let upper_bound_u32 = u32::from(upper_bound);
Ok(upper_bound_u32 as i32)
}
@ -606,14 +686,14 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_rewindToHei
height: jint,
) -> jboolean {
let res = panic::catch_unwind(|| {
let db_data = wallet_db(&env, db_data)?;
let db_data = wallet_db(&env, NETWORK, db_data)?;
let mut update_ops = (&db_data)
.get_update_ops()
.map_err(|e| format_err!("Could not obtain a writable database connection: {}", e))?;
let height = BlockHeight::try_from(height)?;
(&mut update_ops)
.transactionally(|ops| ops.rewind_to_height(&NETWORK, height))
.transactionally(|ops| ops.rewind_to_height(height))
.map(|_| JNI_TRUE)
.map_err(|e| format_err!("Error while rewinding data DB to height {}: {}", height, e))
});
@ -630,9 +710,10 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_scanBlocks(
) -> jboolean {
let res = panic::catch_unwind(|| {
let db_cache = block_db(&env, db_cache)?;
let db_data = wallet_db(&env, db_data)?;
let db_data = wallet_db(&env, NETWORK, db_data)?;
let mut db_data = db_data.get_update_ops()?;
match scan_cached_blocks(&NETWORK, &db_cache, &db_data, None) {
match scan_cached_blocks(&NETWORK, &db_cache, &mut db_data, None) {
Ok(()) => Ok(JNI_TRUE),
Err(e) => Err(format_err!("Error while scanning blocks: {}", e)),
}
@ -640,6 +721,48 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_scanBlocks(
unwrap_exc_or(&env, res, JNI_FALSE)
}
#[no_mangle]
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_putUtxo(
env: JNIEnv<'_>,
_: JClass<'_>,
db_data: JString<'_>,
address: JString<'_>,
txid_bytes: jbyteArray,
index: jint,
script: jbyteArray,
value: jlong,
height: jint,
) -> jboolean {
// debug!("For height {} found consensus branch {:?}", height, branch);
debug!("preparing to store UTXO in db_data");
let res = panic::catch_unwind(|| {
let txid_bytes = env.convert_byte_array(txid_bytes).unwrap();
let mut txid = [0u8; 32];
txid.copy_from_slice(&txid_bytes);
let script = env.convert_byte_array(script).unwrap();
let db_data = wallet_db(&env, NETWORK, db_data)?;
let mut db_data = db_data.get_update_ops()?;
let addr = utils::java_string_to_rust(&env, address);
let address = TransparentAddress::decode(&NETWORK, &addr).unwrap();
let output = WalletTransparentOutput {
address: address,
outpoint: OutPoint::new(txid, index as u32),
script: script,
value: Amount::from_i64(value).unwrap(),
height: BlockHeight::from(height as u32),
};
debug!("Storing UTXO in db_data");
match put_received_transparent_utxo(&mut db_data, &output) {
Ok(_) => Ok(JNI_TRUE),
Err(e) => Err(format_err!("Error while inserting UTXO: {}", e)),
}
});
unwrap_exc_or(&env, res, JNI_FALSE)
}
// ADDED BY ANDROID
#[no_mangle]
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_scanBlockBatch(
@ -651,9 +774,10 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_scanBlockBa
) -> jboolean {
let res = panic::catch_unwind(|| {
let db_cache = block_db(&env, db_cache)?;
let db_data = wallet_db(&env, db_data)?;
let db_data = wallet_db(&env, NETWORK, db_data)?;
let mut db_data = db_data.get_update_ops()?;
match scan_cached_blocks(&NETWORK, &db_cache, &db_data, Some(limit as u32)) {
match scan_cached_blocks(&NETWORK, &db_cache, &mut db_data, Some(limit as u32)) {
Ok(()) => Ok(JNI_TRUE),
Err(e) => Err(format_err!("Error while scanning blocks: {}", e)),
}
@ -661,53 +785,34 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_scanBlockBa
unwrap_exc_or(&env, res, JNI_FALSE)
}
// ////////////////////////////////////////////////////////////////////////////////////////////////
// PROOF-OF-CONCEPT FOR PROTOBUF COMMUNICATION WITH SDK
// ////////////////////////////////////////////////////////////////////////////////////////////////
#[no_mangle]
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_parseTransactionDataList(
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_tool_DerivationTool_deriveTransparentSecretKeyFromSeed(
env: JNIEnv<'_>,
_: JClass<'_>,
tx_data_list: jbyteArray,
) -> jbyteArray {
let err_val: Vec<u8> = Vec::new();
let res_err = env.byte_array_from_slice(&err_val).unwrap();
seed: jbyteArray,
account: jint,
index: jint,
) -> jstring {
let res = panic::catch_unwind(|| {
let tx_data_bytes = env.convert_byte_array(tx_data_list)?;
let input_tx_data = parse_from_bytes::<TransactionDataList>(&tx_data_bytes)?;
let mut tx_list = TransparentTransactionList::new();
let mut txs = protobuf::RepeatedField::<TransparentTransaction>::new();
for data in input_tx_data.data.iter() {
let mut tx = TransparentTransaction::new();
let parsed = Transaction::read(&data[..])?;
tx.set_expiryHeight(parsed.expiry_height.into());
// Note: the wrong value is returned here (negative numbers)
tx.set_value(i64::from(parsed.value_balance));
tx.set_hasShieldedSpends(parsed.shielded_spends.len() > 0);
tx.set_hasShieldedOutputs(parsed.shielded_outputs.len() > 0);
for (_n, vout) in parsed.vout.iter().enumerate() {
match vout.script_pubkey.address() {
// NOTE : this logic below doesn't work. No address is parsed.
Some(TransparentAddress::PublicKey(hash)) => {
tx.set_toAddress(
hash.to_base58check(&NETWORK.b58_pubkey_address_prefix(), &[]),
);
}
_ => {}
}
}
txs.push(tx);
}
tx_list.set_transactions(txs);
match env.byte_array_from_slice(&tx_list.write_to_bytes()?) {
Ok(result) => Ok(result),
Err(e) => Err(format_err!("Error while parsing transaction: {}", e)),
}
let seed = env.convert_byte_array(seed).unwrap();
let account = if account >= 0 {
account as u32
} else {
return Err(format_err!("account argument must be positive"));
};
let index = if index >= 0 {
index as u32
} else {
return Err(format_err!("index argument must be positive"));
};
let sk = derive_secret_key_from_seed(&NETWORK, &seed, AccountId(account), index).unwrap();
let sk_wif = Wif::from_secret_key(&sk, true);
let output = env
.new_string(sk_wif.0)
.expect("Couldn't create Java string for private key!");
Ok(output.into_inner())
});
unwrap_exc_or(&env, res, res_err)
unwrap_exc_or(&env, res, ptr::null_mut())
}
#[no_mangle]
@ -715,38 +820,50 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_tool_DerivationTool_deriveT
env: JNIEnv<'_>,
_: JClass<'_>,
seed: jbyteArray,
account: jint,
index: jint,
) -> jstring {
let res = panic::catch_unwind(|| {
let seed = env.convert_byte_array(seed).unwrap();
// modified from: https://github.com/adityapk00/zecwallet-light-cli/blob/master/lib/src/lightwallet.rs
let ext_t_key = ExtendedPrivKey::with_seed(&seed).unwrap();
let address_sk = ext_t_key
.derive_private_key(KeyIndex::hardened_from_normalize_index(44).unwrap())
.unwrap()
.derive_private_key(
KeyIndex::hardened_from_normalize_index(NETWORK.coin_type()).unwrap(),
)
.unwrap()
.derive_private_key(KeyIndex::hardened_from_normalize_index(0).unwrap())
.unwrap()
.derive_private_key(KeyIndex::Normal(0))
.unwrap()
.derive_private_key(KeyIndex::Normal(0))
.unwrap()
.private_key;
let secp = Secp256k1::new();
let pk = PublicKey::from_secret_key(&secp, &address_sk);
let mut hash160 = ripemd160::Ripemd160::new();
hash160.update(Sha256::digest(&pk.serialize()[..].to_vec()));
let address_string = hash160
.finalize()
.to_base58check(&NETWORK.b58_pubkey_address_prefix(), &[]);
let account = if account >= 0 {
account as u32
} else {
return Err(format_err!("account argument must be positive"));
};
let index = if index >= 0 {
index as u32
} else {
return Err(format_err!("index argument must be positive"));
};
let sk = derive_secret_key_from_seed(&NETWORK, &seed, AccountId(account), index);
let taddr = derive_transparent_address_from_secret_key(sk.unwrap())
.encode(&NETWORK);
let output = env
.new_string(address_string)
.new_string(taddr)
.expect("Couldn't create Java string for taddr!");
Ok(output.into_inner())
});
unwrap_exc_or(&env, res, ptr::null_mut())
}
#[no_mangle]
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_tool_DerivationTool_deriveTransparentAddressFromSecretKey(
env: JNIEnv<'_>,
_: JClass<'_>,
secret_key: JString<'_>,
) -> jstring {
let res = panic::catch_unwind(|| {
let tsk_wif = utils::java_string_to_rust(&env, secret_key);
let sk:SecretKey = (&Wif(tsk_wif)).try_into().expect("invalid private key WIF");
let taddr =
derive_transparent_address_from_secret_key(sk)
.encode(&NETWORK);
let output = env
.new_string(taddr)
.expect("Couldn't create Java string!");
Ok(output.into_inner())
});
unwrap_exc_or(&env, res, ptr::null_mut())
@ -760,11 +877,12 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_decryptAndS
tx: jbyteArray,
) -> jboolean {
let res = panic::catch_unwind(|| {
let db_data = wallet_db(&env, db_data)?;
let db_data = wallet_db(&env, NETWORK, db_data)?;
let mut db_data = db_data.get_update_ops()?;
let tx_bytes = env.convert_byte_array(tx).unwrap();
let tx = Transaction::read(&tx_bytes[..])?;
match decrypt_and_store_transaction(&NETWORK, &db_data, &tx) {
match decrypt_and_store_transaction(&NETWORK, &mut db_data, &tx) {
Ok(()) => Ok(JNI_TRUE),
Err(e) => Err(format_err!("Error while decrypting transaction: {}", e)),
}
@ -788,7 +906,8 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_createToAdd
output_params: JString<'_>,
) -> jlong {
let res = panic::catch_unwind(|| {
let db_data = wallet_db(&env, db_data)?;
let db_data = wallet_db(&env, NETWORK, db_data)?;
let mut db_data = db_data.get_update_ops()?;
let account = if account >= 0 {
account as u32
} else {
@ -830,7 +949,7 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_createToAdd
// let branch = if
create_spend_to_address(
&db_data,
&mut db_data,
&NETWORK,
prover,
AccountId(account),
@ -845,6 +964,64 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_createToAdd
unwrap_exc_or(&env, res, -1)
}
#[no_mangle]
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_shieldToAddress(
env: JNIEnv<'_>,
_: JClass<'_>,
db_data: JString<'_>,
account: jint,
extsk: JString<'_>,
tsk: JString<'_>,
memo: jbyteArray,
spend_params: JString<'_>,
output_params: JString<'_>,
) -> jlong {
let res = panic::catch_unwind(|| {
let db_data = wallet_db(&env, NETWORK, db_data)?;
let mut db_data = db_data.get_update_ops()?;
let account = if account == 0 {
account as u32
} else {
return Err(format_err!("account argument {} must be positive", account));
};
let extsk = utils::java_string_to_rust(&env, extsk);
let tsk_wif = utils::java_string_to_rust(&env, tsk);
let memo_bytes = env.convert_byte_array(memo).unwrap();
let spend_params = utils::java_string_to_rust(&env, spend_params);
let output_params = utils::java_string_to_rust(&env, output_params);
let extsk =
match decode_extended_spending_key(NETWORK.hrp_sapling_extended_spending_key(), &extsk)
{
Ok(Some(extsk)) => extsk,
Ok(None) => {
return Err(format_err!("ExtendedSpendingKey is for the wrong network"));
}
Err(e) => {
return Err(format_err!("Invalid ExtendedSpendingKey: {}", e));
}
};
let sk:SecretKey = (&Wif(tsk_wif)).try_into().expect("invalid private key WIF");
let memo = Memo::from_bytes(&memo_bytes).unwrap();
let prover = LocalTxProver::new(Path::new(&spend_params), Path::new(&output_params));
shield_funds(
&mut db_data,
&NETWORK,
prover,
AccountId(account),
&sk,
&extsk,
&memo,
10
)
.map_err(|e| format_err!("Error while shielding transaction: {}", e))
});
unwrap_exc_or(&env, res, -1)
}
#[no_mangle]
pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_branchIdForHeight(
env: JNIEnv<'_>,
@ -859,33 +1036,3 @@ pub unsafe extern "C" fn Java_cash_z_ecc_android_sdk_jni_RustBackend_branchIdFor
});
unwrap_exc_or(&env, res, -1)
}
//
// Helper code from: https://github.com/adityapk00/zecwallet-light-cli/blob/master/lib/src/lightwallet.rs
//
/// A trait for converting a [u8] to base58 encoded string.
pub trait ToBase58Check {
/// Converts a value of `self` to a base58 value, returning the owned string.
/// The version is a coin-specific prefix that is added.
/// The suffix is any bytes that we want to add at the end (like the "iscompressed" flag for
/// Secret key encoding)
fn to_base58check(&self, version: &[u8], suffix: &[u8]) -> String;
}
impl ToBase58Check for [u8] {
fn to_base58check(&self, version: &[u8], suffix: &[u8]) -> String {
let mut payload: Vec<u8> = Vec::new();
payload.extend_from_slice(version);
payload.extend_from_slice(self);
payload.extend_from_slice(suffix);
let mut checksum = double_sha256(&payload);
payload.append(&mut checksum[..4].to_vec());
payload.to_base58()
}
}
pub fn double_sha256(payload: &[u8]) -> Vec<u8> {
let h1 = Sha256::digest(&payload);
let h2 = Sha256::digest(&h1);
h2.to_vec()
}

View File

@ -1,4 +1,4 @@
// This file is generated by rust-protobuf 2.18.1. Do not edit
// This file is generated by rust-protobuf 2.22.0. Do not edit
// @generated
// https://github.com/rust-lang/rust-clippy/issues/702
@ -21,7 +21,7 @@
/// Generated files are compatible only with the same version
/// of protobuf runtime.
// const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_18_1;
// const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_22_0;
#[derive(PartialEq,Clone,Default)]
pub struct TransactionDataList {
@ -776,7 +776,7 @@ static file_descriptor_proto_data: &'static [u8] = b"\
static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;
fn parse_descriptor_proto() -> ::protobuf::descriptor::FileDescriptorProto {
::protobuf::parse_from_bytes(file_descriptor_proto_data).unwrap()
::protobuf::Message::parse_from_bytes(file_descriptor_proto_data).unwrap()
}
pub fn file_descriptor_proto() -> &'static ::protobuf::descriptor::FileDescriptorProto {