Skip to content

Commit 9fa8a9d

Browse files
committed
feat(flipcash): enable onramp user flow during pool bid, with only deposit for now
also get phantom wallet connection working via deeplinks Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent 6dcf6ef commit 9fa8a9d

52 files changed

Lines changed: 983 additions & 421 deletions

File tree

Some content is hidden

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

apps/codeApp/src/main/java/com/getcode/CodeApp.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ fun CodeApp(tipsEngine: TipsEngine) {
139139
AuthCheck(
140140
navigator = codeNavigator,
141141
onNavigate = { screens ->
142-
codeNavigator.replaceAll(screens, inSheet = false)
142+
codeNavigator.replaceAll(screens)
143143
},
144144
onSwitchAccounts = { seed ->
145145
activity?.let {

apps/flipcash/app/src/main/AndroidManifest.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,18 @@
7878
android:scheme="https" />
7979
</intent-filter>
8080

81+
<intent-filter android:autoVerify="true">
82+
<action android:name="android.intent.action.VIEW" />
83+
84+
<category android:name="android.intent.category.DEFAULT" />
85+
<category android:name="android.intent.category.BROWSABLE" />
86+
87+
<data
88+
android:host="app.flipcash.com"
89+
android:pathPattern="/phantom/connected"
90+
android:scheme="https" />
91+
</intent-filter>
92+
8193
<intent-filter android:autoVerify="true">
8294
<action android:name="android.intent.action.VIEW" />
8395

apps/flipcash/app/src/main/kotlin/com/flipcash/app/FlipcashApp.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@ import com.flipcash.app.currency.PreferredCurrencyController
1818
import com.getcode.crypt.MnemonicCache
1919
import com.getcode.opencode.repositories.EventRepository
2020
import com.getcode.utils.ErrorUtils
21+
import com.getcode.utils.TraceType
2122
import com.getcode.utils.trace
2223
import com.google.firebase.Firebase
2324
import com.google.firebase.crashlytics.crashlytics
2425
import com.google.firebase.initialize
26+
import com.ionspin.kotlin.crypto.LibsodiumInitializer
2527
import dagger.hilt.android.HiltAndroidApp
2628
import io.reactivex.rxjava3.plugins.RxJavaPlugins
2729
import timber.log.Timber
@@ -50,7 +52,6 @@ class FlipcashApp : Application(), Configuration.Provider, SingletonImageLoader.
5052

5153
override fun onCreate() {
5254
super.onCreate()
53-
5455
if (BuildConfig.DEBUG) {
5556
Timber.plant(object : Timber.DebugTree() {
5657
override fun createStackElementTag(element: StackTraceElement): String {
@@ -78,6 +79,10 @@ class FlipcashApp : Application(), Configuration.Provider, SingletonImageLoader.
7879
Bugsnag.start(this)
7980
}
8081

82+
LibsodiumInitializer.initializeWithCallback {
83+
trace("libsodium initialized", type = TraceType.Process)
84+
}
85+
8186
RxJavaPlugins.setErrorHandler {
8287
ErrorUtils.handleError(it)
8388
}

apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/App.kt

Lines changed: 131 additions & 110 deletions
Large diffs are not rendered by default.

apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/AppScreenContent.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import com.flipcash.app.myaccount.MyAccountScreen
2121
import com.flipcash.app.onramp.OnRampAmountScreen
2222
import com.flipcash.app.onramp.OnRampProviderListScreen
2323
import com.flipcash.app.onramp.OnRampFlowTracker
24+
import com.flipcash.app.onramp.internal.data.LocalPhantomDepositState
2425
import com.flipcash.app.permissions.CameraPermissionScreen
2526
import com.flipcash.app.permissions.NotificationPermissionScreen
2627
import com.flipcash.app.pools.PoolBettingScreen
@@ -122,7 +123,7 @@ internal fun AppScreenContent(content: @Composable () -> Unit) {
122123
}
123124

124125
register<NavScreenProvider.HomeScreen.OnRamp.ProviderList> {
125-
OnRampFlowTracker.start()
126+
OnRampFlowTracker.start(it.from)
126127
OnRampProviderListScreen()
127128
}
128129

apps/flipcash/core/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ dependencies {
7777
implementation(project(":ui:theme"))
7878
implementation(Libs.rinku_compose)
7979

80+
api(Libs.sodium_bindings)
81+
82+
8083
api(project(":vendor:kik:scanner"))
8184

8285
api(project(":ui:core"))

apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/NavScreenProvider.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ sealed class NavScreenProvider : ScreenProvider {
5656
data object ShareApp : NavScreenProvider()
5757

5858
sealed class OnRamp {
59-
data object ProviderList: NavScreenProvider()
59+
data class ProviderList(val from: NavScreenProvider? = null): NavScreenProvider()
6060
data object Amount : NavScreenProvider()
6161
}
6262

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package com.flipcash.app.core.encryption
2+
3+
import android.util.Base64
4+
import com.getcode.ed25519.Ed25519.KeyPair
5+
import com.getcode.solana.keys.PrivateKey
6+
import com.getcode.solana.keys.PublicKey
7+
import com.ionspin.kotlin.crypto.box.Box
8+
import com.ionspin.kotlin.crypto.secretbox.SecretBox
9+
import com.ionspin.kotlin.crypto.signature.Signature
10+
11+
/** Some cryptographic function require the private
12+
* key to be formatted this way to work correctly.
13+
* A good example of this would Sodium and the box
14+
* `seal` and `open` functions.
15+
*/
16+
val KeyPair.encryptionPrivateKey: PrivateKey?
17+
get() {
18+
if (seed.isNullOrEmpty()) return null
19+
20+
val bytes = Base64.decode(seed, Base64.DEFAULT) + publicKeyBytes
21+
return PrivateKey(bytes.toList())
22+
}
23+
24+
val PrivateKey.curvePrivate: Result<PrivateKey>
25+
get() {
26+
val localBytes = bytes
27+
28+
val result = runCatching { Signature.ed25519SkToCurve25519(localBytes.map { it.toUByte() }.toUByteArray()) }
29+
30+
if (result.isFailure) {
31+
return Result.failure(SodiumError.ConversionToCurveFailed(result.exceptionOrNull()))
32+
}
33+
return Result.success(result.getOrNull()!!.toPrivateKey())
34+
}
35+
36+
val PublicKey.curvePublic: Result<PublicKey>
37+
get() {
38+
val localBytes = bytes
39+
40+
val result = runCatching { Signature.ed25519PkToCurve25519(localBytes.map { it.toUByte() }.toUByteArray()) }
41+
if (result.isFailure) {
42+
return Result.failure(SodiumError.ConversionToCurveFailed(result.exceptionOrNull()))
43+
}
44+
return Result.success(result.getOrNull()!!.toPublicKey())
45+
}
46+
47+
fun PublicKey.Companion.shared(publicKey: PublicKey, privateKey: PrivateKey): Result<PublicKey> {
48+
val pubKeyBytes = publicKey.bytes.map { it.toUByte() }.toUByteArray()
49+
val secretKeyBytes = privateKey.bytes.map { it.toUByte() }.toUByteArray()
50+
val result = runCatching { Box.beforeNM(pubKeyBytes, secretKeyBytes) }
51+
52+
if (result.isFailure) {
53+
return Result.failure(SodiumError.SharedKeyFailed(result.exceptionOrNull()))
54+
}
55+
return Result.success(result.getOrNull()!!.toPublicKey())
56+
}
57+
58+
fun PublicKey.Companion.fromUbytes(bytes: List<UByte>): PublicKey {
59+
return PublicKey(bytes.map { it.toByte() })
60+
}
61+
62+
fun String.boxSeal(privateKey: PrivateKey, publicKey: PublicKey, nonce: List<Byte>): Result<List<Byte>> {
63+
val publicCurve = publicKey.curvePublic.getOrThrow()
64+
val privateCurve = privateKey.curvePrivate.getOrThrow()
65+
66+
val sharedKey = PublicKey.shared(publicCurve, privateCurve).getOrThrow()
67+
val nonceU = nonce.map { it.toUByte() }.toUByteArray()
68+
val message = toByteArray().map { it.toUByte() }.toUByteArray()
69+
70+
val encrypted = runCatching {
71+
SecretBox.easy(
72+
key = sharedKey.bytes.map { it.toUByte() }.toUByteArray(),
73+
message = message,
74+
nonce = nonceU
75+
)
76+
}
77+
78+
if (encrypted.isFailure) {
79+
return Result.failure(SodiumError.EncryptionFailed(encrypted.exceptionOrNull()))
80+
}
81+
82+
return Result.success(encrypted.getOrNull()!!.map { it.toByte() })
83+
}
84+
85+
fun List<Byte>.boxOpen(privateKey: PrivateKey, publicKey: PublicKey, nonce: List<Byte>): Result<List<Byte>> {
86+
val publicCurve = publicKey.curvePublic.getOrThrow()
87+
val privateCurve = privateKey.curvePrivate.getOrThrow()
88+
89+
val sharedKey = PublicKey.shared(publicCurve, privateCurve).getOrThrow()
90+
.bytes.map { it.toUByte() }.toUByteArray()
91+
92+
val nonceU = nonce.map { it.toUByte() }.toUByteArray()
93+
val cipher = map { it.toUByte() }.toUByteArray()
94+
95+
val decrypted = runCatching { SecretBox.openEasy(cipher, nonceU, sharedKey) }
96+
97+
if (decrypted.isFailure) {
98+
return Result.failure(SodiumError.DecryptionFailed(decrypted.exceptionOrNull()))
99+
}
100+
101+
return Result.success(decrypted.getOrNull()!!.map { it.toByte() })
102+
}
103+
104+
fun List<Byte>.boxOpen(privateKey: List<Byte>, publicKey: List<Byte>, nonce: List<Byte>): Result<List<Byte>> {
105+
val sharedKey = PublicKey.shared(PublicKey(publicKey), PrivateKey(privateKey)).getOrThrow()
106+
.bytes.map { it.toUByte() }.toUByteArray()
107+
108+
val nonceU = nonce.map { it.toUByte() }.toUByteArray()
109+
val cipher = map { it.toUByte() }.toUByteArray()
110+
111+
val decrypted = runCatching { SecretBox.openEasy(cipher, nonceU, sharedKey) }
112+
113+
if (decrypted.isFailure) {
114+
return Result.failure(SodiumError.DecryptionFailed(decrypted.exceptionOrNull()))
115+
}
116+
117+
return Result.success(decrypted.getOrNull()!!.map { it.toByte() })
118+
}
119+
120+
sealed class SodiumError {
121+
data class ConversionToCurveFailed(val root: Throwable? = null): Throwable(cause = root)
122+
data class SharedKeyFailed(val root: Throwable? = null): Throwable(cause = root)
123+
data class EncryptionFailed(val root: Throwable? = null): Throwable(cause = root)
124+
data class DecryptionFailed(val root: Throwable? = null): Throwable(cause = root)
125+
}
126+
127+
internal fun ByteArray.toPublicKey(): PublicKey {
128+
return PublicKey(this.toList())
129+
}
130+
131+
fun UByteArray.toPublicKey(): PublicKey {
132+
return PublicKey.fromUbytes(this.toList())
133+
}
134+
135+
internal fun UByteArray.toPrivateKey(): PrivateKey {
136+
return PrivateKey(this.toList().map { it.toByte() })
137+
}

apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/navigation/DeeplinkType.kt

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ package com.flipcash.app.core.navigation
22

33
import android.net.Uri
44
import android.os.Parcelable
5+
import com.flipcash.app.core.NavScreenProvider
6+
import com.flipcash.app.core.phantom.PhantomConnectionResult
7+
import com.flipcash.app.core.phantom.PhantomDeeplinkOrigin
58
import com.getcode.ed25519.Ed25519
6-
import com.getcode.opencode.utils.base58
7-
import com.getcode.utils.encodeBase64
9+
import com.getcode.opencode.model.core.ID
810
import com.getcode.vendor.Base58
911
import kotlinx.parcelize.Parcelize
1012

@@ -15,8 +17,12 @@ sealed interface DeeplinkType: Parcelable {
1517
data class Pool(val seed: String) : DeeplinkType {
1618
val rendezvous: Ed25519.KeyPair
1719
get() = Ed25519.createKeyPair(Base58.decode(seed))
18-
1920
}
21+
22+
data class PhantomConnection(
23+
val origin: PhantomDeeplinkOrigin,
24+
val result: PhantomConnectionResult,
25+
): DeeplinkType
2026
}
2127

2228
val Uri.fragments: Map<Key, String>
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package com.flipcash.app.core.phantom
2+
3+
import android.os.Parcelable
4+
import com.flipcash.app.core.NavScreenProvider
5+
import com.getcode.ed25519.Ed25519
6+
import com.getcode.opencode.model.core.ID
7+
import com.getcode.solana.keys.PublicKey
8+
import kotlinx.parcelize.Parcelize
9+
import kotlinx.serialization.SerialName
10+
import kotlinx.serialization.Serializable
11+
12+
@Serializable
13+
@Parcelize
14+
data class PhantomEncryptedData(
15+
@SerialName("public_key")
16+
val publicKey: PublicKey,
17+
val session: String,
18+
): Parcelable
19+
20+
@Parcelize
21+
sealed interface PhantomDeeplinkOrigin: Parcelable {
22+
@Parcelize
23+
data object Menu: PhantomDeeplinkOrigin
24+
@Parcelize
25+
data class PoolWithId(val id: ID): PhantomDeeplinkOrigin
26+
@Parcelize
27+
data class PoolWithRendezvous(val keyPair: Ed25519.KeyPair): PhantomDeeplinkOrigin
28+
29+
companion object {
30+
fun fromScreenProvider(provider: NavScreenProvider?): PhantomDeeplinkOrigin? {
31+
return when (provider) {
32+
is NavScreenProvider.HomeScreen.Menu.Root -> Menu
33+
is NavScreenProvider.HomeScreen.Pools.ChoiceSelection -> {
34+
provider.rendezvous?.let { keyPair -> PoolWithRendezvous(keyPair) }
35+
provider.poolId?.let { id -> PoolWithId(id) }
36+
}
37+
38+
else -> null
39+
}
40+
}
41+
}
42+
}
43+
44+
@Parcelize
45+
data class PhantomConnectionResult(
46+
val encryptionPublicKey: List<Byte>,
47+
val nonce: List<Byte>,
48+
val encryptedData: List<Byte>
49+
): Parcelable

0 commit comments

Comments
 (0)