Merge pull request #216 from zcash/integration/auto-shielding-poc
Integration of all auto-shielding PoC changes
This commit is contained in:
commit
9dfc51967f
File diff suppressed because it is too large
Load Diff
37
Cargo.toml
37
Cargo.toml
|
@ -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]
|
||||
|
|
|
@ -214,8 +214,10 @@ cargo {
|
|||
defaultFeaturesAnd("mainnet")
|
||||
}
|
||||
zcashtestnetDebug {
|
||||
defaultFeaturesAnd("testnet")
|
||||
}
|
||||
zcashtestnetRelease {
|
||||
defaultFeaturesAnd("testnet")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
10
build.rs
10
build.rs
|
@ -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");
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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"}))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
||||
|
@ -649,4 +704,4 @@ fun Synchronizer(
|
|||
txManager,
|
||||
processor
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
//
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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:"
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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) {}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue