Skip to content
This repository was archived by the owner on Oct 20, 2025. It is now read-only.
Merged
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ apply from: 'publishLocal.gradle'
```
Build and push the code to Maven Local repo with:
```
./gradlew publishLibraryDebugPublicationToMavenLocal
./gradlew publishToMavenLocal
```
Then add "-local-debug" to the library import from the main app's build.gradle
```
Expand Down
28 changes: 21 additions & 7 deletions app/build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id("kotlin-kapt")
id("kotlinx-serialization")
}

android {
Expand All @@ -10,7 +12,7 @@ android {
defaultConfig {
applicationId "exchange.dydx.carteraexample"
minSdk 24
targetSdk 34
targetSdk 35
versionCode 1
versionName "1.0"

Expand All @@ -36,7 +38,7 @@ android {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion "1.4.7"
kotlinCompilerExtensionVersion "1.5.14"
}

configurations{
Expand All @@ -50,20 +52,20 @@ dependencies {
implementation 'androidx.core:core-ktx:1.15.0'
implementation platform('org.jetbrains.kotlin:kotlin-bom:1.9.24')
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.7'
implementation 'androidx.activity:activity-compose:1.9.3'
implementation platform('androidx.compose:compose-bom:2024.11.00')
implementation 'androidx.activity:activity-compose:1.10.1'
implementation platform('androidx.compose:compose-bom:2025.03.00')
implementation 'com.google.accompanist:accompanist-navigation-material:0.34.0'
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.ui:ui-graphics'
implementation 'androidx.compose.ui:ui-tooling-preview'
implementation 'androidx.compose.material3:material3'
implementation 'androidx.compose.material:material'
implementation 'androidx.navigation:navigation-runtime-ktx:2.8.4'
implementation 'androidx.navigation:navigation-compose:2.8.4'
implementation 'androidx.navigation:navigation-runtime-ktx:2.8.9'
implementation 'androidx.navigation:navigation-compose:2.8.9'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
androidTestImplementation platform('androidx.compose:compose-bom:2024.11.00')
androidTestImplementation platform('androidx.compose:compose-bom:2025.03.00')
androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
debugImplementation 'androidx.compose.ui:ui-tooling'
debugImplementation 'androidx.compose.ui:ui-test-manifest'
Expand All @@ -73,4 +75,16 @@ dependencies {
implementation platform('com.walletconnect:android-bom:1.35.2')
implementation("com.walletconnect:android-core")
implementation("com.walletconnect:walletconnect-modal")

implementation 'com.solanamobile:web3-solana:0.2.5'
implementation 'com.solanamobile:rpc-core:0.2.8'
implementation 'io.github.funkatronics:kborsh:0.1.0'
implementation 'io.github.funkatronics:multimult:0.2.3'

implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
implementation("io.ktor:ktor-client-core:2.3.4")
implementation("io.ktor:ktor-client-android:2.3.4")

implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("com.google.code.gson:gson:2.10.1")
}
23 changes: 15 additions & 8 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,32 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.CarteraExample"
tools:targetApi="31" >
tools:targetApi="31"
android:launchMode="singleTop">

<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>

<intent-filter>
<action android:name="android.intent.action.MAIN" />
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<category android:name="android.intent.category.LAUNCHER" />
<data
android:scheme="https"
android:host="v4-web-internal.vercel.app"
android:pathPrefix="/walletsegueCarteraExample" />

<data android:scheme="app"
android:host="kotlin-dapp-wc"
android:pathPrefix="/request" />
<data
android:scheme="https"
android:host="v4-web-internal.vercel.app"
android:pathPrefix="/phantomCarteraExample" />
</intent-filter>
</activity>
</application>
Expand All @@ -46,6 +52,7 @@
<package android:name="me.rainbow"/>
<package android:name="io.oneinch.android"/>
<package android:name="pro.huobi"/>
<package android:name="app.phantom"/>
</queries>

</manifest>
21 changes: 21 additions & 0 deletions app/src/main/java/exchange/dydx/carteraExample/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package exchange.dydx.carteraexample

import android.app.Application
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
Expand Down Expand Up @@ -29,6 +31,12 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

val action: String? = intent?.action
val data: Uri? = intent?.data
if (action == "android.intent.action.VIEW" && data != null) {
CarteraConfig.handleResponse(data)
}

val launcher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val uri = result.data?.data ?: return@registerForActivityResult
CarteraConfig.handleResponse(uri)
Expand Down Expand Up @@ -73,6 +81,19 @@ class MainActivity : ComponentActivity() {
}
}
}

override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
// must store the new intent unless getIntent()
// will return the old one
setIntent(intent)

val action: String? = intent.action
val data: Uri? = intent.data
if (action == "android.intent.action.VIEW" && data != null) {
CarteraConfig.handleResponse(data)
}
}
}

@Composable
Expand Down
149 changes: 108 additions & 41 deletions app/src/main/java/exchange/dydx/carteraExample/WalletListViewModel.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package exchange.dydx.carteraexample

import android.content.Context
import android.util.Log
import android.widget.Toast
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.solana.publickey.SolanaPublicKey
import com.solana.transaction.Message
import com.solana.transaction.Transaction
import exchange.dydx.cartera.CarteraConfig
import exchange.dydx.cartera.CarteraConstants
import exchange.dydx.cartera.CarteraProvider
Expand All @@ -20,7 +22,11 @@ import exchange.dydx.cartera.walletprovider.WalletRequest
import exchange.dydx.cartera.walletprovider.WalletStatusDelegate
import exchange.dydx.cartera.walletprovider.WalletStatusProtocol
import exchange.dydx.cartera.walletprovider.WalletTransactionRequest
import exchange.dydx.carteraexample.solana.SolanaInteractor
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import timber.log.Timber
import java.math.BigInteger

class WalletListViewModel(
Expand All @@ -40,7 +46,11 @@ class WalletListViewModel(
viewState.value = WalletList.WalletListState(
wallets = CarteraConfig.shared?.wallets ?: listOf(),
walletAction = { action: WalletList.WalletAction, wallet: Wallet?, useTestnet: Boolean, useModal: Boolean ->
val chainId: String = if (useTestnet) CarteraConstants.testnetChainId else "1"
val chainId: String = if (useTestnet) {
CarteraConstants.testnetChainId
} else {
"1"
}
when (action) {
WalletList.WalletAction.Connect -> {
testConnect(wallet, chainId, useModal)
Expand All @@ -55,7 +65,7 @@ class WalletListViewModel(
}

WalletList.WalletAction.SignTransaction -> {
testSendTransaction(wallet, chainId, useModal)
testSendTransaction(wallet, chainId, useTestnet, useModal)
}

WalletList.WalletAction.Disconnect -> {
Expand Down Expand Up @@ -127,10 +137,12 @@ class WalletListViewModel(
request = request,
message = "Test Message",
connected = { info ->
Log.d(tag(this@WalletListViewModel), "Connected to: ${info?.peerName ?: info?.address}")
Timber.tag(tag(this@WalletListViewModel))
.d("Connected to: ${info?.peerName ?: info?.address}")
},
status = { requireAppSwitching ->
Log.d(tag(this@WalletListViewModel), "Require app switching: $requireAppSwitching")
Timber.tag(tag(this@WalletListViewModel))
.d("Require app switching: $requireAppSwitching")
toastMessage("Please switch to the wallet app")
},
completion = { signature, error ->
Expand All @@ -146,8 +158,13 @@ class WalletListViewModel(
}

private fun testSignTypedData(wallet: Wallet?, chainId: String, useModal: Boolean) {
val dydxSign = EIP712DomainTypedDataProvider(name = "dYdX", chainId = chainId.toInt())
dydxSign.message = message(action = "Sample Action", chainId = chainId.toInt())
val chainIdInt = chainId.toIntOrNull()
if (chainIdInt == null) {
toastMessage("Invalid chainId: $chainId, must be an integer")
return
}
val dydxSign = EIP712DomainTypedDataProvider(name = "dYdX", chainId = chainIdInt)
dydxSign.message = message(action = "Sample Action", chainId = chainIdInt)

val request = WalletRequest(wallet = wallet, address = null, chainId = chainId, context = context, useModal = useModal)
provider.sign(
Expand All @@ -157,7 +174,8 @@ class WalletListViewModel(
toastMessage("Connected to: ${info?.peerName ?: info?.address}")
},
status = { requireAppSwitching ->
Log.d(tag(this@WalletListViewModel), "Require app switching: $requireAppSwitching")
Timber.tag(tag(this@WalletListViewModel))
.d("Require app switching: $requireAppSwitching")
toastMessage("Please switch to the wallet app")
},
completion = { signature, error ->
Expand All @@ -172,50 +190,99 @@ class WalletListViewModel(
)
}

private fun testSendTransaction(wallet: Wallet?, chainId: String, useModal: Boolean) {
private fun testSendTransaction(wallet: Wallet?, chainId: String, useTestnet: Boolean, useModal: Boolean) {
val request = WalletRequest(wallet = wallet, address = null, chainId = chainId, context = context, useModal = useModal)
provider.connect(request) { info, error ->
if (error != null) {
toastWalletError(error)
} else {
val ethereumRequest = EthereumTransactionRequest(
fromAddress = info?.address ?: "0x00",
toAddress = "0x0000000000000000000000000000000000000000",
weiValue = BigInteger("1"),
data = "0x",
nonce = null,
gasPriceInWei = BigInteger("100000000"),
maxFeePerGas = null,
maxPriorityFeePerGas = null,
gasLimit = BigInteger("21000"),
chainId = chainId.toString(),
)
val request =
WalletTransactionRequest(walletRequest = request, ethereum = ethereumRequest)
provider.send(
request = request,
connected = { info ->
toastMessage("Connected to: ${info?.peerName ?: info?.address}")
},
status = { requireAppSwitching ->
Log.d(tag(this@WalletListViewModel), "Require app switching: $requireAppSwitching")
toastMessage("Please switch to the wallet app")
},

completion = { txHash, error ->
// delay for 1 second
Thread.sleep(1000)
if (error != null) {
toastWalletError(error)
val publicKey = info?.address
if (wallet?.id == "phantom-wallet" && publicKey != null) {
val interactor = if (useTestnet) {
SolanaInteractor(SolanaInteractor.devnetUrl)
} else {
SolanaInteractor(SolanaInteractor.mainnetUrl)
}
val scope = CoroutineScope(Dispatchers.Unconfined)
scope.launch {
val response = interactor.getRecentBlockhash()
val blockhash = response?.value?.blockhash
if (blockhash != null) {
// val sss = interactor.getBalance(publicKey = publicKey)
// print(sss)
// val ttt = interactor.getTokenBalance(publicKey = publicKey, tokenAddress = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v")
// print(ttt)

val memoInstruction = interactor.buildTestMemoTransaction(address = SolanaPublicKey.from(publicKey), memo = "Hello, Solana!")
val memoTxMessage = Message.Builder()
.addInstruction(memoInstruction) // Pass in instruction from previous step
.setRecentBlockhash(blockhash)
.build()
val unsignedTx = Transaction(memoTxMessage)
val request =
WalletTransactionRequest(
walletRequest = request,
ethereum = null,
solana = unsignedTx.serialize(),
)
CoroutineScope(Dispatchers.Main).launch {
doSendTransaction(request)
}
} else {
toastMessage("Transaction Hash: $txHash")
CoroutineScope(Dispatchers.Main).launch {
toastMessage("Error fetching blockhash")
}
}
},
)
}
} else {
val ethereumRequest = EthereumTransactionRequest(
fromAddress = info?.address ?: "0x00",
toAddress = "0x0000000000000000000000000000000000000000",
weiValue = BigInteger("1"),
data = "0x",
nonce = null,
gasPriceInWei = BigInteger("100000000"),
maxFeePerGas = null,
maxPriorityFeePerGas = null,
gasLimit = BigInteger("21000"),
chainId = chainId.toString(),
)
val request =
WalletTransactionRequest(
walletRequest = request,
ethereum = ethereumRequest,
solana = null,
)
doSendTransaction(request)
}
}
}
}

private fun doSendTransaction(request: WalletTransactionRequest) {
provider.send(
request = request,
connected = { info ->
toastMessage("Connected to: ${info?.peerName ?: info?.address}")
},
status = { requireAppSwitching ->
Timber.tag(tag(this@WalletListViewModel))
.d("Require app switching: $requireAppSwitching")
toastMessage("Please switch to the wallet app")
},

completion = { txHash, error ->
// delay for 1 second
Thread.sleep(1000)
if (error != null) {
toastWalletError(error)
} else {
toastMessage("Transaction Hash: $txHash")
}
},
)
}

override fun statusChanged(status: WalletStatusProtocol) {
status.connectedWallet?.address?.let {
toastMessage("Connected to: $it")
Expand Down
Loading