Skip to content

Commit ee30d9c

Browse files
Merge pull request #21 from Soneso/sa-ext-signer
Add external wallet signing to smart account demo
2 parents c7ad69f + 97f361c commit ee30d9c

28 files changed

Lines changed: 2749 additions & 130 deletions

File tree

smart-account-demo/README.md

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,11 +120,21 @@ open StellarSmartDemo.xcodeproj
120120
```
121121

122122
In Xcode:
123-
1. Add the swift-sodium package if not already added (File > Add Packages > `https://github.com/jedisct1/swift-sodium`). Select the Clibsodium product.
124-
2. Select the StellarSmartDemo scheme and an iOS 16.0+ simulator.
125-
3. Run (Cmd+R).
123+
1. Resolve packages: File -> Packages -> Resolve Package Versions (downloads swift-sodium and reown-swift dependencies).
124+
2. Set your signing team: select the StellarSmartDemo target -> Signing & Capabilities -> Team. This is required for both simulator and device builds and must be set after every `xcodegen generate`.
125+
3. Select the StellarSmartDemo scheme and an iOS 16.0+ simulator.
126+
4. Run (Cmd+R).
126127

127-
**Running on a physical device**: The default configuration targets the iOS simulator. To run on a physical iPhone, change the framework dependency path in `iosApp/project.yml` from `iosSimulatorArm64` to `iosArm64`, regenerate the Xcode project with `xcodegen generate`, select your device in Xcode, and build. You also need a valid signing team configured in the Signing & Capabilities tab.
128+
**Switching between simulator and physical device:**
129+
130+
The framework dependency in `project.yml` (line 55) must match the target platform. After changing it, run `xcodegen generate` to regenerate the Xcode project, then set your signing team again.
131+
132+
| Target | Framework path in project.yml line 55 |
133+
|--------|---------------------------------------|
134+
| Simulator | `../shared/build/bin/iosSimulatorArm64/debugFramework/shared.framework` |
135+
| Physical device | `../shared/build/bin/iosArm64/debugFramework/shared.framework` |
136+
137+
The pre-build script automatically builds the correct Kotlin framework based on the selected target platform. You only need to change the framework dependency path and regenerate.
128138

129139
### macOS
130140

@@ -201,11 +211,41 @@ All testnet configuration is centralized in `shared/src/commonMain/kotlin/com/so
201211
| `DEFAULT_INDEXER_URL` | Credential-to-contract address lookup service |
202212
| `DEFAULT_RP_ID` | WebAuthn Relying Party ID (`soneso.com`) |
203213
| `RP_NAME` | Display name for passkey prompts |
214+
| `REOWN_PROJECT_ID` | Reown (WalletConnect) project ID for external wallet connection |
204215

205216
DEMO token settings (`DEMO_TOKEN_*`) control the deterministic deployment and minting of a custom Soroban token used for testing transfers.
206217

207218
Known policy contracts (threshold, spending limit, weighted threshold) are defined in `KNOWN_POLICIES`.
208219

220+
## External Wallet Connection (Freighter)
221+
222+
The demo supports connecting an external Stellar wallet (Freighter) as a delegated signer, as an alternative to entering a secret key manually.
223+
224+
| Platform | Method | Requirement |
225+
|----------|--------|-------------|
226+
| Web | Freighter browser extension | Install from [freighter.app](https://www.freighter.app/) |
227+
| Android | WalletConnect v2 (Reown) | Freighter Mobile on the same device |
228+
| iOS | WalletConnect v2 (Reown) | Freighter Mobile on the same device |
229+
| macOS | Not available | Use secret key entry |
230+
231+
Wallet connection buttons are hidden on simulators and emulators since WalletConnect requires the wallet app on a real device.
232+
233+
### Reown Project ID
234+
235+
Android and iOS use the [Reown](https://cloud.reown.com/) (WalletConnect v2) SDK. A project ID is required. Set `REOWN_PROJECT_ID` in `DemoConfig.kt`. Register for a free project ID at [cloud.reown.com](https://cloud.reown.com/).
236+
237+
### iOS App Group (required for device builds)
238+
239+
The Reown SDK requires an App Group for relay session storage. The entitlement `group.com.soneso.stellar.smartdemo` is configured in `iosApp/StellarSmartDemo/StellarSmartDemo.entitlements`.
240+
241+
To run on a physical iOS device, register this App Group in your Apple Developer account:
242+
243+
1. Go to [developer.apple.com](https://developer.apple.com/account/resources/identifiers/list/applicationGroup) -> Certificates, Identifiers & Profiles -> Identifiers -> App Groups
244+
2. Click **+** and register `group.com.soneso.stellar.smartdemo`
245+
3. In Xcode, refresh the App Group capability (Signing & Capabilities -> App Groups -> refresh button)
246+
247+
This is not needed for simulator builds (wallet connection is disabled on simulators).
248+
209249
## Quick Reference
210250

211251
| Task | Command |

smart-account-demo/androidApp/build.gradle.kts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,10 @@ kotlin {
4242
dependencies {
4343
implementation(project(":smart-account-demo:shared"))
4444
implementation("androidx.activity:activity-compose:1.8.2")
45+
46+
// Reown (WalletConnect v2) for external wallet connection via Freighter Mobile.
47+
// com.reown:android-core provides CoreClient (relay, pairing, metadata).
48+
// com.reown:sign provides SignClient (session proposal, request/response, delegates).
49+
implementation("com.reown:android-core:1.6.12")
50+
implementation("com.reown:sign:1.6.12")
4551
}

smart-account-demo/androidApp/src/main/AndroidManifest.xml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,30 @@
44
<uses-permission android:name="android.permission.INTERNET" />
55

66
<application
7+
android:name=".SmartDemoApplication"
78
android:allowBackup="true"
89
android:label="Smart Account Demo"
910
android:supportsRtl="true"
1011
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
12+
1113
<activity
1214
android:name=".MainActivity"
13-
android:exported="true">
15+
android:exported="true"
16+
android:launchMode="singleTop">
17+
18+
<!-- App launcher intent -->
1419
<intent-filter>
1520
<action android:name="android.intent.action.MAIN" />
1621
<category android:name="android.intent.category.LAUNCHER" />
1722
</intent-filter>
23+
24+
<!-- Deep link return from Freighter Mobile / WalletConnect redirect -->
25+
<intent-filter>
26+
<action android:name="android.intent.action.VIEW" />
27+
<category android:name="android.intent.category.DEFAULT" />
28+
<category android:name="android.intent.category.BROWSABLE" />
29+
<data android:scheme="stellar-smartdemo" />
30+
</intent-filter>
1831
</activity>
1932

2033
<!-- Passkey support: Digital Asset Links configuration -->

smart-account-demo/androidApp/src/main/java/com/soneso/stellar/smartdemo/android/MainActivity.kt

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.soneso.stellar.smartdemo.android
22

3+
import android.content.Intent
4+
import android.os.Build
35
import android.os.Bundle
46
import androidx.activity.ComponentActivity
57
import androidx.activity.compose.setContent
@@ -9,31 +11,44 @@ import com.soneso.smartdemo.config.DemoConfig
911
import com.soneso.smartdemo.platform.initAndroidClipboard
1012
import com.soneso.smartdemo.state.ActivityLogState
1113
import com.soneso.smartdemo.state.DemoState
14+
import com.soneso.smartdemo.wallet.ReownConnector
1215
import com.soneso.stellar.sdk.smartaccount.AndroidStorageAdapter
1316
import com.soneso.stellar.sdk.smartaccount.AndroidWebAuthnProvider
1417
import kotlinx.coroutines.launch
1518

1619
class MainActivity : ComponentActivity() {
20+
1721
override fun onCreate(savedInstanceState: Bundle?) {
1822
super.onCreate(savedInstanceState)
1923

2024
initAndroidClipboard(this)
2125

22-
// Initialize Smart Account Kit with Android-specific providers
26+
// Register the Reown wallet connector so shared code can trigger wallet flows.
27+
// Skipped on emulators (WalletConnect requires Freighter Mobile on a real device)
28+
// and when REOWN_PROJECT_ID is blank. This heuristic covers the standard Android
29+
// emulator (ranchu/goldfish) and SDK-based images. Some third-party emulators
30+
// (e.g., Genymotion) may not be detected — wallet connection will timeout on those.
31+
val isEmulator = Build.HARDWARE == "ranchu" || Build.HARDWARE == "goldfish" ||
32+
Build.PRODUCT.contains("sdk") || Build.FINGERPRINT.contains("generic")
33+
if (!isEmulator && DemoConfig.REOWN_PROJECT_ID.isNotBlank()) {
34+
DemoState.setWalletConnector(ReownConnector(applicationContext))
35+
}
36+
37+
// Initialize Smart Account Kit with Android-specific providers.
2338
lifecycleScope.launch {
2439
try {
25-
// Create storage adapter using encrypted SharedPreferences
40+
// Create storage adapter using encrypted SharedPreferences.
2641
val storage = AndroidStorageAdapter(applicationContext)
2742

28-
// Create WebAuthn provider for passkey authentication
29-
// rpId must match the domain configured in assetlinks.json
43+
// Create WebAuthn provider for passkey authentication.
44+
// rpId must match the domain configured in assetlinks.json.
3045
val webauthnProvider = AndroidWebAuthnProvider(
3146
context = this@MainActivity,
3247
rpId = DemoConfig.DEFAULT_RP_ID,
3348
rpName = DemoConfig.RP_NAME
3449
)
3550

36-
// Store platform providers in DemoState so shared code can use them
51+
// Store platform providers in DemoState so shared code can use them.
3752
DemoState.setWebAuthnProvider(webauthnProvider)
3853
DemoState.setStorage(storage)
3954

@@ -47,4 +62,17 @@ class MainActivity : ComponentActivity() {
4762
App()
4863
}
4964
}
65+
66+
/**
67+
* Called when the activity receives a new intent after creation (e.g. the deep link
68+
* redirect URI `stellar-smartdemo://request` sent by Freighter Mobile on return).
69+
*
70+
* The Reown SDK handles the relay connection internally via its WebSocket; this callback
71+
* exists to satisfy the `singleTop` launch mode contract and ensure the activity is
72+
* brought to the foreground correctly after the wallet redirects back.
73+
*/
74+
override fun onNewIntent(intent: Intent) {
75+
super.onNewIntent(intent)
76+
setIntent(intent)
77+
}
5078
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package com.soneso.stellar.smartdemo.android
2+
3+
import android.app.Application
4+
import android.util.Log
5+
import com.reown.android.Core
6+
import com.reown.android.CoreClient
7+
import com.reown.android.relay.ConnectionType
8+
import com.reown.sign.client.Sign
9+
import com.reown.sign.client.SignClient
10+
import com.soneso.smartdemo.config.DemoConfig
11+
12+
/**
13+
* Application subclass that initializes Reown CoreClient and SignClient at app start.
14+
*
15+
* Both clients must be initialized before any Activity is created so that the
16+
* relay connection is available when the wallet connector is first used.
17+
* Initialization is skipped when REOWN_PROJECT_ID is blank to allow the app
18+
* to run without wallet connection support during development.
19+
*/
20+
class SmartDemoApplication : Application() {
21+
22+
override fun onCreate() {
23+
super.onCreate()
24+
25+
val projectId = DemoConfig.REOWN_PROJECT_ID
26+
if (projectId.isBlank()) {
27+
Log.w(TAG, "REOWN_PROJECT_ID is not set. Wallet connection via Freighter Mobile will not work.")
28+
return
29+
}
30+
31+
CoreClient.initialize(
32+
application = this,
33+
projectId = projectId,
34+
metaData = Core.Model.AppMetaData(
35+
name = "Stellar Smart Account Demo",
36+
description = "Smart account demo app for the KMP Stellar SDK",
37+
url = "https://soneso.com",
38+
icons = listOf("https://soneso.com/icon.png"),
39+
redirect = "stellar-smartdemo://request"
40+
),
41+
connectionType = ConnectionType.AUTOMATIC,
42+
onError = { error ->
43+
Log.e(TAG, "CoreClient initialization error: ${error.throwable.message}", error.throwable)
44+
}
45+
)
46+
47+
SignClient.initialize(
48+
init = Sign.Params.Init(core = CoreClient),
49+
onSuccess = {
50+
Log.d(TAG, "SignClient initialized successfully")
51+
},
52+
onError = { error ->
53+
Log.e(TAG, "SignClient initialization error: ${error.throwable.message}", error.throwable)
54+
}
55+
)
56+
}
57+
58+
private companion object {
59+
private const val TAG = "SmartDemoApplication"
60+
}
61+
}
Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,10 @@
11
// Smart Account Demo - root build file
2-
// This module is intentionally empty; subprojects define their own plugins and dependencies.
2+
// Subprojects define their own plugins and dependencies.
3+
4+
subprojects {
5+
repositories {
6+
// Required by Reown (WalletConnect v2) transitive dependencies:
7+
// com.github.komputing.kethereum, com.github.multiformats, com.walletconnect.Scarlet
8+
maven("https://jitpack.io")
9+
}
10+
}

smart-account-demo/iosApp/StellarSmartDemo/Info.plist

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,19 @@
1818
<string>APPL</string>
1919
<key>CFBundleShortVersionString</key>
2020
<string>0.1.0</string>
21+
<key>CFBundleURLTypes</key>
22+
<array>
23+
<dict>
24+
<key>CFBundleURLName</key>
25+
<string>com.soneso.stellar.smartdemo</string>
26+
<key>CFBundleURLSchemes</key>
27+
<array>
28+
<string>stellar-smartdemo</string>
29+
</array>
30+
</dict>
31+
</array>
2132
<key>CFBundleVersion</key>
22-
<integer>1</integer>
33+
<string>1</string>
2334
<key>UILaunchStoryboardName</key>
2435
<string>LaunchScreen</string>
2536
<key>UISupportedInterfaceOrientations</key>

smart-account-demo/iosApp/StellarSmartDemo/StellarSmartDemo.entitlements

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,9 @@
66
<array>
77
<string>webcredentials:soneso.com?mode=developer</string>
88
</array>
9+
<key>com.apple.security.application-groups</key>
10+
<array>
11+
<string>group.com.soneso.stellar.smartdemo</string>
12+
</array>
913
</dict>
1014
</plist>

0 commit comments

Comments
 (0)