diff --git a/wallet-sdk/app/build.gradle.kts b/wallet-sdk/app/build.gradle.kts
index 0222ea7..ce8ba0f 100644
--- a/wallet-sdk/app/build.gradle.kts
+++ b/wallet-sdk/app/build.gradle.kts
@@ -1,11 +1,18 @@
+import java.util.Properties
+
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.jetbrains.kotlin.android)
+ id("kotlin-kapt")
}
android {
namespace = "com.snphone.snwalletsdk"
- compileSdk = 34
+ compileSdk = 35
+
+ buildFeatures {
+ buildConfig = true
+ }
defaultConfig {
applicationId = "com.snphone.snwalletsdk"
@@ -15,6 +22,14 @@ android {
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+
+ val properties = Properties()
+ properties.load(project.rootProject.file("local.properties").inputStream())
+ buildConfigField("String", "RPC_URL", "\"${properties.getProperty("RPC_URL")}\"")
+ buildConfigField("String", "publicAddress", "\"${properties.getProperty("publicAddress")}\"")
+ buildConfigField("String", "privateKey", "\"${properties.getProperty("privateKey")}\"")
+ buildConfigField("String", "recepientAddress", "\"${properties.getProperty("recepientAddress")}\"")
+
}
buildTypes {
@@ -27,20 +42,38 @@ android {
}
}
compileOptions {
- sourceCompatibility = JavaVersion.VERSION_1_8
- targetCompatibility = JavaVersion.VERSION_1_8
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
- jvmTarget = "1.8"
+ jvmTarget = "17"
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion = "1.5.7"
}
}
dependencies {
+ implementation("com.swmansion.starknet:starknet:0.13.0@aar"){
+ isTransitive = true
+ }
+
+ //Room
+ implementation(libs.androidx.room.runtime)
+ implementation(libs.androidx.activity)
+ implementation(libs.androidx.constraintlayout)
+ kapt(libs.androidx.room.compiler)
+ implementation(libs.androidx.room.ktx)
+
+ //crypto-security
+ implementation(libs.androidx.security.crypto.v110alpha06)
+
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
+ testImplementation(libs.kotlinx.coroutines.test)
}
\ No newline at end of file
diff --git a/wallet-sdk/app/src/main/AndroidManifest.xml b/wallet-sdk/app/src/main/AndroidManifest.xml
index 5a259f4..7bc602a 100644
--- a/wallet-sdk/app/src/main/AndroidManifest.xml
+++ b/wallet-sdk/app/src/main/AndroidManifest.xml
@@ -2,6 +2,8 @@
+
+
+ tools:targetApi="31">
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/wallet-sdk/app/src/main/java/com/snphone/snwalletsdk/MainActivity.kt b/wallet-sdk/app/src/main/java/com/snphone/snwalletsdk/MainActivity.kt
new file mode 100644
index 0000000..b40ffb9
--- /dev/null
+++ b/wallet-sdk/app/src/main/java/com/snphone/snwalletsdk/MainActivity.kt
@@ -0,0 +1,67 @@
+package com.snphone.snwalletsdk
+
+import android.content.Context
+import android.os.Bundle
+import android.util.Log
+import androidx.activity.enableEdgeToEdge
+import androidx.appcompat.app.AppCompatActivity
+import androidx.lifecycle.lifecycleScope
+import com.snphone.snwalletsdk.utils.StarknetClient
+import com.swmansion.starknet.account.StandardAccount
+import com.swmansion.starknet.data.types.Felt
+import com.swmansion.starknet.data.types.StarknetChainId
+import com.swmansion.starknet.extensions.toFelt
+import com.swmansion.starknet.provider.rpc.JsonRpcProvider
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+class MainActivity : AppCompatActivity() {
+
+ init {
+ instance = this
+ }
+
+ companion object {
+ private var instance: MainActivity? = null
+
+ fun applicationContext() : Context {
+ return instance!!.applicationContext
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+ instance = this
+ setContentView(R.layout.activity_main)
+
+ val starknetClient = StarknetClient(BuildConfig.RPC_URL)
+ lifecycleScope.launch(Dispatchers.IO) {
+
+ //Deploy account
+ //val (privateKey, accountAddress) = deployAccount()
+
+ //GetBalance
+ //val balance = getBalance(accountAddress)
+ val addressUser = BuildConfig.publicAddress.toFelt
+ val privateKey = BuildConfig.privateKey.toFelt
+
+ val account = StandardAccount(
+ address = addressUser,
+ privateKey = privateKey,
+ provider = JsonRpcProvider(BuildConfig.RPC_URL),
+ chainId = StarknetChainId.SEPOLIA,
+ )
+ val recipientAddress = Felt.fromHex(BuildConfig.recepientAddress)
+ try {
+ val amount = StarknetClient.toUint256Amount(1.toString())
+ val address = starknetClient.transferFunds(account,recipientAddress,amount)
+ Log.d("MainActivity", "transaction id: $address")
+
+ }catch (e: Exception){
+ Log.d("MainActivity", "Error in amount: $e")
+
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/wallet-sdk/app/src/main/java/com/snphone/snwalletsdk/SNWalletSDK.kt b/wallet-sdk/app/src/main/java/com/snphone/snwalletsdk/SNWalletSDK.kt
index 8bbdaa7..8a66d40 100644
--- a/wallet-sdk/app/src/main/java/com/snphone/snwalletsdk/SNWalletSDK.kt
+++ b/wallet-sdk/app/src/main/java/com/snphone/snwalletsdk/SNWalletSDK.kt
@@ -1,4 +1,32 @@
package com.snphone.snwalletsdk
-class SNWalletSDK {
+import android.content.Context
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.lifecycle.lifecycleScope
+import com.snphone.snwalletsdk.utils.StarknetClient
+import kotlinx.coroutines.launch
+
+class SNWalletSDK : ComponentActivity() {
+ init {
+ instance = this
+ }
+
+ companion object {
+ private var instance: SNWalletSDK? = null
+
+ fun applicationContext() : Context {
+ return instance!!.applicationContext
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ instance = this
+
+ val starknetClient = StarknetClient(BuildConfig.RPC_URL)
+ lifecycleScope.launch {
+ starknetClient.deployAccount()
+ }
+ }
}
\ No newline at end of file
diff --git a/wallet-sdk/app/src/main/java/com/snphone/snwalletsdk/utils/Keystore.kt b/wallet-sdk/app/src/main/java/com/snphone/snwalletsdk/utils/Keystore.kt
new file mode 100644
index 0000000..825e3d1
--- /dev/null
+++ b/wallet-sdk/app/src/main/java/com/snphone/snwalletsdk/utils/Keystore.kt
@@ -0,0 +1,64 @@
+package com.snphone.snwalletsdk.utils
+
+import android.util.Log
+import androidx.security.crypto.EncryptedSharedPreferences
+import androidx.security.crypto.MasterKey
+import com.snphone.snwalletsdk.MainActivity
+import com.snphone.snwalletsdk.SNWalletSDK
+import java.io.IOException
+import java.security.GeneralSecurityException
+
+class Keystore() {
+
+ private val tag = "Keystore"
+ private val context = MainActivity.applicationContext()
+ fun storeData(message: String) {
+ try {
+ val masterKey = MasterKey.Builder(context)
+ .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
+ .build()
+
+ val sharedPreferences = EncryptedSharedPreferences.create(
+ context,
+ "my_encrypted_prefs",
+ masterKey,
+ EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
+ EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
+ )
+
+ val editor = sharedPreferences.edit()
+ editor.putString("key", message)
+ editor.apply()
+
+ } catch (e: GeneralSecurityException) {
+ Log.e(tag, "Security exception while storing data: ${e.message}", e)
+ } catch (e: IOException) {
+ Log.e(tag, "I/O exception while storing data: ${e.message}", e)
+ }
+ }
+
+ fun retrieveData(): String {
+ try {
+ val masterKey = MasterKey.Builder(context)
+ .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
+ .build()
+
+ val sharedPreferences = EncryptedSharedPreferences.create(
+ context,
+ "my_encrypted_prefs",
+ masterKey,
+ EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
+ EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
+ )
+
+ return sharedPreferences.getString("key", "") ?: ""
+
+ } catch (e: GeneralSecurityException) {
+ Log.e(tag, "Security exception while retrieving data: ${e.message}", e)
+ return ""
+ } catch (e: IOException) {
+ Log.e(tag, "I/O exception while retrieving data: ${e.message}", e)
+ return ""
+ }
+ }
+}
\ No newline at end of file
diff --git a/wallet-sdk/app/src/main/java/com/snphone/snwalletsdk/utils/StarknetClient.kt b/wallet-sdk/app/src/main/java/com/snphone/snwalletsdk/utils/StarknetClient.kt
new file mode 100644
index 0000000..884a1f6
--- /dev/null
+++ b/wallet-sdk/app/src/main/java/com/snphone/snwalletsdk/utils/StarknetClient.kt
@@ -0,0 +1,180 @@
+package com.snphone.snwalletsdk.utils
+
+import android.util.Log
+import com.swmansion.starknet.account.Account
+import com.swmansion.starknet.account.StandardAccount
+import com.swmansion.starknet.crypto.StarknetCurve
+import com.swmansion.starknet.data.ContractAddressCalculator
+import com.swmansion.starknet.data.types.Call
+import com.swmansion.starknet.data.types.DeployAccountResponse
+import com.swmansion.starknet.data.types.Felt
+import com.swmansion.starknet.data.types.StarknetChainId
+import com.swmansion.starknet.data.types.Uint256
+import com.swmansion.starknet.extensions.toFelt
+import com.swmansion.starknet.provider.rpc.JsonRpcProvider
+import com.swmansion.starknet.signer.StarkCurveSigner
+import kotlinx.coroutines.future.await
+import java.math.BigDecimal
+import java.math.BigInteger
+
+class StarknetClient(rpcUrl: String) {
+
+ private val ETH_ERC20_ADDRESS = "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d"
+ private val ACCOUNTCLASSHASH = "0x061dac032f228abef9c6626f995015233097ae253a7f72d68552db02f2971b8f"
+
+ private val provider = JsonRpcProvider(rpcUrl)
+ private val tag = "StarknetClient"
+ private lateinit var keystore: Keystore
+
+ fun deployAccount(): Pair {
+ // Predefined values for account creation
+ keystore = Keystore()
+ val randomPrivateKey = StandardAccount.generatePrivateKey()
+ keystore.storeData(randomPrivateKey.value.toString()) // save the key generated
+ val data = keystore.retrieveData() // retrieve it to generate public key
+ val privateKey = BigInteger(data).toFelt
+
+ val publicKey = StarknetCurve.getPublicKey(privateKey)
+
+ Log.d(tag, "Private key: ${privateKey.hexString()}")
+ Log.d(tag,"Public key: ${publicKey.hexString()}")
+
+ val signer = StarkCurveSigner(privateKey)
+
+ val salt = Felt.ONE
+
+ val calldata = listOf(publicKey)
+
+ val accountContractClassHash = Felt.fromHex(ACCOUNTCLASSHASH)
+
+ val address = ContractAddressCalculator.calculateAddressFromHash(
+ classHash = accountContractClassHash,
+ calldata = calldata,
+ salt = salt
+ )
+
+ val account = StandardAccount(
+ address = address,
+ signer = signer,
+ provider = provider,
+ chainId = StarknetChainId.SEPOLIA,
+ )
+
+
+ // Fund address first with Eth
+ // how to approach this, we need the user to fund the address with eth
+
+ val payload = account.signDeployAccountV1(
+ classHash = accountContractClassHash,
+ calldata = listOf(publicKey),
+ salt = salt,
+ // 10*fee from estimate deploy account fee
+ maxFee = Felt.fromHex("0x219d2e16ea"),
+ )
+
+ val res: DeployAccountResponse = provider.deployAccount(payload).send()
+ Log.d(tag, "Account deployed successfully: $res")
+
+ val deployedAddress = res.address?.hexString() ?: ""
+ return Pair(privateKey.hexString(), deployedAddress)
+ }
+
+ suspend fun getBalance(accountAddress: Felt): String {
+ // Create a call to Starknet ERC-20 ETH contract
+ val call = Call(
+ contractAddress = ETH_ERC20_ADDRESS.toFelt,
+ entrypoint = "balanceOf",
+ calldata = listOf(accountAddress), // calldata is List
+ )
+
+ // Create a Request object which has to be executed in synchronous or asynchronous way
+ val request = provider.callContract(call)
+
+ // Execute a Request asynchronously and handle potential errors
+ return try {
+ val future = request.sendAsync()
+ val response = future.await() // Await the completion without blocking
+
+ // Validate response size
+ require(response.size >= 2) { "Response size is less than 2; cannot construct Uint256" }
+
+ // Output value's type is UInt256 and is represented by two Felt values
+ val balance = Uint256(
+ low = response[0],
+ high = response[1],
+ )
+
+ convertToTokenBalance(balance)
+ } catch (e: Exception) {
+ // Handle or log the exception as needed
+ throw RuntimeException("Failed to fetch balance $e", e)
+ }
+ }
+
+
+ suspend fun transferFunds(account: Account, toAddress: Felt, amount: Uint256): Felt {
+ val erc20ContractAddress = Felt.fromHex(ETH_ERC20_ADDRESS)
+ val calldata = listOf(toAddress) + amount.toCalldata()
+ val call = Call(
+ contractAddress = erc20ContractAddress,
+ entrypoint = "transfer",
+ calldata = calldata,
+ )
+ val request = account.executeV3(call)
+ val future = request.sendAsync()
+ val response = future.await()
+ return response.transactionHash
+ }
+
+ private fun convertToTokenBalance(balance: Uint256): String {
+ // Convert the Uint256 to BigInteger for easier manipulation
+ val bigIntBalance = balance.value
+
+ // Use fixed divisor for 18 decimals (10^18)
+ val divisor = BigInteger.TEN.pow(18)
+
+ // Divide the balance by the divisor to get the whole number part
+ val whole = bigIntBalance.divide(divisor)
+
+ // Get the fractional part by taking the remainder
+ val fractional = bigIntBalance.remainder(divisor)
+
+ // Convert fractional to string and pad with leading zeros if necessary
+ var fractionalStr = fractional.toString()
+ // Pad with leading zeros to match 18 decimal places
+ while (fractionalStr.length < 18) {
+ fractionalStr = "0$fractionalStr"
+ }
+
+ // Trim trailing zeros from fractional part
+ fractionalStr = fractionalStr.trimEnd('0')
+
+ // Construct the final string
+ return if (fractionalStr.isEmpty()) {
+ whole.toString()
+ } else {
+ "$whole.$fractionalStr"
+ }
+ }
+
+ companion object {
+ fun toUint256Amount(amount: String, decimals: Int = 18): Uint256 {
+ require(decimals >= 0) { "Decimals must be non-negative" }
+
+ return try {
+ val bigDecimalAmount = BigDecimal(amount)
+ require(bigDecimalAmount >= BigDecimal.ZERO) { "Amount must be non-negative" }
+
+ // Check if decimal places don't exceed the specified decimals
+ val scale = bigDecimalAmount.scale()
+ require(scale <= decimals) { "Too many decimal places. Maximum allowed: $decimals" }
+
+ val scaledAmount = bigDecimalAmount.multiply(BigDecimal.TEN.pow(decimals)).toBigInteger()
+ val hexValue = "0x" + scaledAmount.toString(16)
+ Uint256.fromHex(hexValue)
+ } catch (e: NumberFormatException) {
+ throw IllegalArgumentException("Invalid amount format: $amount")
+ }
+ }
+ }
+}
diff --git a/wallet-sdk/app/src/main/res/layout/activity_main.xml b/wallet-sdk/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..9affce0
--- /dev/null
+++ b/wallet-sdk/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,10 @@
+
+
+
+
\ No newline at end of file
diff --git a/wallet-sdk/gradle/libs.versions.toml b/wallet-sdk/gradle/libs.versions.toml
index f31eb0e..80bf62c 100644
--- a/wallet-sdk/gradle/libs.versions.toml
+++ b/wallet-sdk/gradle/libs.versions.toml
@@ -1,21 +1,34 @@
[versions]
agp = "8.5.2"
-kotlin = "1.9.0"
+kotlin = "2.0.0"
coreKtx = "1.15.0"
junit = "4.13.2"
junitVersion = "1.2.1"
espressoCore = "3.6.1"
appcompat = "1.7.0"
+kotlinxCoroutinesTest = "1.7.2"
material = "1.12.0"
+roomRuntime = "2.6.1"
+securityCryptoVersion = "1.1.0-alpha06"
+activity = "1.9.3"
+constraintlayout = "2.1.4"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomRuntime" }
+androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomRuntime" }
+androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomRuntime" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
+kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutinesTest" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
+androidx-security-crypto-v110alpha06 = { module = "androidx.security:security-crypto", version.ref = "securityCryptoVersion" }
+androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
+androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
+
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }