Skip to content

Commit ef6e24a

Browse files
committed
Merge branch 'develop'
2 parents 3baaeee + 65df96c commit ef6e24a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1444
-506
lines changed

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,20 @@
44
</div>
55

66
<h2 align="center">[BETA] Sentinel x</h2>
7-
<p align="center">bitcoin watch only wallet based <a href="https://github.com/Samourai-Wallet/sentinel-android"> samourai sentinel</a>
7+
<p align="center">bitcoin watch only wallet based on <a href="https://github.com/Samourai-Wallet/sentinel-android"> samourai sentinel</a>
88
</p>
99

10+
<p align="center"> <a href="https://github.com/InvertedX/sentinelx/releases">Download</a>
11+
</p>
12+
1013
## Sentinel x Features
1114

1215
* Sleek and faster UI
1316
* Tor Support
1417
* Offline Mode
1518
* Theme support (light/dark and accent)
1619
* db level encryption
17-
* [WIP] Dojo node support
20+
* Dojo node support
1821

1922

2023
## Screenshots
@@ -76,5 +79,5 @@ Sentinel implementation based on [original BIP](https://github.com/bitcoin/bips/
7679

7780
------
7881

79-
[Note]: Sentinel x does not support ios (for now)(since this app is completely based on sentinel android core libs are based on java, in order to support IOS we need to port/implement native calls in ios )
82+
[Note]: Sentinel x does not support ios (for now)(since this app is completely based on sentinel-android, in order to support IOS we need to port/implement native calls in ios )
8083

android/app/build.gradle

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,55 @@ android {
5252
signingConfig signingConfigs.debug
5353
}
5454
}
55+
splits {
5556

57+
// Configures multiple APKs based on ABI.
58+
abi {
59+
60+
enable true
61+
62+
reset()
63+
64+
// include "armeabi-v7a", "arm64-v8a"
65+
66+
universalApk true
67+
68+
}
69+
}
70+
}
71+
72+
ext.abiCodes = ['armeabi-v7a':2, 'arm64-v8a':3]
73+
74+
import com.android.build.OutputFile
75+
76+
// For each APK output variant, override versionCode with a combination of
77+
// ext.abiCodes * 1000 + variant.versionCode. In this example, variant.versionCode
78+
// is equal to defaultConfig.versionCode. If you configure product flavors that
79+
// define their own versionCode, variant.versionCode uses that value instead.
80+
android.applicationVariants.all { variant ->
81+
82+
// Assigns a different version code for each output APK
83+
// other than the universal APK.
84+
variant.outputs.each { output ->
85+
86+
// Stores the value of ext.abiCodes that is associated with the ABI for this variant.
87+
def baseAbiVersionCode =
88+
// Determines the ABI for this variant and returns the mapped value.
89+
project.ext.abiCodes.get(output.getFilter(OutputFile.ABI))
90+
91+
// Because abiCodes.get() returns null for ABIs that are not mapped by ext.abiCodes,
92+
// the following code does not override the version code for universal APKs.
93+
// However, because we want universal APKs to have the lowest version code,
94+
// this outcome is desirable.
95+
if (baseAbiVersionCode != null) {
96+
97+
// Assigns the new version code to versionCodeOverride, which changes the version code
98+
// for only the output APK, not for the variant itself. Skipping this step simply
99+
// causes Gradle to use the value of variant.versionCode for the APK.
100+
output.versionCodeOverride =
101+
baseAbiVersionCode * 1000 + variant.versionCode
102+
}
103+
}
56104
}
57105

58106
flutter {

android/app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,5 +67,6 @@
6767
<uses-permission android:name="android.permission.INTERNET" />
6868
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
6969
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
70+
<uses-permission android:name="android.permission.WAKE_LOCK" />
7071

7172
</manifest>

android/app/src/main/kotlin/com/invertedx/sentinelx/SentinelxApp.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ object SentinelxApp {
2626

2727

2828
public var networkParameters: NetworkParameters = if (BuildConfig.DEBUG) TestNet3Params.get() else MainNetParams.get()
29+
public var accessToken = "";
30+
public var refreshToken = "";
31+
public var dojoEneabled = false
32+
public var dojoUrl = ""
2933

3034

3135
fun isTestNet(): Boolean {
@@ -37,5 +41,10 @@ object SentinelxApp {
3741
networkParameters = param;
3842
}
3943

44+
fun setToken(accessToken: String, refreshToken: String) {
45+
this.accessToken = accessToken
46+
this.refreshToken = refreshToken
47+
}
48+
4049

4150
}

android/app/src/main/kotlin/com/invertedx/sentinelx/api/ApiService.kt

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ import android.content.Context
44
import android.util.Log
55
import com.invertedx.sentinelx.BuildConfig
66
import com.invertedx.sentinelx.SentinelxApp
7+
import com.invertedx.sentinelx.i
78
import com.invertedx.sentinelx.tor.TorManager
89
import com.invertedx.sentinelx.utils.LoggingInterceptor
910
import io.reactivex.Observable
1011
import okhttp3.FormBody
1112
import okhttp3.OkHttpClient
1213
import okhttp3.Request
13-
import java.time.Duration
14-
import java.time.temporal.TemporalUnit
14+
import okhttp3.RequestBody
1515
import java.util.concurrent.TimeUnit
1616
import javax.net.ssl.SSLContext
1717
import javax.net.ssl.TrustManager
@@ -35,7 +35,7 @@ class ApiService(private val applicationContext: Context) {
3535

3636
fun getTxAndXPUBData(XpubOrAddress: String): Observable<String> {
3737
val baseAddress = getBaseUrl()
38-
val url = "${baseAddress}multiaddr?active=$XpubOrAddress"
38+
val url = "${baseAddress}multiaddr?active=$XpubOrAddress&at=${SentinelxApp.accessToken}"
3939
Log.i("API", "CALL url -> $url")
4040
return Observable.fromCallable {
4141
val request = Request.Builder()
@@ -60,6 +60,11 @@ class ApiService(private val applicationContext: Context) {
6060
*/
6161
makeClient()
6262

63+
i("DOJO URL SentinelxApp.dojoUrl")
64+
if (SentinelxApp.dojoUrl.isNotBlank()) {
65+
return SentinelxApp.dojoUrl
66+
}
67+
6368
return if (TorManager.getInstance(this.applicationContext).isConnected) {
6469
if (SentinelxApp.isTestNet()) SAMOURAI_API2_TESTNET_TOR_DIST else SAMOURAI_API2_TOR_DIST
6570
} else {
@@ -69,7 +74,7 @@ class ApiService(private val applicationContext: Context) {
6974

7075
fun getTx(txid: String): Observable<String> {
7176
val baseAddress = getBaseUrl()
72-
val baseUrl = "${baseAddress}tx/$txid/?fees=true&at="
77+
val baseUrl = "${baseAddress}tx/$txid/?fees=trues&at=${SentinelxApp.accessToken}"
7378

7479
return Observable.fromCallable {
7580

@@ -89,11 +94,32 @@ class ApiService(private val applicationContext: Context) {
8994
}
9095
}
9196

97+
98+
fun authenticate(url: String, key: String): Observable<String> {
99+
val targetUrl = "$url/auth/login?apikey=$key"
100+
return Observable.fromCallable {
101+
102+
val request = Request.Builder()
103+
.url(targetUrl)
104+
.post(RequestBody.create(null, ByteArray(0)))
105+
.build()
106+
107+
val response = client.newCall(request).execute()
108+
try {
109+
return@fromCallable response.body()!!.string()
110+
} catch (ex: Exception) {
111+
throw ex
112+
}
113+
114+
}
115+
}
116+
117+
92118
fun getUnspent(xpubOrAddress: String): Observable<String> {
93119
makeClient()
94120

95121
val baseAddress = getBaseUrl()
96-
val baseUrl = "${baseAddress}unspent?active=$xpubOrAddress"
122+
val baseUrl = "${baseAddress}unspent?active=$xpubOrAddress&at=${SentinelxApp.accessToken}"
97123

98124
return Observable.fromCallable {
99125

@@ -114,7 +140,7 @@ class ApiService(private val applicationContext: Context) {
114140

115141
fun addHDAccount(xpub: String, bip: String): Observable<String> {
116142
val baseAddress = getBaseUrl()
117-
val baseUrl = "${baseAddress}xpub"
143+
val baseUrl = "${baseAddress}xpub&at=${SentinelxApp.accessToken}"
118144

119145

120146
val requestBody = FormBody.Builder()
@@ -139,7 +165,6 @@ class ApiService(private val applicationContext: Context) {
139165
}
140166
}
141167

142-
143168
private fun makeClient() {
144169
val builder = OkHttpClient.Builder()
145170
if (BuildConfig.DEBUG) {

android/app/src/main/kotlin/com/invertedx/sentinelx/channel/ApiChannel.kt

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ package com.invertedx.sentinelx.channel
22

33
import android.content.Context
44
import android.util.Log
5+
import com.invertedx.sentinelx.SentinelxApp
56
import com.invertedx.sentinelx.api.ApiService
7+
import com.invertedx.sentinelx.e
8+
import com.invertedx.sentinelx.i
69
import io.flutter.plugin.common.MethodCall
710
import io.flutter.plugin.common.MethodChannel
811
import io.reactivex.android.schedulers.AndroidSchedulers
@@ -105,6 +108,50 @@ class ApiChannel(private val applicationContext: Context) : MethodChannel.Method
105108
result.error("APIError", "Error", it.message)
106109
})
107110
}
111+
"setDojo" -> {
112+
try {
113+
114+
115+
val accessToken = methodCall.argument<String>("accessToken")
116+
var url = methodCall.argument<String>("dojoUrl")
117+
val refreshToken = methodCall.argument<String>("refreshToken")
118+
if (refreshToken == null || accessToken == null) {
119+
result.notImplemented()
120+
return
121+
}
122+
if (!url!!.endsWith("/") && url.isNotEmpty()) {
123+
url = "${url}/"
124+
}
125+
SentinelxApp.setToken(accessToken, refreshToken)
126+
SentinelxApp.dojoUrl = url
127+
SentinelxApp.dojoEneabled = url.trim().isNotEmpty()
128+
} catch (er: Exception) {
129+
e(er)
130+
}
131+
result.success(true);
132+
}
133+
"authenticateDojo" -> {
134+
val url = methodCall.argument<String>("url")
135+
val apiKey = methodCall.argument<String>("apiKey")
136+
if (apiKey == null || url == null) {
137+
result.notImplemented()
138+
return
139+
}
140+
ApiService(applicationContext).authenticate(url, apiKey)
141+
.subscribeOn(Schedulers.io())
142+
.observeOn(AndroidSchedulers.mainThread())
143+
.subscribe({
144+
if (it != null) {
145+
val obj = JSONObject(it)
146+
result.success(obj.toString())
147+
} else {
148+
result.success(false)
149+
}
150+
}, {
151+
it.printStackTrace()
152+
result.error("APIError", "Error", it.message)
153+
})
154+
}
108155

109156
}
110157
}

android/app/src/main/kotlin/com/invertedx/sentinelx/channel/NetworkChannel.kt

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,16 @@ import android.content.Intent
55
import android.net.ConnectivityManager
66
import android.net.wifi.WifiManager
77
import com.invertedx.sentinelx.MainActivity
8-
import com.invertedx.sentinelx.utils.Connectivity
98
import com.invertedx.sentinelx.tor.TorManager
109
import com.invertedx.sentinelx.tor.TorService
10+
import com.invertedx.sentinelx.utils.Connectivity
1111
import io.flutter.plugin.common.EventChannel
1212
import io.flutter.plugin.common.MethodCall
1313
import io.flutter.plugin.common.MethodChannel
1414
import io.reactivex.Observable
1515
import io.reactivex.android.schedulers.AndroidSchedulers
1616
import io.reactivex.disposables.CompositeDisposable
17+
import io.reactivex.functions.Consumer
1718
import io.reactivex.schedulers.Schedulers
1819
import java.util.concurrent.TimeUnit
1920

@@ -74,6 +75,40 @@ class NetworkChannel(private val applicationContext: Context, private val activi
7475
} catch (ex: Exception) {
7576
}
7677
}
78+
"startAndWait" -> {
79+
try {
80+
val startIntent = Intent(applicationContext, TorService::class.java)
81+
startIntent.action = TorService.START_SERVICE
82+
applicationContext.startService(startIntent)
83+
val disposable = TorManager.getInstance(applicationContext)
84+
.torStatus
85+
.subscribeOn(Schedulers.io())
86+
.observeOn(AndroidSchedulers.mainThread())
87+
.subscribe ({
88+
if (it != null)
89+
when (it) {
90+
TorManager.CONNECTION_STATES.CONNECTED -> {
91+
92+
result.success(true)
93+
}
94+
TorManager.CONNECTION_STATES.IDLE -> {
95+
96+
}
97+
TorManager.CONNECTION_STATES.DISCONNECTED -> {
98+
99+
}
100+
TorManager.CONNECTION_STATES.CONNECTING -> {
101+
}
102+
}
103+
}, {
104+
print(it);
105+
})
106+
107+
compositeDisposable.addAll(disposable)
108+
} catch (ex: Exception) {
109+
result.error("Error", "TorError", ex.message)
110+
}
111+
}
77112

78113
"stopTor" -> {
79114
val startIntent = Intent(applicationContext, TorService::class.java)
Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import 'dart:convert';
22

33
import 'package:flutter/services.dart';
4-
import 'package:sentinelx/models/txDetailsResponse.dart';
4+
import 'package:sentinelx/models/dojo.dart';
5+
import 'package:sentinelx/models/tx_details_response.dart';
56

67
class ApiChannel {
78
static const platform = const MethodChannel('api.channel');
@@ -49,8 +50,23 @@ class ApiChannel {
4950
Future<String> getUnspent(List<String> xpubsAndAddresses) async {
5051
String joined = xpubsAndAddresses.join("|");
5152
String response =
52-
await platform.invokeMethod("unspent", {'params': joined});
53+
await platform.invokeMethod("unspent", {'params': joined});
5354
return response;
5455
}
55-
}
5656

57+
Future<DojoAuth> authenticateDojo(String url, String apiKey) async {
58+
String status = await platform
59+
.invokeMethod("authenticateDojo", {"url": url, "apiKey": apiKey});
60+
DojoAuth auth = DojoAuth.fromJson(jsonDecode(status));
61+
return Future.value(auth);
62+
}
63+
64+
Future<bool> setDojo(String accessToken, String refreshToken, String url) async {
65+
await platform.invokeMethod("setDojo", {
66+
'accessToken': accessToken,
67+
"refreshToken": refreshToken,
68+
"dojoUrl": url
69+
});
70+
return Future.value(true);
71+
}
72+
}

0 commit comments

Comments
 (0)