Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ build/
*.iml
local.properties
.DS_Store

# FFI binary — shipped via ant-sdk release, not tracked
*.aar
44 changes: 44 additions & 0 deletions Examples/AntAndroidDemo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,50 @@ adb shell am start -n com.autonomi.examples.antdemo/.MainActivity
- **Download**: paste any chunk address (or tap "Use last") and pull
the content back as text.

## WalletConnect spike

An exploratory spike (`app/.../wallet/`) wiring an external self-custody
wallet via **Reown AppKit** (successor to Web3Modal — same stack family as
the desktop app). The app never holds a private key: it builds the transaction
and the user's wallet signs it. Store-policy-safe payment model; Kotlin mirror
of the iOS spike (`ant-swift` AntSwiftDemo) and of `ant-ui/utils/payment.ts`.
Tracked in Linear **V2-531**.

**What the spike proves:** Connect Wallet → wallet signs a real
`eth_sendTransaction` → tx hash. The tx is an ERC-20 `approve` of the Autonomi
payment vault (the desktop's first payment step). With approve amount `0` it
costs only gas — **no token balance needed**.

**Why a real device shines here:** unlike the iOS *simulator*, a physical
Android device runs MetaMask/Trust, so it completes the signature
**end-to-end**. The approve tx targets Arbitrum, so **no devnet is needed** for
the wallet test — just internet + a wallet.

**Not yet a paid upload** — that needs the external-signer prepare/finalize
surface in `ant-ffi` (Linear **V2-391**). `EthCalldata.payForQuotes(...)` is
already implemented here for that next step.

### Running the spike

1. Get a WalletConnect project id from <https://dashboard.reown.com> and set
`REOWN_PROJECT_ID` in `MainActivity.kt`.
2. `./gradlew :app:installDebug` on a **physical device** with a wallet app
installed.
3. Tap **Connect Wallet**, approve in the wallet, then **Send test approve
tx**. Needs a little ETH on Arbitrum One for gas; for a no-real-funds run,
fill in the Arbitrum **Sepolia** token/vault addresses in
`AutonomiContracts.kt` and target `ARBITRUM_SEPOLIA`.

### Known unknowns (verify in Android Studio)

Not yet compiled. The Reown AppKit Android surface in
`wallet/WalletConnectManager.kt` (init params, `setChains`, the
`ModalDelegate` method set, `AppKit.request`/`getAccount` shapes) and the
Compose modal host (`AppKitComponent` vs a navigation host) are written
against the docs + the WalletConnect Android lineage and **need verification
against the resolved `com.reown:appkit` 1.4.x**. These are the spots most
likely to need adjustment.

## Real-device notes

A real Android device on the same LAN as your dev machine bypasses
Expand Down
19 changes: 19 additions & 0 deletions Examples/AntAndroidDemo/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,23 @@ dependencies {
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.ui:ui-tooling-preview")
debugImplementation("androidx.compose.ui:ui-tooling")

// WalletConnect spike — Reown AppKit (successor to Web3Modal), same stack
// family as the desktop app. BOM 1.4.1 is the latest stable on Maven
// Central (com.reown); artifacts resolve from the mavenCentral() already
// declared in settings.gradle.kts.
implementation(platform("com.reown:android-bom:1.4.1"))
implementation("com.reown:android-core")
implementation("com.reown:appkit")
// The AppKit modal presents itself through Compose navigation: it plugs an
// `appKitGraph` into a NavHost that uses a bottom-sheet navigator
// (androidx.compose.material.navigation, versioned by the Compose BOM).
implementation("androidx.navigation:navigation-compose:2.8.0")
// AppKit's `appKitGraph` (appkit 1.4.1) hosts the modal via ACCOMPANIST's
// navigation-material `bottomSheet {}`, so the NavController must register
// Accompanist's BottomSheetNavigator (not the newer androidx
// material-navigation one — appkit's `develop` sample uses that, but the
// 1.4.1 release still uses accompanist). Pin the exact version appkit pulls.
implementation("com.google.accompanist:accompanist-navigation-material:0.34.0")
implementation("androidx.compose.material:material")
}
Binary file not shown.
28 changes: 28 additions & 0 deletions Examples/AntAndroidDemo/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,39 @@
android:theme="@android:style/Theme.Material.Light.NoActionBar">
<activity
android:name=".MainActivity"
android:launchMode="singleTask"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- WalletConnect spike: deep link the wallet uses to return to us
after signing. Must match the `redirect` in
WalletConnectManager.configure (antdemo-wc://request). -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="antdemo-wc" android:host="request" />
</intent-filter>
<!-- autonomi:// content links (ant-webex scheme). Opens the app,
downloads the addressed content, and shows the Downloads tab. -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="autonomi" />
</intent-filter>
</activity>
<!-- Shares saved datamap files with other apps (the "Open file" action). -->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -1,19 +1,58 @@
package com.autonomi.examples.antdemo

import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import com.autonomi.examples.antdemo.deeplink.AntUri
import com.autonomi.examples.antdemo.deeplink.DeepLinkNav
import com.autonomi.examples.antdemo.files.FilesStore
import com.autonomi.examples.antdemo.ui.AntTheme
import com.autonomi.examples.antdemo.ui.AppShell
import com.autonomi.examples.antdemo.ui.ThemeController
import com.autonomi.examples.antdemo.wallet.WalletConnectManager

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AntFfiBootstrap.plantOnce(applicationContext)
ThemeController.load(applicationContext)
// WalletConnect: dedicated projectId from https://dashboard.reown.com.
WalletConnectManager.configure(application, REOWN_PROJECT_ID)
setContent {
MaterialTheme {
Surface { MainScreen() }
AntTheme {
Surface { AppShell() }
}
}
handleDeepLink(intent)
}

override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
handleDeepLink(intent)
}

/// Handle an `autonomi://<address>[?name=&filetype=&filename=]` VIEW intent:
/// start the download (ant-webex-style filename fallbacks) and jump to the
/// Downloads tab.
private fun handleDeepLink(intent: Intent?) {
val data = intent?.dataString ?: return
if (!data.startsWith("autonomi://")) return
FilesStore.download(data, applicationContext)
DeepLinkNav.goto("downloads")
}

override fun onResume() {
super.onResume()
// The wallet may have approved the session while we were backgrounded.
WalletConnectManager.refreshAccount()
}

companion object {
// Dedicated Reown (WalletConnect Cloud) projectId for the mobile SDK.
// projectIds are public client identifiers (shipped in apps), not secrets.
private const val REOWN_PROJECT_ID = "2cd5b44944e27d5234557a9183dc1cdd"
}
}
Original file line number Diff line number Diff line change
@@ -1,134 +1,5 @@
package com.autonomi.examples.antdemo

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import uniffi.ant_ffi.Client
import java.io.File
import kotlin.random.Random

/// Path the rewritten devnet manifest is pushed to on the emulator:
/// adb push /tmp/devnet-manifest-android.json /data/local/tmp/devnet-manifest.json
/// See README for the rewrite step (127.0.0.1 -> 10.0.2.2).
private const val MANIFEST_PATH = "/data/local/tmp/devnet-manifest.json"

@Composable
fun MainScreen() {
var inputText by remember { mutableStateOf("hello autonomi") }
var addressInput by remember { mutableStateOf("") }
var lastUploadedAddress by remember { mutableStateOf("") }
var downloadedText by remember { mutableStateOf("") }
var status by remember { mutableStateOf("Idle. Push a devnet manifest, then tap Upload.") }
var busy by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()

Column(
modifier = Modifier.fillMaxWidth().padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text("AntFfi Demo", style = MaterialTheme.typography.headlineSmall)

OutlinedTextField(
value = inputText,
onValueChange = { inputText = it },
label = { Text("Text to upload") },
modifier = Modifier.fillMaxWidth()
)
Button(
enabled = !busy && inputText.isNotEmpty(),
onClick = {
scope.launch {
busy = true
status = "Connecting to devnet…"
try {
val client = withContext(Dispatchers.IO) {
Client.connectFromDevnetManifest(MANIFEST_PATH)
}
// Random suffix so successive taps produce distinct chunks
// despite content-addressed storage.
val suffix = Random.nextInt().toUInt().toString(36)
val payload = "$inputText [$suffix]".toByteArray()
status = "Uploading ${payload.size} bytes…"
val result = withContext(Dispatchers.IO) { client.chunkPut(payload) }
lastUploadedAddress = result.address
status = "Uploaded. Tap Download or copy the address."
} catch (e: Throwable) {
status = "Upload failed: ${e.message}"
} finally { busy = false }
}
}
) { Text("Upload (appends random suffix)") }

if (lastUploadedAddress.isNotEmpty()) {
SelectionContainer {
Text(
"Address: $lastUploadedAddress",
fontFamily = FontFamily.Monospace,
style = MaterialTheme.typography.bodySmall
)
}
}

OutlinedTextField(
value = addressInput,
onValueChange = { addressInput = it },
label = { Text("Address (hex)") },
modifier = Modifier.fillMaxWidth()
)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(
enabled = !busy && addressInput.isNotEmpty(),
onClick = {
scope.launch {
busy = true
status = "Connecting to devnet…"
try {
val client = withContext(Dispatchers.IO) {
Client.connectFromDevnetManifest(MANIFEST_PATH)
}
status = "Downloading…"
val data = withContext(Dispatchers.IO) { client.chunkGet(addressInput) }
downloadedText = runCatching { String(data, Charsets.UTF_8) }
.getOrDefault("<${data.size} non-UTF8 bytes>")
status = "Downloaded ${data.size} bytes."
} catch (e: Throwable) {
status = "Download failed: ${e.message}"
} finally { busy = false }
}
}
) { Text("Download") }
Button(
enabled = lastUploadedAddress.isNotEmpty(),
onClick = { addressInput = lastUploadedAddress }
) { Text("Use last") }
}

if (downloadedText.isNotEmpty()) {
SelectionContainer { Text("Content: $downloadedText") }
}

Text(status, style = MaterialTheme.typography.bodySmall)
if (busy) { CircularProgressIndicator() }
}
}
// The app shell + screens now live in ui/AppShell.kt, NodesScreen.kt,
// files/FilesScreen.kt, WalletScreen.kt and SettingsScreen.kt. This file is
// intentionally empty (kept to avoid churn in the module file list).
Loading