@chainberry/trust-wallet-core 2.0.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/{TrustWalletCoreModule.podspec → ChainberryTrustWalletCoreModule.podspec} +2 -2
  2. package/README.md +15 -22
  3. package/android/build.gradle +29 -6
  4. package/android/libs/README.md +34 -0
  5. package/android/libs/com/trustwallet/wallet-core/4.1.19/wallet-core-4.1.19.aar +0 -0
  6. package/android/libs/com/trustwallet/wallet-core/4.1.19/wallet-core-4.1.19.aar.md5 +1 -0
  7. package/android/libs/com/trustwallet/wallet-core/4.1.19/wallet-core-4.1.19.aar.sha1 +1 -0
  8. package/android/libs/com/trustwallet/wallet-core/4.1.19/wallet-core-4.1.19.pom +22 -0
  9. package/android/libs/com/trustwallet/wallet-core/4.1.19/wallet-core-4.1.19.pom.md5 +1 -0
  10. package/android/libs/com/trustwallet/wallet-core/4.1.19/wallet-core-4.1.19.pom.sha1 +1 -0
  11. package/android/libs/com/trustwallet/wallet-core-proto/4.1.19/wallet-core-proto-4.1.19.jar +0 -0
  12. package/android/libs/com/trustwallet/wallet-core-proto/4.1.19/wallet-core-proto-4.1.19.jar.md5 +1 -0
  13. package/android/libs/com/trustwallet/wallet-core-proto/4.1.19/wallet-core-proto-4.1.19.jar.sha1 +1 -0
  14. package/android/libs/com/trustwallet/wallet-core-proto/4.1.19/wallet-core-proto-4.1.19.pom +21 -0
  15. package/android/libs/com/trustwallet/wallet-core-proto/4.1.19/wallet-core-proto-4.1.19.pom.md5 +1 -0
  16. package/android/libs/com/trustwallet/wallet-core-proto/4.1.19/wallet-core-proto-4.1.19.pom.sha1 +1 -0
  17. package/android/libs/download.sh +52 -0
  18. package/android/src/androidTest/java/com/chainberry/trustwalletcore/AddressDerivationConformanceTest.kt +106 -0
  19. package/android/src/androidTest/java/com/chainberry/trustwalletcore/SigningConformanceTest.kt +186 -0
  20. package/android/src/main/java/com/chainberry/trustwalletcore/AmountParsing.kt +45 -0
  21. package/android/src/main/java/com/chainberry/trustwalletcore/Bech32.kt +68 -0
  22. package/android/src/main/java/com/chainberry/trustwalletcore/ChainSigning.kt +526 -0
  23. package/android/src/main/java/com/chainberry/trustwalletcore/ChainberryTrustWalletCoreModule.kt +147 -0
  24. package/android/src/main/java/com/chainberry/trustwalletcore/NativeWalletStore.kt +796 -0
  25. package/android/src/test/java/com/chainberry/trustwalletcore/AmountParsingConformanceTest.kt +57 -0
  26. package/android/src/test/java/com/chainberry/trustwalletcore/Bech32Test.kt +35 -0
  27. package/android/src/test/java/com/chainberry/trustwalletcore/NativeWalletStoreTest.kt +274 -0
  28. package/expo-module.config.json +3 -2
  29. package/ios/AmountParsing.swift +62 -0
  30. package/ios/Bech32.swift +66 -0
  31. package/ios/ChainSigning.swift +602 -0
  32. package/ios/ChainberryTrustWalletCoreModule.swift +231 -0
  33. package/ios/NativeWalletStore.swift +232 -0
  34. package/package.json +4 -3
  35. package/src/index.ts +27 -12
  36. package/android/src/main/java/expo/modules/trustwalletcore/ChainSigning.kt +0 -299
  37. package/android/src/main/java/expo/modules/trustwalletcore/NativeWalletStore.kt +0 -182
  38. package/android/src/main/java/expo/modules/trustwalletcore/TrustWalletCoreModule.kt +0 -91
  39. package/ios/TrustWalletCoreModule.swift +0 -107
@@ -3,7 +3,7 @@ require 'json'
3
3
  package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
4
4
 
5
5
  Pod::Spec.new do |s|
6
- s.name = 'TrustWalletCoreModule'
6
+ s.name = 'ChainberryTrustWalletCoreModule'
7
7
  s.version = package['version']
8
8
  s.summary = package['description']
9
9
  s.description = s.summary
@@ -17,5 +17,5 @@ Pod::Spec.new do |s|
17
17
  s.source_files = 'ios/**/*.{h,m,mm,swift}'
18
18
 
19
19
  s.dependency 'ExpoModulesCore'
20
- s.dependency 'TrustWalletCore'
20
+ s.dependency 'TrustWalletCore', '4.1.19'
21
21
  end
package/README.md CHANGED
@@ -12,26 +12,11 @@ npx expo install @chainberry/trust-wallet-core
12
12
 
13
13
  This is an Expo config plugin module with native Android/iOS code, so it requires a development build (`expo prebuild` / EAS Build) — it will not work in Expo Go.
14
14
 
15
- ### Android: GitHub Packages authentication required
15
+ ### Android: no GitHub credentials needed
16
16
 
17
- Trust Wallet Core's Android artifact is published to GitHub Packages, which requires authentication even though the package itself is public. Without credentials, `./gradlew` will fail to resolve `com.trustwallet:wallet-core`.
17
+ Trust Wallet Core's Android artifact is only published to GitHub Packages, which requires authentication even though the package itself is public. To avoid making that a requirement for every dev/CI machine, `com.trustwallet:wallet-core` and its `wallet-core-proto` dependency are vendored locally as a plain Maven repo in `android/libs/` — `android/build.gradle` resolves from there first, so a normal `./gradlew` build needs no credentials at all. See `android/libs/README.md` for how it's kept up to date.
18
18
 
19
- Set up **one** of:
20
-
21
- ```sh
22
- # Env vars (CI / one-off terminal)
23
- export GITHUB_ACTOR=your-github-username
24
- export GITHUB_TOKEN=ghp_xxxxxxxxxxxx # classic PAT, read:packages scope only
25
- ```
26
-
27
- or add to `~/.gradle/gradle.properties` (permanent local dev setup):
28
-
29
- ```properties
30
- gpr.user=your-github-username
31
- gpr.key=ghp_xxxxxxxxxxxx
32
- ```
33
-
34
- Generate a token at github.com/settings/tokens → "classic" → check `read:packages`.
19
+ GitHub Packages auth is still needed (via `GITHUB_ACTOR`/`GITHUB_TOKEN` env vars or `gpr.user`/`gpr.key` in `~/.gradle/gradle.properties`) only when bumping the pinned wallet-core version — see `android/libs/download.sh`.
35
20
 
36
21
  ### iOS
37
22
 
@@ -58,8 +43,8 @@ const mnemonic = await exportMnemonic(walletId);
58
43
 
59
44
  ## API
60
45
 
61
- - `createWallet(strength = 128, passphrase = "")` — generates a new BIP-39 mnemonic and persists it natively (Keychain on iOS / Keystore-backed file on Android, biometry-or-passcode gated). Returns `{ walletId, addresses }` — the mnemonic itself never leaves native code.
62
- - `importWallet(mnemonic, passphrase = "")` — validates and persists an existing mnemonic the same way. The `mnemonic` argument is a one-time exposure from the caller (e.g. a text-entry backup-restore screen); discard your own copy immediately after this call resolves.
46
+ - `createWallet(strength = 128)` — generates a new BIP-39 mnemonic and persists it natively (Keychain on iOS / Keystore-backed file on Android, biometry-or-passcode gated). Returns `{ walletId, addresses }` — the mnemonic itself never leaves native code. No BIP-39 passphrase support: `signTransaction` always reconstructs the wallet with an empty passphrase, so a caller-supplied one would derive addresses from a seed different from the one actually used to sign.
47
+ - `importWallet(mnemonic)` — validates and persists an existing mnemonic the same way. The `mnemonic` argument is a one-time exposure from the caller (e.g. a text-entry backup-restore screen); discard your own copy immediately after this call resolves.
63
48
  - `listWallets()` — returns `{ walletId, addresses }[]` for every persisted wallet, reading only the ungated metadata store. No biometric prompt.
64
49
  - `deleteWallet(walletId)` — removes the wallet's native key material and metadata entry. Irreversible; not biometric-gated (deleting reveals nothing, so this is a UX confirmation concern, not a key-secrecy one).
65
50
  - `signTransaction(walletId, chain, unsignedTx)` — triggers a native biometry/passcode prompt, then derives the key and signs entirely inside native code. Returns `{ signedTx, meta? }`; `meta` currently only carries TON's `txHash`.
@@ -69,11 +54,19 @@ const mnemonic = await exportMnemonic(walletId);
69
54
 
70
55
  Mnemonic and derived private keys are generated, persisted, and used for signing entirely inside this module's native code (Swift/Kotlin) — they are never serialized back across the JS bridge, with the single exception of `exportMnemonic`'s explicit backup flow. This is a deliberate change from this module's earlier version, which returned raw private keys to JS on every address derivation; that let a compromised/malicious JS dependency, an attached JS debugger, or a JS-heap memory dump read wallet secrets in full. Now the JS runtime never holds them at all.
71
56
 
72
- Storage: one biometry-or-passcode-gated secret per wallet (`SecAccessControl` + iOS Keychain; a hardware-backed Android Keystore AES key gating an encrypted on-disk file), plus a separate, ungated metadata entry (`walletId` → addresses) for read-only UI that shouldn't need a biometric prompt just to show an address or balance.
57
+ Storage: one biometry-or-passcode-gated secret per wallet (`SecAccessControl` + iOS Keychain; an Android Keystore AES key gating an encrypted on-disk file), plus a separate, ungated metadata entry (`walletId` → addresses) for read-only UI that shouldn't need a biometric prompt just to show an address or balance.
58
+
59
+ **Android hardware backing is best-effort, not guaranteed.** Key creation requests StrongBox first (API 28+), falling back to a plain Keystore key (which Keymaster backs with a TEE on virtually all real devices, but which can be software-only on an emulator or a device with no secure hardware at all) on `StrongBoxUnavailableException` or below API 28. The actual security level achieved is verified via `KeyInfo` right after creation and logged (`NativeWalletStore` logcat tag) — a software-only key is tolerated rather than blocking wallet creation, so this is a best-effort hardening measure, not an enforced guarantee. Don't assume every wallet on every device is hardware-backed; check the logs on a specific device/build if that matters for your threat model.
73
60
 
74
61
  Bitcoin Cash is derivation-only: sending BCH is unsupported both here and upstream (`chainberry-wallet`'s self-custody transaction preparation has no BCH case), so this module only derives its address.
75
62
 
76
- **Not yet verified against a real device/build**: the per-chain `SigningInput` field mappings for Bitcoin, Litecoin, Tron, XRP, and TON, and Solana's raw-transaction signing mode, were written against the general Trust Wallet Core API shape but haven't been compiled or run against this module's pinned `wallet-core` version. Confirm field names and produce byte-for-byte signed-output parity against the previous JS-based signers (or a testnet broadcast) before trusting any of these chains with real funds. Ethereum/BNB/Polygon signing is a straightforward reshape of what this module already proved out.
63
+ Both platforms pin **Trust Wallet Core 4.1.19** (`com.trustwallet:wallet-core:4.1.19` in `android/build.gradle`; `s.dependency 'TrustWalletCore', '4.1.19'` in the podspec).
64
+
65
+ **Address derivation is verified for all 10 chains** in `conformance/address-derivation-vectors.json`, asserted by the Android instrumented test (`src/androidTest/.../AddressDerivationConformanceTest.kt`; run via `./gradlew connectedDebugAndroidTest`). Methodology: real on-device 4.1.19 addresses were harvested from the instrumented test running on an emulator, cross-checked against an independent derivation via the WASM build (`@trustwallet/wallet-core` 3.3.3, run standalone in Node — a different upstream release than the pinned 4.1.19, used only as a second data point); on-device 4.1.19 is authoritative wherever the two disagree, since that's what the app actually ships. They agreed on 6 of 7 previously-pending chains exactly; `ton` disagreed only in address-flag encoding (bounceable vs. non-bounceable — same underlying key/hash, see that fixture entry's `_note`). This same run also caught a real bug: the Ethereum/BNB/Polygon address that had been marked `"verified"` since before this pass was actually wrong — the instrumented test that should have caught it had never successfully executed (two pre-existing bugs: `coin.name()` didn't compile against this Kotlin binding, and no `testInstrumentationRunner` was configured, so `connectedAndroidTest` silently ran "0 tests" instead of failing). Both are fixed now; see the test file's header comment for details.
66
+
67
+ **Byte-for-byte signing output is verified for 8 of 9 signable chains** in `conformance/signing-vectors.json`, asserted by `src/androidTest/.../SigningConformanceTest.kt`. Each vector calls `ChainSigner.sign()` — the same call path production code uses — with a fixed, deterministic (not necessarily broadcast-valid) unsigned tx. `ton` is verified but *not* byte-exact-asserted: `signTon()` embeds a wall-clock `expireAt` into the signed payload, so its output legitimately differs every run — the test instead checks the output is a well-formed signed BOC. `bitcoincash` has no signing vector (sending is unsupported, see above).
68
+
69
+ **iOS gap — not closed by this pass.** No Swift/Xcode toolchain was available in the environment that did this verification, so none of the above has been independently confirmed on iOS. `ChainKey.coinType` maps identically to Android's, so derivation *should* match, but this hasn't been checked on-device. `ConformanceTests/` currently has no address-derivation or signing test target at all (only the pre-existing amount-parsing one) — before this finding is fully closed for both platforms, an iOS engineer needs to (1) add `AddressDerivationConformanceTests`/`SigningConformanceTests` targets mirroring the Android ones, (2) run them against these same fixtures on a Mac, and (3) specifically resolve the **Tron divergence**: Android's `signTron` signs just the `txId` digest and reassembles JSON in app code, while iOS's hands wallet-core the full `rawJson` and returns its own reconstruction — `conformance/signing-vectors.json`'s `tron` entry spells out exactly what to check (the embedded signature hex must match Android's byte-for-byte; a mismatch there is a real bug, not a formatting difference).
77
70
 
78
71
  ## License
79
72
 
@@ -12,12 +12,16 @@ apply from: new File(
12
12
  applyKotlinExpoModulesCorePlugin()
13
13
 
14
14
  android {
15
- namespace 'expo.modules.trustwalletcore'
16
- compileSdkVersion 35
15
+ namespace 'com.chainberry.trustwalletcore'
16
+ compileSdkVersion 36
17
17
 
18
18
  defaultConfig {
19
19
  minSdkVersion 24
20
- targetSdkVersion 35
20
+ targetSdkVersion 36
21
+ // Without this, AGP defaults to the legacy android.test.InstrumentationTestRunner, which
22
+ // cannot discover @RunWith(AndroidJUnit4::class)-annotated tests at all (connectedAndroidTest
23
+ // silently reports "0 tests"/"No tests found" instead of running AddressDerivationConformanceTest).
24
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
21
25
  }
22
26
 
23
27
  compileOptions {
@@ -30,9 +34,14 @@ android {
30
34
  }
31
35
  }
32
36
 
33
- // ─── wallet-core credentials ──────────────────────────────────────────────────
34
- // GitHub Packages requires auth even for public packages.
35
- // Priority order (first non-empty wins):
37
+ // ─── wallet-core artifact ──────────────────────────────────────────────────
38
+ // com.trustwallet:wallet-core and its wallet-core-proto dependency are vendored
39
+ // in libs/ as a plain local Maven repo, so a normal build needs NO GitHub
40
+ // credentials at all — see libs/README.md for how it got there and how to
41
+ // re-vendor when bumping the pinned version below.
42
+ //
43
+ // The GitHub Packages repo is kept as a fallback (e.g. resolving a version not
44
+ // yet vendored) and needs auth even for this public package:
36
45
  // 1. Env vars GITHUB_ACTOR / GITHUB_TOKEN (CI / one-off terminal)
37
46
  // 2. Gradle props gpr.user / gpr.key in (permanent dev setup)
38
47
  // ~/.gradle/gradle.properties
@@ -49,6 +58,8 @@ def githubToken = System.getenv("GITHUB_TOKEN")
49
58
  ?: (project.hasProperty("gpr.key") ? project.property("gpr.key") : "")
50
59
 
51
60
  repositories {
61
+ maven { url = uri("$projectDir/libs") }
62
+
52
63
  maven {
53
64
  url = uri("https://maven.pkg.github.com/trustwallet/wallet-core")
54
65
  credentials {
@@ -66,4 +77,16 @@ dependencies {
66
77
  implementation 'com.google.protobuf:protobuf-javalite:3.21.9'
67
78
  // BiometricPrompt (biometry-or-device-credential gating on the Keystore-backed wallet cipher)
68
79
  implementation 'androidx.biometric:biometric:1.1.0'
80
+
81
+ // JVM unit tests (src/test) — AmountParsingConformanceTest reads the shared JSON fixture
82
+ // in ../conformance/. Use the pure-Java org.json artifact, not android.jar's stubbed
83
+ // org.json (which throws "Stub!" outside Robolectric/instrumented tests).
84
+ testImplementation 'junit:junit:4.13.2'
85
+ testImplementation 'org.json:json:20240303'
86
+
87
+ // Instrumented tests (src/androidTest) — AddressDerivationConformanceTest needs the
88
+ // TrustWalletCore JNI library, which can only load on a real device or emulator.
89
+ // Run via: ./gradlew connectedDebugAndroidTest
90
+ androidTestImplementation 'androidx.test.ext:junit:1.1.5'
91
+ androidTestImplementation 'androidx.test:runner:1.5.2'
69
92
  }
@@ -0,0 +1,34 @@
1
+ # Vendored wallet-core (local Maven repo)
2
+
3
+ `com.trustwallet:wallet-core` (and its `wallet-core-proto` dependency) are only
4
+ published to GitHub Packages, which requires authentication even to download a
5
+ public package. To spare every dev/CI machine from needing a
6
+ `read:packages` PAT, the resolved `.aar`/`.jar` + `.pom` files are vendored
7
+ here in a plain Maven-layout directory:
8
+
9
+ ```
10
+ libs/com/trustwallet/wallet-core/4.1.19/wallet-core-4.1.19.{aar,pom}
11
+ libs/com/trustwallet/wallet-core-proto/4.1.19/wallet-core-proto-4.1.19.{jar,pom}
12
+ ```
13
+
14
+ `../build.gradle` points its `repositories {}` block at this directory
15
+ *before* the GitHub Packages fallback, so a normal build resolves entirely
16
+ from here — no credentials needed.
17
+
18
+ Files were downloaded from `maven.pkg.github.com/trustwallet/wallet-core` on
19
+ 2026-09-04 and their sha1 checksums verified against GitHub's own
20
+ `.sha1` files (still alongside each artifact here).
21
+
22
+ ## Bumping the pinned version
23
+
24
+ 1. `./download.sh <new-version>` (needs `GITHUB_ACTOR`/`GITHUB_TOKEN` env vars,
25
+ or `gpr.user`/`gpr.key` in `~/.gradle/gradle.properties` — same credentials
26
+ the GitHub Packages fallback in `build.gradle` already uses).
27
+ 2. Update the pinned version in `../build.gradle`
28
+ (`implementation 'com.trustwallet:wallet-core:...'`) and in
29
+ `../../ChainberryTrustWalletCoreModule.podspec` (`s.dependency 'TrustWalletCore', '...'`)
30
+ to keep both platforms on the same wallet-core release.
31
+ 3. Delete the old version's directories under `com/trustwallet/wallet-core*/`
32
+ here (or leave them if you want to keep an easy rollback).
33
+ 4. Re-run the conformance suite (`conformance/`) — a wallet-core bump can
34
+ change derivation/signing output.
@@ -0,0 +1 @@
1
+ 3f07349ecbe013f2098684cc70cef49e
@@ -0,0 +1 @@
1
+ 58c6ee87c3ab4fb90c6ef4e9ba4f78260740ca16
@@ -0,0 +1,22 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
3
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
4
+ <!-- This module was also published with a richer model, Gradle metadata, -->
5
+ <!-- which should be used instead. Do not delete the following line which -->
6
+ <!-- is to indicate to Gradle or any Gradle module metadata file consumer -->
7
+ <!-- that they should prefer consuming it instead. -->
8
+ <!-- do_not_remove: published-with-gradle-metadata -->
9
+ <modelVersion>4.0.0</modelVersion>
10
+ <groupId>com.trustwallet</groupId>
11
+ <artifactId>wallet-core</artifactId>
12
+ <version>4.1.19</version>
13
+ <packaging>aar</packaging>
14
+ <dependencies>
15
+ <dependency>
16
+ <groupId>com.trustwallet</groupId>
17
+ <artifactId>wallet-core-proto</artifactId>
18
+ <version>4.1.19</version>
19
+ <scope>compile</scope>
20
+ </dependency>
21
+ </dependencies>
22
+ </project>
@@ -0,0 +1 @@
1
+ d4a399c80d4f896ae4d4ea6013f1f15f
@@ -0,0 +1 @@
1
+ a00894bc91fbbbf496fd3792484082334363e0fe
@@ -0,0 +1 @@
1
+ 1a68899feeb645d9895264d4c58e046dbcb3d528
@@ -0,0 +1,21 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
3
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
4
+ <!-- This module was also published with a richer model, Gradle metadata, -->
5
+ <!-- which should be used instead. Do not delete the following line which -->
6
+ <!-- is to indicate to Gradle or any Gradle module metadata file consumer -->
7
+ <!-- that they should prefer consuming it instead. -->
8
+ <!-- do_not_remove: published-with-gradle-metadata -->
9
+ <modelVersion>4.0.0</modelVersion>
10
+ <groupId>com.trustwallet</groupId>
11
+ <artifactId>wallet-core-proto</artifactId>
12
+ <version>4.1.19</version>
13
+ <dependencies>
14
+ <dependency>
15
+ <groupId>com.google.protobuf</groupId>
16
+ <artifactId>protobuf-javalite</artifactId>
17
+ <version>3.22.3</version>
18
+ <scope>compile</scope>
19
+ </dependency>
20
+ </dependencies>
21
+ </project>
@@ -0,0 +1 @@
1
+ 90383b24ce13303b7c7a435ba815209115fa7d06
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env bash
2
+ # Re-vendors com.trustwallet:wallet-core (+ its wallet-core-proto dependency)
3
+ # from GitHub Packages into this directory, laid out as a plain local Maven
4
+ # repo (com/<group>/<artifact>/<version>/...). build.gradle points at this
5
+ # directory first, so a normal build needs no GitHub credentials — only
6
+ # re-running this script (to bump the pinned version, or the first time
7
+ # someone sets this up) needs a token.
8
+ #
9
+ # Usage:
10
+ # GITHUB_ACTOR=you GITHUB_TOKEN=ghp_xxx ./download.sh 4.1.19
11
+ # or rely on gpr.user / gpr.key in ~/.gradle/gradle.properties (same
12
+ # credentials used by build.gradle's GitHub Packages fallback).
13
+ set -euo pipefail
14
+
15
+ VERSION="${1:?Usage: download.sh <wallet-core version, e.g. 4.1.19>}"
16
+ LIBS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
17
+
18
+ GITHUB_USER="${GITHUB_ACTOR:-}"
19
+ GITHUB_PASS="${GITHUB_TOKEN:-}"
20
+ if [ -z "$GITHUB_USER" ] || [ -z "$GITHUB_PASS" ]; then
21
+ GRADLE_PROPS="$HOME/.gradle/gradle.properties"
22
+ if [ -f "$GRADLE_PROPS" ]; then
23
+ GITHUB_USER="${GITHUB_USER:-$(grep '^gpr.user=' "$GRADLE_PROPS" | cut -d= -f2-)}"
24
+ GITHUB_PASS="${GITHUB_PASS:-$(grep '^gpr.key=' "$GRADLE_PROPS" | cut -d= -f2-)}"
25
+ fi
26
+ fi
27
+ if [ -z "$GITHUB_USER" ] || [ -z "$GITHUB_PASS" ]; then
28
+ echo "error: no GitHub credentials found (env vars or ~/.gradle/gradle.properties gpr.user/gpr.key)" >&2
29
+ exit 1
30
+ fi
31
+
32
+ REGISTRY="https://maven.pkg.github.com/trustwallet/wallet-core"
33
+
34
+ # module:packaging pairs — wallet-core is an .aar, wallet-core-proto is a plain .jar
35
+ MODULES=("wallet-core:aar" "wallet-core-proto:jar")
36
+
37
+ for entry in "${MODULES[@]}"; do
38
+ artifact="${entry%%:*}"
39
+ ext="${entry##*:}"
40
+ dest="$LIBS_DIR/com/trustwallet/$artifact/$VERSION"
41
+ mkdir -p "$dest"
42
+ base="$REGISTRY/com/trustwallet/$artifact/$VERSION"
43
+ for file in "$artifact-$VERSION.pom" "$artifact-$VERSION.$ext" "$artifact-$VERSION.pom.sha1" "$artifact-$VERSION.$ext.sha1"; do
44
+ echo "fetching $file"
45
+ curl -sS -L -f -u "$GITHUB_USER:$GITHUB_PASS" -o "$dest/$file" "$base/$file"
46
+ done
47
+ # verify what we just wrote against GitHub's own sha1
48
+ ( cd "$dest" && sha1sum -c "$artifact-$VERSION.pom.sha1" "$artifact-$VERSION.$ext.sha1" )
49
+ done
50
+
51
+ echo "done — vendored wallet-core $VERSION into $LIBS_DIR"
52
+ echo "remember to bump the version in ../build.gradle (dependencies block) and the podspec, and delete the old version's directories above if this is a version bump rather than a first-time vendor."
@@ -0,0 +1,106 @@
1
+ package com.chainberry.trustwalletcore
2
+
3
+ import androidx.test.ext.junit.runners.AndroidJUnit4
4
+ import org.junit.Assert.assertEquals
5
+ import org.junit.Assert.assertNotEquals
6
+ import org.junit.Test
7
+ import org.junit.runner.RunWith
8
+ import wallet.core.jni.CoinType
9
+ import wallet.core.jni.HDWallet
10
+
11
+ // Instrumented test — must run on a device or emulator (needs the TrustWalletCore JNI library).
12
+ // Run via: ./gradlew connectedDebugAndroidTest
13
+ //
14
+ // Verifies that HDWallet.getAddressForCoin produces the expected address for every chain
15
+ // under WalletCore 4.1.19 (the pinned version). The test mnemonic is the BIP39 standard
16
+ // test vector — never use it with real funds.
17
+ //
18
+ // All 10 chains were verified on-device (real emulator, WalletCore 4.1.19 JNI) on 2026-08-27,
19
+ // cross-checked against an independent derivation via the WASM build (@trustwallet/wallet-core
20
+ // 3.3.3, run in plain Node — see conformance/address-derivation-vectors.json's _note). On-device
21
+ // 4.1.19 is authoritative wherever the two disagree.
22
+ //
23
+ // IMPORTANT — this test did not actually run before this pass. Two pre-existing bugs prevented
24
+ // it from ever executing: (1) `coin.name()` doesn't compile against this Kotlin CoinType binding
25
+ // (name is a property, not a method — fixed here), and (2) no `testInstrumentationRunner` was
26
+ // configured in build.gradle, so AGP defaulted to the legacy runner, which cannot discover
27
+ // @RunWith(AndroidJUnit4::class) tests at all ("0 tests"/"No tests found", not a real pass).
28
+ // Once actually run, it immediately caught a real bug: the ETHEREUM/SMARTCHAIN "verified" address
29
+ // below was wrong (0x9d8A62f6...) — never actually checked against real WalletCore output despite
30
+ // being marked "verified". The corrected value below is the real on-device 4.1.19 result.
31
+ //
32
+ // For any NEW chain added to ChainKey in the future: add its CoinType to PENDING, run this test,
33
+ // read the printed address in logcat, verify it independently (e.g. against a TWC 4.1.19 REPL or
34
+ // the iOS companion test — see ConformanceTests/), then move it into VERIFIED.
35
+ //
36
+ // Human-readable source of truth: conformance/address-derivation-vectors.json.
37
+ @RunWith(AndroidJUnit4::class)
38
+ class AddressDerivationConformanceTest {
39
+
40
+ companion object {
41
+ init { System.loadLibrary("TrustWalletCore") }
42
+
43
+ const val MNEMONIC = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
44
+
45
+ // Chains with expected addresses confirmed against WalletCore 4.1.19, on a real device/emulator.
46
+ // ETH / BNB / POL all share CoinType.ETHEREUM (same secp256k1 key, BIP44 m/44'/60'/0'/0/0).
47
+ val VERIFIED = mapOf(
48
+ CoinType.ETHEREUM to "0x9858EfFD232B4033E47d90003D41EC34EcaEda94", // ethereum / bnb / polygon
49
+ CoinType.SMARTCHAIN to "0x9858EfFD232B4033E47d90003D41EC34EcaEda94", // bnb (same key as ethereum)
50
+ CoinType.BITCOIN to "bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu",
51
+ CoinType.LITECOIN to "ltc1qjmxnz78nmc8nq77wuxh25n2es7rzm5c2rkk4wh",
52
+ CoinType.XRP to "rHsMGQEkVNJmpGWs8XUBoTBiAAbwxZN5v3",
53
+ CoinType.TRON to "TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH",
54
+ // Non-bounceable (UQ...) — WalletCore 4.1.19's default for this CoinType. The WASM build
55
+ // (@trustwallet/wallet-core 3.3.3) derives the same underlying hash but defaults to the
56
+ // bounceable (EQ...) encoding instead — a version/binding difference in address-flag
57
+ // defaults, not a key-derivation discrepancy. On-device 4.1.19 (what the app actually ships)
58
+ // is authoritative here.
59
+ CoinType.TON to "UQAzWZa6nM5mJev91wGc7VCSfBoIsYRqKJpV78N8Add9-RKY",
60
+ CoinType.SOLANA to "GjJyeC1r2RgkuoCWMyPYkCWSGSGLcz266EaAkLA27AhL",
61
+ CoinType.BITCOINCASH to "bitcoincash:qqyx49mu0kkn9ftfj6hje6g2wfer34yfnq5tahq3q6",
62
+ )
63
+
64
+ // Steady state is empty — add a chain's CoinType here (and drop it from VERIFIED) only while
65
+ // actively harvesting a newly-added chain's address for the first time.
66
+ val PENDING = emptyList<CoinType>()
67
+ }
68
+
69
+ @Test
70
+ fun verifiedAddressesMatch() {
71
+ val wallet = HDWallet(MNEMONIC, "")
72
+ for ((coin, expected) in VERIFIED) {
73
+ val actual = wallet.getAddressForCoin(coin)
74
+ assertEquals("address mismatch for coin=${coin.name}", expected, actual)
75
+ }
76
+ }
77
+
78
+ // Regression test for a real bug: ChainSigner.addressForChain originally derived Litecoin's
79
+ // and Bitcoin Cash's hand-rolled testnet address from the identical path m/44'/1'/0'/0/0.
80
+ // BIP32 derivation only depends on (seed, path, curve) — CoinType alone doesn't perturb it
81
+ // when the literal path and curve (secp256k1 for both) are the same — so LTC and BCH silently
82
+ // derived the *same* key, and therefore the same address. Fixed by using each coin's own
83
+ // SLIP-44 index as the account (3rd) path component (see ChainSigner.utxoTestnetPath).
84
+ @Test
85
+ fun testnetAddressesAreDistinctPerChain() {
86
+ val wallet = HDWallet(MNEMONIC, "")
87
+ val btc = ChainSigner.addressForChain(wallet, ChainKey.BITCOIN, isTestnet = true)
88
+ val ltc = ChainSigner.addressForChain(wallet, ChainKey.LITECOIN, isTestnet = true)
89
+ val bch = ChainSigner.addressForChain(wallet, ChainKey.BITCOINCASH, isTestnet = true)
90
+ println("[AddressDerivationConformanceTest] testnet: BTC=$btc LTC=$ltc BCH=$bch")
91
+ assertNotEquals("BTC/LTC testnet addresses must not collide", btc, ltc)
92
+ assertNotEquals("BTC/BCH testnet addresses must not collide", btc, bch)
93
+ assertNotEquals("LTC/BCH testnet addresses must not collide", ltc, bch)
94
+ }
95
+
96
+ @Test
97
+ fun printPendingAddressesForVerification() {
98
+ val wallet = HDWallet(MNEMONIC, "")
99
+ val lines = PENDING.map { coin -> " ${coin.name} -> ${wallet.getAddressForCoin(coin)}" }
100
+ println(
101
+ "\n[AddressDerivationConformanceTest] Pending — verify and move to VERIFIED map:\n" +
102
+ lines.joinToString("\n")
103
+ )
104
+ // This test intentionally never fails — it exists only to harvest pending addresses.
105
+ }
106
+ }
@@ -0,0 +1,186 @@
1
+ package com.chainberry.trustwalletcore
2
+
3
+ import androidx.test.ext.junit.runners.AndroidJUnit4
4
+ import org.junit.Assert.assertEquals
5
+ import org.junit.Test
6
+ import org.junit.runner.RunWith
7
+ import wallet.core.jni.BitcoinScript
8
+ import wallet.core.jni.CoinType
9
+ import wallet.core.jni.HDWallet
10
+
11
+ // Instrumented test — must run on a device or emulator (needs the TrustWalletCore JNI library).
12
+ // Run via: ./gradlew connectedDebugAndroidTest
13
+ //
14
+ // Verifies that ChainSigner.sign() — the exact same call path ChainberryTrustWalletCoreModule.kt uses in
15
+ // production — produces the expected byte-for-byte signed output for a fixed, representative
16
+ // unsignedTx per chain, under WalletCore 4.1.19 (the pinned version). Inputs are deterministic
17
+ // but NOT necessarily broadcast-valid (fake txids/blockhash/etc.) — this is a regression fixture,
18
+ // not a live-network test.
19
+ //
20
+ // BCH is intentionally absent — ChainSigner.sign() throws for BITCOINCASH (sending unsupported
21
+ // on both platforms; see ChainSigning.kt). Every other advertised chain has exactly one vector,
22
+ // except EVM: `ethereum` covers the legacy gasPriceHex path and `polygon` covers the separate
23
+ // EIP-1559 maxFeePerGasHex/maxPriorityFeePerGasHex branch in signEvm — bnb needs no case of its
24
+ // own beyond that (same code path/CoinType as ethereum).
25
+ //
26
+ // Human-readable source of truth: conformance/signing-vectors.json.
27
+ @RunWith(AndroidJUnit4::class)
28
+ class SigningConformanceTest {
29
+
30
+ companion object {
31
+ init { System.loadLibrary("TrustWalletCore") }
32
+
33
+ const val MNEMONIC = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
34
+
35
+ // Wallet's own on-device-derived addresses (see AddressDerivationConformanceTest.VERIFIED) —
36
+ // used as toAddress/changeAddress/Account/feePayer below so every UTXO/XRP/Solana vector is a
37
+ // (fake, never-broadcast) *self-send*, keeping this fixture self-contained with no external
38
+ // address dependency.
39
+ const val ETH_ADDR = "0x9858EfFD232B4033E47d90003D41EC34EcaEda94"
40
+ const val BTC_ADDR = "bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu"
41
+ const val LTC_ADDR = "ltc1qjmxnz78nmc8nq77wuxh25n2es7rzm5c2rkk4wh"
42
+ const val XRP_ADDR = "rHsMGQEkVNJmpGWs8XUBoTBiAAbwxZN5v3"
43
+ const val TON_ADDR = "UQAzWZa6nM5mJev91wGc7VCSfBoIsYRqKJpV78N8Add9-RKY"
44
+
45
+ // Fixed, deterministic (not necessarily real/broadcastable) inputs per chain.
46
+ fun vectors(): Map<String, Pair<ChainKey, Map<String, Any>>> {
47
+ // Same 32-byte all-zero value reused below purely as a deterministic placeholder txid/hash
48
+ // — never broadcast, so it doesn't need to reference a real UTXO/blockhash.
49
+ val fakeTxId = "00".repeat(32)
50
+
51
+ return linkedMapOf(
52
+ "ethereum" to (ChainKey.ETHEREUM to mapOf(
53
+ "to" to ETH_ADDR,
54
+ "chainId" to 1,
55
+ "nonce" to 0,
56
+ "gasLimitHex" to "5208", // 21000
57
+ "valueHex" to "de0b6b3a7640000", // 1 ETH
58
+ "gasPriceHex" to "4a817c800", // 20 gwei — legacy path
59
+ )),
60
+ "polygon" to (ChainKey.POLYGON to mapOf(
61
+ "to" to ETH_ADDR,
62
+ "chainId" to 137,
63
+ "nonce" to 0,
64
+ "gasLimitHex" to "5208",
65
+ "valueHex" to "de0b6b3a7640000",
66
+ "maxFeePerGasHex" to "9502f9000", // EIP-1559 path
67
+ "maxPriorityFeePerGasHex" to "3b9aca00",
68
+ )),
69
+ "bitcoin" to (ChainKey.BITCOIN to utxoTx(CoinType.BITCOIN, BTC_ADDR, fakeTxId)),
70
+ "litecoin" to (ChainKey.LITECOIN to utxoTx(CoinType.LITECOIN, LTC_ADDR, fakeTxId)),
71
+ "xrp" to (ChainKey.XRP to mapOf(
72
+ "Account" to XRP_ADDR,
73
+ "Destination" to XRP_ADDR,
74
+ "Amount" to "1000000", // 1 XRP, drops
75
+ "Fee" to "12",
76
+ "Sequence" to 1,
77
+ "LastLedgerSequence" to 100,
78
+ )),
79
+ "ton" to (ChainKey.TON to mapOf(
80
+ "toAddress" to TON_ADDR,
81
+ "amount" to "1000000000", // 1 TON, nanotons
82
+ "seqno" to 1,
83
+ "memoId" to "conformance-test",
84
+ )),
85
+ "tron" to (ChainKey.TRON to mapOf(
86
+ // A genuinely all-zero digest degenerates silently in wallet-core's direct-sign path
87
+ // (empty signature, no error) — use a non-trivial fixed fake digest instead.
88
+ "txID" to "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
89
+ )),
90
+ "solana" to (ChainKey.SOLANA to mapOf(
91
+ // Fixed unsigned tx built via @solana/web3.js: feePayer/from = this wallet's own
92
+ // Solana address, to = the System Program id (32 zero bytes, a fixed valid pubkey),
93
+ // recentBlockhash = the same all-zero value (never broadcast, only needs to be
94
+ // shape-valid base58 for TransactionDecoder). See conformance/signing-vectors.json's
95
+ // solana entry for the exact base64 blob and how it was generated.
96
+ "unsignedTxBase64" to "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAC6bYGKEG7l3rSHeceyWGQBjPCbyE4TgFbAUpjemFJlUcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQECAAEMAgAAAEBCDwAAAAAA",
97
+ )),
98
+ )
99
+ }
100
+
101
+ // scriptPubKeyHex is computed here (BitcoinScript.lockScriptForAddress), not hand-derived from
102
+ // bech32, so the UTXO vector always matches this wallet's own real P2WPKH lock script.
103
+ private fun utxoTx(coin: CoinType, ownAddress: String, fakeTxId: String): Map<String, Any> {
104
+ val scriptHex = BitcoinScript.lockScriptForAddress(ownAddress, coin).data()
105
+ .joinToString("") { "%02x".format(it) }
106
+ return mapOf(
107
+ "toAddress" to ownAddress,
108
+ "changeAddress" to ownAddress,
109
+ "sendAmountSats" to "50000",
110
+ "changeAmountSats" to "40000",
111
+ "satsPerByte" to 10,
112
+ "inputs" to listOf(
113
+ mapOf(
114
+ "txIdHex" to fakeTxId,
115
+ "vout" to 0,
116
+ "amountSats" to "100000",
117
+ "scriptPubKeyHex" to scriptHex,
118
+ )
119
+ ),
120
+ )
121
+ }
122
+
123
+ // Expected signed outputs, confirmed on-device against WalletCore 4.1.19 (harvested via
124
+ // printSigningVectorsForVerification, see conformance/signing-vectors.json for the full
125
+ // methodology). "ton" is deliberately absent — signTon() embeds a wall-clock
126
+ // `expireAt = now + 600s` into the signed payload, so its output is never byte-reproducible
127
+ // across runs; see tonVectorIsWellFormed() below for what's actually checked instead.
128
+ val EXPECTED: Map<String, String> = mapOf(
129
+ "ethereum" to "0xf86c808504a817c800825208949858effd232b4033e47d90003d41ec34ecaeda94880de0b6b3a76400008026a0b7cca5f69561cd482cec4e692bd2da17d9eb1d6b3a83fe3f7f11d117de99ca97a00ad0dc153460e39b355c226a2744fc2926f84b95b89c954551571f1bbc251478",
130
+ "polygon" to "0x02f874818980843b9aca008509502f9000825208949858effd232b4033e47d90003d41ec34ecaeda94880de0b6b3a764000080c001a038b5c72ee38aa18607462b80ab0cb170a8d4df1c4090ac49acd41d6b4b0f9acca05bdee390ee8a50ff569f55d408371213df72a3065902b07317289b39788cb434",
131
+ "bitcoin" to "0100000000010100000000000000000000000000000000000000000000000000000000000000000000000000000000000250c3000000000000160014c0cebcd6c3d3ca8c75dc5ec62ebe55330ef910e2cebd000000000000160014c0cebcd6c3d3ca8c75dc5ec62ebe55330ef910e20247304402201e8f3663c97712bbf662c96df532e701ca9b660b507ab170d7b4ffbd966d4be0022033bd537c7720b1e7be2b7663d1a0adcbe565ff18a25794aec828015529ab80e101210330d54fd0dd420a6e5f8d3624f5f3482cae350f79d5f0753bf5beef9c2d91af3c00000000",
132
+ "litecoin" to "0100000000010100000000000000000000000000000000000000000000000000000000000000000000000000000000000250c300000000000016001496cd3178f3de0f307bcee1aeaa4d5987862dd30acebd00000000000016001496cd3178f3de0f307bcee1aeaa4d5987862dd30a0248304502210093a73dec624132e9ea3d1a629c0c08be77be68416b0f99cbeb23381967ce8c560220385912e0b0835bcd1dd844bae68e4df208af8b07b095d5ac5ce6cd4407a27a85012102e49c9b9b5d0f127235dc26a0c252814c52fb333d651a946773f59d72c2da990400000000",
133
+ "xrp" to "12000022000000002400000001201b000000646140000000000f424068400000000000000c7321031d68bc1a142e6766b2bdfb006ccfe135ef2e0e2e94abb5cf5c9ab6104776fbae7446304402207fb80c118799a9b3d4dad6e91ad1b8f0c5c9d189a0bcdaf50e823ea44f8044e70220422f638bf4b4e634cb39a4250cf72513d5e3c7e810a13c2f35b5df54b02dc7c98114aff3c2e33458b30714ca16ffee19952dd35c17c88314aff3c2e33458b30714ca16ffee19952dd35c17c8",
134
+ "tron" to "{\"txID\":\"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\",\"signature\":[\"4b63d125c56637e9f09d9578bf9fbf30e0e6796085bb0fe8aa7c466b2ae9bc690a26d26fe36ff0d42d416ce76b70b486c50c0294b50d5a6e9871d2f3a7ebd28901\"]}",
135
+ "solana" to "Aa+eJgDHJo2f9vGrz1EpPPnfGWcZoySFIEw0sS7OVvYiF4U421YCQqLme/uESu3SLan0AM6xblAfUgYlM28StQcBAAAC6bYGKEG7l3rSHeceyWGQBjPCbyE4TgFbAUpjemFJlUcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQECAAEMAgAAAEBCDwAAAAAA",
136
+ )
137
+ }
138
+
139
+ @Test
140
+ fun printSigningVectorsForVerification() {
141
+ val wallet = HDWallet(MNEMONIC, "")
142
+ val lines = vectors().map { (chain, pair) ->
143
+ val (chainKey, unsignedTx) = pair
144
+ val result = try {
145
+ ChainSigner.sign(chainKey, wallet, unsignedTx, isTestnet = false)
146
+ } catch (e: Exception) {
147
+ return@map " $chain -> ERROR: ${e.message}"
148
+ }
149
+ val metaStr = result.meta?.let { " meta=$it" } ?: ""
150
+ " $chain -> ${result.signedTx}$metaStr"
151
+ }
152
+ println(
153
+ "\n[SigningConformanceTest] Harvested signed outputs — verify and populate EXPECTED:\n" +
154
+ lines.joinToString("\n")
155
+ )
156
+ // This test intentionally never fails — it exists only to harvest signing vectors.
157
+ }
158
+
159
+ @Test
160
+ fun signedOutputsMatch() {
161
+ val wallet = HDWallet(MNEMONIC, "")
162
+ for ((chain, pair) in vectors()) {
163
+ val expected = EXPECTED[chain] ?: continue
164
+ val (chainKey, unsignedTx) = pair
165
+ val actual = ChainSigner.sign(chainKey, wallet, unsignedTx, isTestnet = false).signedTx
166
+ assertEquals("signed output mismatch for chain=$chain", expected, actual)
167
+ }
168
+ }
169
+
170
+ // TON's signTon() embeds a wall-clock `expireAt = now + 600s` into the signed BOC, so its
171
+ // output (and meta.txHash, which hashes that BOC) is never byte-reproducible across runs —
172
+ // a real constraint of the production code, not a test gap. This checks the parts that ARE
173
+ // deterministic instead: signing succeeds, the output is a well-formed base64 BOC, and
174
+ // meta.txHash is present and the right shape.
175
+ @Test
176
+ fun tonVectorIsWellFormed() {
177
+ val wallet = HDWallet(MNEMONIC, "")
178
+ val (chainKey, unsignedTx) = vectors().getValue("ton")
179
+ val result = ChainSigner.sign(chainKey, wallet, unsignedTx, isTestnet = false)
180
+ assert(result.signedTx.startsWith("te6cc")) {
181
+ "TON signed output doesn't look like a BOC (expected te6cc... prefix): ${result.signedTx}"
182
+ }
183
+ val txHash = result.meta?.get("txHash") as? String
184
+ assertEquals("TON meta.txHash should be a 32-byte hex string", 64, txHash?.length)
185
+ }
186
+ }