diff --git a/Cargo.lock b/Cargo.lock index 9ab191e0..dd7907c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4930,6 +4930,7 @@ name = "zk-commit-mobile" version = "0.1.0" dependencies = [ "plonky2", + "serde", "serde_json", "thiserror", "uniffi", diff --git a/crates/zk-commit-core/src/prover.rs b/crates/zk-commit-core/src/prover.rs index f647b0f5..6bfe3cb5 100644 --- a/crates/zk-commit-core/src/prover.rs +++ b/crates/zk-commit-core/src/prover.rs @@ -1,5 +1,3 @@ -use std::fs::File; - use anyhow::Result; use log::Level; use plonky2::{ @@ -17,6 +15,7 @@ use plonky2::{ }; use plonky2_field::{goldilocks_field::GoldilocksField, types::PrimeField64}; use serde::{Deserialize, Serialize}; +use std::{fs::File, io::Write}; use crate::{ circuits::{ @@ -25,8 +24,10 @@ use crate::{ }, claim_execution::{get_claim_proving_inputs, Claim}, commitment_tree::CommitmentTree, + groth16::Cbn128, types::{C, F}, utils::AmountSecretPairing, + verifier::{generate_proof_base64, generate_verifier_config}, }; use thiserror::Error; @@ -43,6 +44,9 @@ pub enum ProofError { #[error("Unknown error")] Unknown, + + #[error("Unable to generate json: {0}")] + GenerateJsonError(#[from] anyhow::Error), } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -68,23 +72,16 @@ pub fn generate_proof_of_claim( eth_addr: [u8; 20], ) -> Result<(), ProofError> { // Create claim from inputs - let proof_res = get_claim_proof(amount, secret, index, &siblings, root, eth_addr); - // If proof failed then return error - if proof_res.is_err() { - return Err(ProofError::InvalidProof); - } + let (proof, _, _) = + get_claim_proof::(amount, secret, index, &siblings, root, eth_addr)?; - let (proof, _, _) = proof_res.expect("Proof failed"); + let conf = generate_verifier_config(&proof)?; - let write_res = write_to_file( - path, - MobileProofData { proof_with_pis: proof.clone(), merkle_depth: siblings.len() }, - ); + let proof_json = generate_proof_base64(&proof, &conf)?; - if write_res.is_err() { - return Err(ProofError::FileReadError); - } + let mut file = File::create(path)?; + file.write_all(proof_json.as_bytes())?; return Ok(()); } diff --git a/crates/zk-commit-mobile/Cargo.toml b/crates/zk-commit-mobile/Cargo.toml index a006aad9..df8ae25a 100644 --- a/crates/zk-commit-mobile/Cargo.toml +++ b/crates/zk-commit-mobile/Cargo.toml @@ -13,6 +13,7 @@ uniffi = { version = "0.28.0", features = ["cli"] } thiserror = { workspace = true } zk-commit-core = { path = "../zk-commit-core" } plonky2 = { workspace = true } +serde = { workspace = true } serde_json = { workspace = true } [lib] diff --git a/crates/zk-commit-mobile/android/app/build.gradle.kts b/crates/zk-commit-mobile/android/app/build.gradle.kts index 2ff19ac4..d356c22d 100644 --- a/crates/zk-commit-mobile/android/app/build.gradle.kts +++ b/crates/zk-commit-mobile/android/app/build.gradle.kts @@ -46,7 +46,7 @@ android { } packaging { resources { - excludes += "/META-INF/{AL2.0,LGPL2.1}" + excludes += "/META-INF/{AL2.0,LGPL2.1,DISCLAIMER}" } } } diff --git a/crates/zk-commit-mobile/android/app/proguard-rules.pro b/crates/zk-commit-mobile/android/app/proguard-rules.pro index 7c1bfccc..2c77d138 100644 --- a/crates/zk-commit-mobile/android/app/proguard-rules.pro +++ b/crates/zk-commit-mobile/android/app/proguard-rules.pro @@ -38,3 +38,5 @@ -keepclassmembers class * extends org.web3j.abi.datatypes.NumericType { (...); } +-keep class org.web3j.** { *; } +-dontwarn groovy.lang.GroovyShell diff --git a/crates/zk-commit-mobile/android/app/src/main/kotlin/com/okx/zkcommitmobile/Web3jManager.kt b/crates/zk-commit-mobile/android/app/src/main/kotlin/com/okx/zkcommitmobile/Web3jManager.kt index 1c5b29a6..a296d204 100644 --- a/crates/zk-commit-mobile/android/app/src/main/kotlin/com/okx/zkcommitmobile/Web3jManager.kt +++ b/crates/zk-commit-mobile/android/app/src/main/kotlin/com/okx/zkcommitmobile/Web3jManager.kt @@ -2,6 +2,7 @@ package com.okx.zkcommitmobile +import androidx.compose.ui.util.fastMapIndexed import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import com.okx.zkcommitmobile.data.Chain @@ -16,10 +17,12 @@ import java.math.BigInteger import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map @@ -32,11 +35,17 @@ import kotlinx.coroutines.reactive.collect import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import org.java_websocket.exceptions.WebsocketNotConnectedException +import org.web3j.abi.EventEncoder +import org.web3j.abi.EventValues +import org.web3j.abi.FunctionReturnDecoder +import org.web3j.abi.datatypes.Event import org.web3j.protocol.Web3j import org.web3j.protocol.Web3jService +import org.web3j.protocol.core.DefaultBlockParameter import org.web3j.protocol.core.DefaultBlockParameterName import org.web3j.protocol.core.Request import org.web3j.protocol.core.Response +import org.web3j.protocol.core.methods.request.EthFilter import org.web3j.protocol.core.methods.response.TransactionReceipt import org.web3j.protocol.http.HttpService import org.web3j.protocol.websocket.WebSocketService @@ -172,6 +181,30 @@ class Web3jManager( get() = web3jContext.flatMapLatest { it.flow { transactionFlowable() } } .onEach { Log.i("Transaction: ${it.hash}") } + fun getEventFlow(address: String, event: Event): Flow { + val topic = EventEncoder.encode(event) + return web3jContext.flatMapLatest { web3jContext -> + if (web3jContext.service is WebSocketService) { + flow { logsNotifications(listOf(address), listOf(topic)) } + .map { it.params.result } + .map { extractEventParameters(event, it.topics, it.data) } + } else { + val blockNumber = + send { + ethBlockNumber() + }.onErrorReturn { return@flatMapLatest emptyFlow() }.blockNumber + val ethFilter = EthFilter( + DefaultBlockParameter.valueOf(blockNumber), + DefaultBlockParameterName.LATEST, + address + ) + ethFilter.addSingleTopic(topic) + flow { ethLogFlowable(ethFilter) } + .map { extractEventParameters(event, it.topics, it.data) } + } + } + } + suspend fun waitForTransactionReceipt(transactionHash: String) = web3jContext.flatMapLatest { it.processor.waitForTransactionReceipt(transactionHash) transactionReceipts @@ -186,3 +219,19 @@ class Web3jManager( return Result.success(balance) } } + +private fun extractEventParameters( + event: Event, + topics: List?, + data: String +): EventValues? { + val encodedEventSignature = EventEncoder.encode(event) + if (topics.isNullOrEmpty() || topics[0] != encodedEventSignature) { + return null + } + val nonIndexedValues = FunctionReturnDecoder.decode(data, event.nonIndexedParameters) + val indexedValues = event.indexedParameters.fastMapIndexed { index, indexedParameter -> + FunctionReturnDecoder.decodeIndexedValue(topics[index + 1], indexedParameter) + } + return EventValues(indexedValues, nonIndexedValues) +} diff --git a/crates/zk-commit-mobile/android/app/src/main/kotlin/com/okx/zkcommitmobile/ZkCommitMobileViewModel.kt b/crates/zk-commit-mobile/android/app/src/main/kotlin/com/okx/zkcommitmobile/ZkCommitMobileViewModel.kt index fdbe8a25..0beed7d2 100644 --- a/crates/zk-commit-mobile/android/app/src/main/kotlin/com/okx/zkcommitmobile/ZkCommitMobileViewModel.kt +++ b/crates/zk-commit-mobile/android/app/src/main/kotlin/com/okx/zkcommitmobile/ZkCommitMobileViewModel.kt @@ -31,8 +31,10 @@ import com.okx.zkcommitmobile.data.send import com.okx.zkcommitmobile.data.totalAmount import com.okx.zkcommitmobile.data.trySend import com.okx.zkcommitmobile.data.withClaimStatus +import com.okx.zkcommitmobile.network.RapidsnarkService +import com.okx.zkcommitmobile.network.StatusResponse import com.okx.zkcommitmobile.network.ZkCommitService -import com.okx.zkcommitmobile.network.wrapGroth16 +import com.okx.zkcommitmobile.network.inputPlonky2 import com.okx.zkcommitmobile.nfc.firstTextOrNull import com.okx.zkcommitmobile.nfc.onTagDiscovered import com.okx.zkcommitmobile.uniffi.AmountSecretPairing @@ -53,11 +55,11 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -82,6 +84,7 @@ class ZkCommitMobileViewModel( private val json: Json, private val preferences: DataStore, private val zkCommitService: ZkCommitService, + private val rapidsnarkService: RapidsnarkService, private val defaultDispatcher: CoroutineDispatcher ) : ViewModel(), NfcAdapter.ReaderCallback { @@ -316,8 +319,7 @@ class ZkCommitMobileViewModel( viewModelScope.launch { val from = getFrom(action = "test transfer") ?: return@launch launch { - val transfer = testERC20.transfer.onEach { Log.d("testTransfer: transfer=$it") } - .first { it.to == from } + val transfer = testERC20.transfer.first { it.to == from } Log.d("testTransfer: first=$transfer") } testERC20.transfer(Address(from), Uint256(BigInteger.ONE)).onSuccess { @@ -553,34 +555,61 @@ class ZkCommitMobileViewModel( } private suspend fun claimInternal(proofFile: File, from: String): Result = runCatching { - zkCommitService.wrapGroth16(proofFile) +// zkCommitService.wrapGroth16(proofFile) + rapidsnarkService.inputPlonky2(proofFile) }.onFailure { Log.e(it, "Failed to wrap Groth16") _messages.send("Failed to wrap Groth16: ${it.localizedMessage}") return Result.failure(it) - }.suspendMapCatching { pwi -> - withContext(defaultDispatcher) { getClaimTokenCallData(pwi).toHexString() } - }.onFailure { - Log.e(it, "Failed to get claim token call data") - _messages.send("Failed to get claim token call data: ${it.localizedMessage}") - return Result.failure(it) }.fold( - onSuccess = { data -> - if (useWalletConnect()) { - sendTransactionWithWalletConnect( - from = from, - to = payCommitment.getAddress(), - data = data - ).map { Unit } - } else { - payCommitment.withdraw(data).onFailure { - Log.e(it, "Failed to withdraw") - _messages.send("Failed to withdraw: ${it.localizedMessage}") + onSuccess = { + suspendRunCatching { + val success: StatusResponse.Success + while (true) { + when (val statusResponse = rapidsnarkService.status()) { + is StatusResponse.Success -> { + success = statusResponse + break + } + + is StatusResponse.Busy -> delay(1000) + } } + success } }, onFailure = { Result.failure(it) } ) +// .suspendMapCatching { pwi -> +// withContext(defaultDispatcher) { getClaimTokenCallData(pwi).toHexString() } +// } + .suspendMapCatching { success -> + withContext(defaultDispatcher) { + val pwi = json.encodeToString(success) + getClaimTokenCallData(pwi).toHexString() + } + } + .onFailure { + Log.e(it, "Failed to get claim token call data") + _messages.send("Failed to get claim token call data: ${it.localizedMessage}") + return Result.failure(it) + }.fold( + onSuccess = { data -> + if (useWalletConnect()) { + sendTransactionWithWalletConnect( + from = from, + to = payCommitment.getAddress(), + data = data + ).map { Unit } + } else { + payCommitment.withdraw(data).onFailure { + Log.e(it, "Failed to withdraw") + _messages.send("Failed to withdraw: ${it.localizedMessage}") + } + } + }, + onFailure = { Result.failure(it) } + ) private suspend fun generateProofOfClaim( amount: ULong, diff --git a/crates/zk-commit-mobile/android/app/src/main/kotlin/com/okx/zkcommitmobile/contract/TestERC20.kt b/crates/zk-commit-mobile/android/app/src/main/kotlin/com/okx/zkcommitmobile/contract/TestERC20.kt index 13ba1c98..ae7172fb 100644 --- a/crates/zk-commit-mobile/android/app/src/main/kotlin/com/okx/zkcommitmobile/contract/TestERC20.kt +++ b/crates/zk-commit-mobile/android/app/src/main/kotlin/com/okx/zkcommitmobile/contract/TestERC20.kt @@ -7,7 +7,6 @@ import androidx.datastore.preferences.core.Preferences import com.okx.zkcommitmobile.ContractManager import com.okx.zkcommitmobile.Web3jManager import com.okx.zkcommitmobile.data.chain -import com.okx.zkcommitmobile.onErrorReturn import java.math.BigInteger import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.first @@ -16,6 +15,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.onEach import org.web3j.abi.EventEncoder +import org.web3j.abi.EventValues import org.web3j.abi.FunctionEncoder import org.web3j.abi.FunctionReturnDecoder import org.web3j.abi.Utils @@ -27,11 +27,6 @@ import org.web3j.abi.datatypes.Type import org.web3j.abi.datatypes.Utf8String import org.web3j.abi.datatypes.generated.Uint256 import org.web3j.abi.datatypes.generated.Uint8 -import org.web3j.protocol.core.DefaultBlockParameter -import org.web3j.protocol.core.DefaultBlockParameterName -import org.web3j.protocol.core.methods.request.EthFilter -import org.web3j.protocol.core.methods.response.Log -import org.web3j.tx.Contract import timber.log.Timber class TestERC20( @@ -114,31 +109,22 @@ class TestERC20( } val transfer - get() = addresses.mapNotNull { address -> - val blockNumber = web3jManager.send { ethBlockNumber() } - .onErrorReturn { return@mapNotNull null }.blockNumber - EthFilter( - DefaultBlockParameter.valueOf(blockNumber), - DefaultBlockParameterName.LATEST, - address.value - ).apply { addSingleTopic(encodedTransfer) } - }.flatMapLatest { ethFilter -> web3jManager.flow { ethLogFlowable(ethFilter) } } - .mapNotNull { log -> - val eventValues = Contract.staticExtractEventParameters(Transfer, log) - eventValues?.let { - TransferEventResponse( - log = log, - from = it.indexedValues[0].value as String, - to = it.indexedValues[1].value as String, - value = it.nonIndexedValues[0].value as BigInteger - ) - } + get() = addresses.flatMapLatest { address -> + web3jManager.getEventFlow(address.value, Transfer) + }.mapNotNull { eventValues -> + eventValues?.let { + TransferEventResponse( + eventValues = it, + from = it.indexedValues[0].value as String, + to = it.indexedValues[1].value as String, + value = it.nonIndexedValues[0].value as BigInteger + ) } - .onEach { Log.i("Transfer: $it") } + }.onEach { Log.i("Transfer: $it") } } data class TransferEventResponse( - val log: Log, + val eventValues: EventValues, val from: String, val to: String, val value: BigInteger diff --git a/crates/zk-commit-mobile/android/app/src/main/kotlin/com/okx/zkcommitmobile/data/Preferences.kt b/crates/zk-commit-mobile/android/app/src/main/kotlin/com/okx/zkcommitmobile/data/Preferences.kt index 52b63145..037fb013 100644 --- a/crates/zk-commit-mobile/android/app/src/main/kotlin/com/okx/zkcommitmobile/data/Preferences.kt +++ b/crates/zk-commit-mobile/android/app/src/main/kotlin/com/okx/zkcommitmobile/data/Preferences.kt @@ -11,6 +11,9 @@ val Context.preferences by preferencesDataStore("zk-commit-mobile") val BaseUrlKey = stringPreferencesKey("baseUrl") const val DEFAULT_BASE_URL = "http://10.20.91.188:8080/" +val RapidsnarkUrlKey = stringPreferencesKey("rapidsnarkUrl") +const val DEFAULT_RAPIDSNARK_URL = "http://10.20.91.188:3000/" + val PrivateKeyKey = stringPreferencesKey("privateKey") const val DEFAULT_PRIVATE_KEY = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" @@ -24,8 +27,8 @@ enum class Chain( Anvil( chainName = "Local Anvil", defaultRpcUrl = "ws://10.20.91.188:8545/", - groth16VerifierAddress = "0x0165878A594ca255338adfa4d48449f69242Eb8F", - payCommitmentAddress = "0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0", + groth16VerifierAddress = "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", + payCommitmentAddress = "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", testERC20Address = "0x5FbDB2315678afecb367f032d93F642f64180aa3" ), XLayerTestnet( diff --git a/crates/zk-commit-mobile/android/app/src/main/kotlin/com/okx/zkcommitmobile/di/DIComponent.kt b/crates/zk-commit-mobile/android/app/src/main/kotlin/com/okx/zkcommitmobile/di/DIComponent.kt index 6ea4c894..04eacc4d 100644 --- a/crates/zk-commit-mobile/android/app/src/main/kotlin/com/okx/zkcommitmobile/di/DIComponent.kt +++ b/crates/zk-commit-mobile/android/app/src/main/kotlin/com/okx/zkcommitmobile/di/DIComponent.kt @@ -14,6 +14,7 @@ import com.okx.zkcommitmobile.contract.Groth16Verifier import com.okx.zkcommitmobile.contract.PayCommitment import com.okx.zkcommitmobile.contract.TestERC20 import com.okx.zkcommitmobile.data.preferences +import com.okx.zkcommitmobile.network.RapidsnarkService import com.okx.zkcommitmobile.network.ReplaceUrlInterceptor import com.okx.zkcommitmobile.network.ZkCommitService import java.time.Duration @@ -77,6 +78,12 @@ class DIComponentImpl(context: Context) : DIComponent { ) override val walletConnectManager by lazy { WalletConnectManager(accountManager) } private val zkCommitService by lazy { retrofit.create() } + private val rapidsnarkService by lazy { + retrofit.newBuilder() + .baseUrl(ReplaceUrlInterceptor.RAPIDSNARK_URL_PLACEHOLDER) + .build() + .create() + } val preferences by lazy { applicationContext.preferences } private val depositManager by lazy { DepositManager(preferences, json, applicationScope) } @@ -108,6 +115,7 @@ class DIComponentImpl(context: Context) : DIComponent { json = json, preferences = preferences, zkCommitService = zkCommitService, + rapidsnarkService = rapidsnarkService, defaultDispatcher = defaultDispatcher ) } diff --git a/crates/zk-commit-mobile/android/app/src/main/kotlin/com/okx/zkcommitmobile/network/RapidsnarkService.kt b/crates/zk-commit-mobile/android/app/src/main/kotlin/com/okx/zkcommitmobile/network/RapidsnarkService.kt new file mode 100644 index 00000000..19df9ea3 --- /dev/null +++ b/crates/zk-commit-mobile/android/app/src/main/kotlin/com/okx/zkcommitmobile/network/RapidsnarkService.kt @@ -0,0 +1,44 @@ +@file:OptIn(ExperimentalSerializationApi::class) + +package com.okx.zkcommitmobile.network + +import androidx.annotation.Keep +import java.io.File +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonClassDiscriminator +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.asRequestBody +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.Headers +import retrofit2.http.POST + +@Serializable +@JsonClassDiscriminator("status") +sealed interface StatusResponse { + @Serializable + @SerialName("busy") + data object Busy : StatusResponse + + @Serializable + @SerialName("success") + data class Success(val proof: String, val pubData: String) : StatusResponse +} + +@Keep +interface RapidsnarkService { + @GET("status") + @Headers("Accept: application/json") + suspend fun status(): StatusResponse + + @POST("input/plonky2") + suspend fun inputPlonky2(@Body body: RequestBody) +} + +suspend fun RapidsnarkService.inputPlonky2(file: File) { + val requestBody = file.asRequestBody("application/json".toMediaType()) + inputPlonky2(requestBody) +} diff --git a/crates/zk-commit-mobile/android/app/src/main/kotlin/com/okx/zkcommitmobile/network/ReplaceUrlInterceptor.kt b/crates/zk-commit-mobile/android/app/src/main/kotlin/com/okx/zkcommitmobile/network/ReplaceUrlInterceptor.kt index 2f24e1d6..e69e2e3d 100644 --- a/crates/zk-commit-mobile/android/app/src/main/kotlin/com/okx/zkcommitmobile/network/ReplaceUrlInterceptor.kt +++ b/crates/zk-commit-mobile/android/app/src/main/kotlin/com/okx/zkcommitmobile/network/ReplaceUrlInterceptor.kt @@ -4,25 +4,60 @@ import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import com.okx.zkcommitmobile.data.BaseUrlKey import com.okx.zkcommitmobile.data.DEFAULT_BASE_URL +import com.okx.zkcommitmobile.data.DEFAULT_RAPIDSNARK_URL +import com.okx.zkcommitmobile.data.RapidsnarkUrlKey import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking import okhttp3.Interceptor +import okhttp3.Request import okhttp3.Response class ReplaceUrlInterceptor(private val preferences: DataStore) : Interceptor { companion object { const val BASE_URL_PLACEHOLDER = "http://base.url.placeholder/" + const val RAPIDSNARK_URL_PLACEHOLDER = "http://rapidsnark.url.placeholder/" } override fun intercept(chain: Interceptor.Chain): Response { val request = chain.request() val url = request.url.toString() - if (!url.startsWith(BASE_URL_PLACEHOLDER)) return chain.proceed(request) - val baseUrl = - runBlocking { preferences.data.first()[BaseUrlKey] }.takeIf { !it.isNullOrBlank() } - ?: DEFAULT_BASE_URL + + if (url.startsWith(BASE_URL_PLACEHOLDER)) { + return replaceUrl( + key = BaseUrlKey, + defaultUrl = DEFAULT_BASE_URL, + chain = chain, + request = request, + url = url, + placeholder = BASE_URL_PLACEHOLDER + ) + } + if (url.startsWith(RAPIDSNARK_URL_PLACEHOLDER)) { + return replaceUrl( + key = RapidsnarkUrlKey, + defaultUrl = DEFAULT_RAPIDSNARK_URL, + chain = chain, + request = request, + url = url, + placeholder = RAPIDSNARK_URL_PLACEHOLDER + ) + } + + return chain.proceed(request) + } + + private fun replaceUrl( + key: Preferences.Key, + defaultUrl: String, + chain: Interceptor.Chain, + request: Request, + url: String, + placeholder: String + ): Response { + val baseUrl = runBlocking { preferences.data.first()[key] }.takeIf { !it.isNullOrBlank() } + ?: defaultUrl return chain.proceed( - request.newBuilder().url(url.replaceFirst(BASE_URL_PLACEHOLDER, baseUrl)).build() + request.newBuilder().url(url.replaceFirst(placeholder, baseUrl)).build() ) } } diff --git a/crates/zk-commit-mobile/android/app/src/main/kotlin/com/okx/zkcommitmobile/ui/settings/RapidsnarkUrlItem.kt b/crates/zk-commit-mobile/android/app/src/main/kotlin/com/okx/zkcommitmobile/ui/settings/RapidsnarkUrlItem.kt new file mode 100644 index 00000000..3d32e8f7 --- /dev/null +++ b/crates/zk-commit-mobile/android/app/src/main/kotlin/com/okx/zkcommitmobile/ui/settings/RapidsnarkUrlItem.kt @@ -0,0 +1,32 @@ +package com.okx.zkcommitmobile.ui.settings + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.okx.zkcommitmobile.data.DEFAULT_RAPIDSNARK_URL +import com.okx.zkcommitmobile.data.RapidsnarkUrlKey + +@Composable +fun RapidsnarkUrlItem(modifier: Modifier = Modifier) { + StringItem( + key = RapidsnarkUrlKey, + headline = "Rapidsnark URL", + default = DEFAULT_RAPIDSNARK_URL, + enabled = true, + modifier = modifier, + onSave = { rapidsnarkUrl -> + var url = rapidsnarkUrl + if (url.isNullOrBlank()) { + remove(RapidsnarkUrlKey) + return@StringItem + } + + if (!url.startsWith("http://", ignoreCase = true)) { + url = "http://$url" + } + if (!url.endsWith("/")) { + url += "/" + } + this[RapidsnarkUrlKey] = url + } + ) +} diff --git a/crates/zk-commit-mobile/android/app/src/main/kotlin/com/okx/zkcommitmobile/ui/settings/SettingsScreen.kt b/crates/zk-commit-mobile/android/app/src/main/kotlin/com/okx/zkcommitmobile/ui/settings/SettingsScreen.kt index 33c3b4f8..5311001e 100644 --- a/crates/zk-commit-mobile/android/app/src/main/kotlin/com/okx/zkcommitmobile/ui/settings/SettingsScreen.kt +++ b/crates/zk-commit-mobile/android/app/src/main/kotlin/com/okx/zkcommitmobile/ui/settings/SettingsScreen.kt @@ -60,6 +60,7 @@ fun SettingsScreen( val useWalletConnect by useWalletConnectFlow.collectAsStateWithLifecycle(false) LazyColumn(contentPadding = innerPaddings) { item { BaseUrlItem() } + item { RapidsnarkUrlItem() } item { UseWalletConnectItem( useWalletConnect = useWalletConnect, diff --git a/crates/zk-commit-mobile/android/gradle/libs.versions.toml b/crates/zk-commit-mobile/android/gradle/libs.versions.toml index 5719dfbc..bde2100d 100644 --- a/crates/zk-commit-mobile/android/gradle/libs.versions.toml +++ b/crates/zk-commit-mobile/android/gradle/libs.versions.toml @@ -10,7 +10,7 @@ codeScanner = "16.1.0" composeBom = "2024.06.00" core = "1.13.1" #noinspection GradleDependency -coreVersion = "4.8.9-android" +coreVersion = "4.12.0" coroutines = "1.8.1" datastorePreferences = "1.1.1" desugarJdkLibs = "2.0.4" diff --git a/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp.xcodeproj/project.pbxproj b/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp.xcodeproj/project.pbxproj index 8405eb6a..9b95d83d 100644 --- a/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp.xcodeproj/project.pbxproj +++ b/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp.xcodeproj/project.pbxproj @@ -19,6 +19,8 @@ 4C1A0E172C466DED00A6B846 /* AppConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A0E162C466DED00A6B846 /* AppConfig.swift */; }; 4C5B5B0D2C216E3F00CC93F0 /* CreateDepositorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5B5B0C2C216E3F00CC93F0 /* CreateDepositorView.swift */; }; 4C5B5B0F2C216E4600CC93F0 /* ClaimListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5B5B0E2C216E4600CC93F0 /* ClaimListView.swift */; }; + 4C6934D82C50DD4A00E055DA /* LocalNetworkAuthorization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C6934D72C50DD4A00E055DA /* LocalNetworkAuthorization.swift */; }; + 4C6934DA2C51FF3E00E055DA /* NFCDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C6934D92C51FF3E00E055DA /* NFCDelegate.swift */; }; 4CCAFA142C325715008380E3 /* ClaimManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCAFA132C325715008380E3 /* ClaimManager.swift */; }; 4CCAFA1A2C328059008380E3 /* PacketItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCAFA192C328059008380E3 /* PacketItem.swift */; }; 4CCAFA1C2C33DCB2008380E3 /* ERC20View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCAFA1B2C33DCB2008380E3 /* ERC20View.swift */; }; @@ -79,6 +81,8 @@ 4C1A0E162C466DED00A6B846 /* AppConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConfig.swift; sourceTree = ""; }; 4C5B5B0C2C216E3F00CC93F0 /* CreateDepositorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateDepositorView.swift; sourceTree = ""; }; 4C5B5B0E2C216E4600CC93F0 /* ClaimListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClaimListView.swift; sourceTree = ""; }; + 4C6934D72C50DD4A00E055DA /* LocalNetworkAuthorization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalNetworkAuthorization.swift; sourceTree = ""; }; + 4C6934D92C51FF3E00E055DA /* NFCDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NFCDelegate.swift; sourceTree = ""; }; 4CCAFA132C325715008380E3 /* ClaimManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClaimManager.swift; sourceTree = ""; }; 4CCAFA192C328059008380E3 /* PacketItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketItem.swift; sourceTree = ""; }; 4CCAFA1B2C33DCB2008380E3 /* ERC20View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ERC20View.swift; sourceTree = ""; }; @@ -189,6 +193,8 @@ 4C06BD592C2E5E4C00D52234 /* WalletService.swift */, 4C06BD5D2C2E8B2100D52234 /* DepositStorage.swift */, 4CCAFA132C325715008380E3 /* ClaimManager.swift */, + 4C6934D72C50DD4A00E055DA /* LocalNetworkAuthorization.swift */, + 4C6934D92C51FF3E00E055DA /* NFCDelegate.swift */, ); path = Service; sourceTree = ""; @@ -371,6 +377,7 @@ 4C5B5B0F2C216E4600CC93F0 /* ClaimListView.swift in Sources */, 4CCAFA1C2C33DCB2008380E3 /* ERC20View.swift in Sources */, 4CD9EF0D2C3538DB007327B3 /* PacketQRCode.swift in Sources */, + 4C6934D82C50DD4A00E055DA /* LocalNetworkAuthorization.swift in Sources */, 4C5B5B0D2C216E3F00CC93F0 /* CreateDepositorView.swift in Sources */, 4CCAFA142C325715008380E3 /* ClaimManager.swift in Sources */, 4CD9EF152C3551A7007327B3 /* SettingView.swift in Sources */, @@ -383,6 +390,7 @@ 4C04AF262C1AC3FA00F9C104 /* ZKSimpleAppApp.swift in Sources */, 4C06BD582C2E5AA000D52234 /* Groth16Service.swift in Sources */, 4CD9EF092C352EB1007327B3 /* PopupView.swift in Sources */, + 4C6934DA2C51FF3E00E055DA /* NFCDelegate.swift in Sources */, 4CD9EF0F2C353A99007327B3 /* CameraView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -535,10 +543,11 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"ZKSimpleApp/Preview Content\""; - DEVELOPMENT_TEAM = WG75BKZ8CJ; + DEVELOPMENT_TEAM = 8S5PMRDS8H; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = ZKSimpleApp/Info.plist; + INFOPLIST_KEY_NFCReaderUsageDescription = "Request to use NFC scan"; INFOPLIST_KEY_NSCameraUsageDescription = "Your app needs access to the camera to scan QR codes."; INFOPLIST_KEY_NSLocalNetworkUsageDescription = "This app requires access to local network devices for [explain the purpose here]."; INFOPLIST_KEY_NSMicrophoneUsageDescription = "Your app needs access to the microphone for audio recording during video capture."; @@ -556,7 +565,7 @@ "@loader_path/../Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = org.okx.ZKSimpleApp.test; + PRODUCT_BUNDLE_IDENTIFIER = org.okx.ZKSimpleApp; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; @@ -578,10 +587,11 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"ZKSimpleApp/Preview Content\""; - DEVELOPMENT_TEAM = WG75BKZ8CJ; + DEVELOPMENT_TEAM = 8S5PMRDS8H; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = ZKSimpleApp/Info.plist; + INFOPLIST_KEY_NFCReaderUsageDescription = "Request to use NFC scan"; INFOPLIST_KEY_NSCameraUsageDescription = "Your app needs access to the camera to scan QR codes."; INFOPLIST_KEY_NSLocalNetworkUsageDescription = "This app requires access to local network devices for [explain the purpose here]."; INFOPLIST_KEY_NSMicrophoneUsageDescription = "Your app needs access to the microphone for audio recording during video capture."; @@ -599,7 +609,7 @@ "@loader_path/../Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = org.okx.ZKSimpleApp.test; + PRODUCT_BUNDLE_IDENTIFIER = org.okx.ZKSimpleApp; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; diff --git a/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000..f5b778da --- /dev/null +++ b/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,149 @@ +{ + "pins" : [ + { + "identity" : "alamofire", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Alamofire/Alamofire.git", + "state" : { + "revision" : "f455c2975872ccd2d9c81594c658af65716e9b9a", + "version" : "5.9.1" + } + }, + { + "identity" : "bigint", + "kind" : "remoteSourceControl", + "location" : "https://github.com/attaswift/BigInt.git", + "state" : { + "revision" : "0ed110f7555c34ff468e72e1686e59721f2b0da6", + "version" : "5.3.0" + } + }, + { + "identity" : "cryptoswift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/krzyzanowskim/CryptoSwift.git", + "state" : { + "revision" : "c9c3df6ab812de32bae61fc0cd1bf6d45170ebf0", + "version" : "1.8.2" + } + }, + { + "identity" : "moya", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Moya/Moya", + "state" : { + "revision" : "c263811c1f3dbf002be9bd83107f7cdc38992b26", + "version" : "15.0.3" + } + }, + { + "identity" : "promisekit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mxcl/PromiseKit.git", + "state" : { + "revision" : "8a98e31a47854d3180882c8068cc4d9381bf382d", + "version" : "6.22.1" + } + }, + { + "identity" : "qrcode", + "kind" : "remoteSourceControl", + "location" : "https://github.com/WalletConnect/QRCode", + "state" : { + "revision" : "263f280d2c8144adfb0b6676109846cfc8dd552b", + "version" : "14.3.1" + } + }, + { + "identity" : "reactiveswift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ReactiveCocoa/ReactiveSwift.git", + "state" : { + "revision" : "c43bae3dac73fdd3cb906bd5a1914686ca71ed3c", + "version" : "6.7.0" + } + }, + { + "identity" : "rxswift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ReactiveX/RxSwift.git", + "state" : { + "revision" : "b06a8c8596e4c3e8e7788e08e720e3248563ce6a", + "version" : "6.7.1" + } + }, + { + "identity" : "secp256k1.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Boilertalk/secp256k1.swift.git", + "state" : { + "revision" : "cd187c632fb812fd93711a9f7e644adb7e5f97f0", + "version" : "0.1.7" + } + }, + { + "identity" : "starscream", + "kind" : "remoteSourceControl", + "location" : "https://github.com/daltoniam/Starscream", + "state" : { + "revision" : "a063fda2b8145a231953c20e7a646be254365396", + "version" : "3.1.2" + } + }, + { + "identity" : "swift-qrcode-generator", + "kind" : "remoteSourceControl", + "location" : "https://github.com/dagronf/swift-qrcode-generator", + "state" : { + "revision" : "5ca09b6a2ad190f94aa3d6ddef45b187f8c0343b", + "version" : "1.0.3" + } + }, + { + "identity" : "swiftimagereadwrite", + "kind" : "remoteSourceControl", + "location" : "https://github.com/dagronf/SwiftImageReadWrite", + "state" : { + "revision" : "5596407d1cf61b953b8e658fa8636a471df3c509", + "version" : "1.1.6" + } + }, + { + "identity" : "wallet-mobile-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/WalletConnect/wallet-mobile-sdk", + "state" : { + "revision" : "b6dfb7d6b8447c7c5b238a10443a1ac28223f38f", + "version" : "1.0.0" + } + }, + { + "identity" : "walletconnectswiftv2", + "kind" : "remoteSourceControl", + "location" : "https://github.com/WalletConnect/WalletConnectSwiftV2", + "state" : { + "revision" : "8ab4897841a5d04b832f2010a542a246be37998d", + "version" : "1.19.3" + } + }, + { + "identity" : "web3.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/WalletConnect/Web3.swift", + "state" : { + "revision" : "569255adcfff0b37e4cb8004aea29d0e2d6266df", + "version" : "1.0.2" + } + }, + { + "identity" : "web3modal-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/WalletConnect/web3modal-swift", + "state" : { + "revision" : "5d148bfceb9f6ec1b4d39dba63b9b8129b041ef0", + "version" : "1.5.0" + } + } + ], + "version" : 2 +} diff --git a/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/AppDelegate.swift b/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/AppDelegate.swift index 01478823..da69f976 100644 --- a/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/AppDelegate.swift +++ b/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/AppDelegate.swift @@ -28,5 +28,20 @@ class AppDelegate: NSObject, UIApplicationDelegate { return true } + func application(_ application: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { + // Handle the URL + // Post a notification or update a published property to notify your SwiftUI views + NotificationCenter.default.post(name: .didReceiveDeepLink, object: url) + return true + } +} + + +extension Notification.Name { + static var didReceiveDeepLink: Self { .init("NSdidReceiveDeepLink") } } +class DeepLinkHandler { + + +} diff --git a/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/Entitlements/ZKSimpleApp.entitlements b/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/Entitlements/ZKSimpleApp.entitlements index 7ee00da3..5c801180 100644 --- a/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/Entitlements/ZKSimpleApp.entitlements +++ b/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/Entitlements/ZKSimpleApp.entitlements @@ -2,10 +2,13 @@ - com.apple.security.application-groups + com.apple.developer.nfc.readersession.formats - group.org.okx.ZKSimpleApp + TAG + NDEF + com.apple.security.application-groups + keychain-access-groups $(AppIdentifierPrefix)org.okx.ZKSimpleApp diff --git a/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/Entitlements/ZKSimpleAppRelease.entitlements b/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/Entitlements/ZKSimpleAppRelease.entitlements index 7ee00da3..da0a4c13 100644 --- a/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/Entitlements/ZKSimpleAppRelease.entitlements +++ b/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/Entitlements/ZKSimpleAppRelease.entitlements @@ -2,10 +2,12 @@ - com.apple.security.application-groups + com.apple.developer.nfc.readersession.formats - group.org.okx.ZKSimpleApp + NDEF + com.apple.security.application-groups + keychain-access-groups $(AppIdentifierPrefix)org.okx.ZKSimpleApp diff --git a/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/Info.plist b/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/Info.plist index c882f844..ffae9753 100644 --- a/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/Info.plist +++ b/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/Info.plist @@ -19,10 +19,10 @@ NSAppTransportSecurity - NSAllowsLocalNetworking - NSAllowsArbitraryLoads + NSAllowsLocalNetworking + diff --git a/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/Model/PacketQRCode.swift b/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/Model/PacketQRCode.swift index 1848f31b..53d81998 100644 --- a/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/Model/PacketQRCode.swift +++ b/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/Model/PacketQRCode.swift @@ -15,6 +15,15 @@ struct PacketQRPayload: Codable { let commitmentRoot: HexEncodedData } +extension PacketQRPayload { + var jsonString: String { + let encoder = JSONEncoder() + encoder.outputFormatting = [.withoutEscapingSlashes, .sortedKeys] + let data = try! encoder.encode(self) + return String(data: data, encoding: .utf8)! + } +} + struct HexEncodedData: Codable { let data: Data diff --git a/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/Service/AppConfig.swift b/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/Service/AppConfig.swift index 88dc9433..c147bea9 100644 --- a/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/Service/AppConfig.swift +++ b/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/Service/AppConfig.swift @@ -32,7 +32,7 @@ enum AppConfig { AppConfig.contractMap[env]! } - static var defaultGroth16Url: String { "http://0.0.0.0:8080" } + static var defaultGroth16Url: String { "http://10.20.91.188:3000" } static var baseGroth16URLString: String { get { UserDefaults.standard.string(forKey: "baseURLString") ?? defaultGroth16Url @@ -56,9 +56,9 @@ enum AppConfig { ), .x1Layer: .init( contractAddresses: [ - .token : "0xBa202dbDC9371eEb6dD64Fc8B6660978d00E9f5c", - .verifier : "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", - .commitment : "0xaed9d46c1faaa1f89b1a5c41ef0d4d5a774f0df8", + .token : "0x688E4eCe7F374Acbf26faed3Bdc1d771505855f3", + .verifier : "0xc695815B61344Eb189B3D050B70F3348018Eb7DC", + .commitment : "0xC66f55Cd27d2162511ef9f1f3300C56f4A1C14A3", ], chainId: 195, addressList: [ diff --git a/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/Service/ClaimManager.swift b/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/Service/ClaimManager.swift index e8c17b0b..7abcb825 100644 --- a/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/Service/ClaimManager.swift +++ b/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/Service/ClaimManager.swift @@ -31,33 +31,7 @@ class ClaimManager { let key = claimantFilname(id: depositId, claimantId: claimantId) claimed[key] = time } - - func claim(item: PacketItem, claimant: Claimant) async throws { - let d = Date().timeIntervalSince1970 - let proofUrl = claimantFileUrl(id: item.id, claimantId: claimant.index) - DepositStorage.shared.createEmptyFile(at: proofUrl) - - let siblings = item.commitment.toCommitmentTree.getSiblings(index: Int32(claimant.index)) - - try generateProofOfClaim ( - amount: claimant.amount, - secret: claimant.secret, - index: Int32(claimant.index), - commitmentRoot: item.commitment.commitmentRoot, - siblings: siblings, - path: proofUrl.filePath, - ethAddr: Data(WalletService.shared.currentWallet.addr.hexToBytes())) - - let takingTime = Date().timeIntervalSince1970 - d - print("Taking \(takingTime)") - - let key = claimantFilname(id: item.id, claimantId: claimant.index) - - setClaimed(depositId: item.id, claimantId: claimant.index, time: "\(takingTime)s") - - try? await Groth16Service.shared.requestGroth16Proof(proofUrl, fileName: key) - } - + func packetDirectory(id: String) -> URL { DepositStorage.shared.getFilePath(fileName: id) } @@ -75,6 +49,11 @@ class ClaimManager { } func generateQRCodePayload(item: PacketItem, claimant: Claimant) -> String { + let payload = makeQRCodePayload(item: item, claimant: claimant) + return try! payload.json() + } + + func makeQRCodePayload(item: PacketItem, claimant: Claimant) -> PacketQRPayload { let siblings = item.commitment.toCommitmentTree.getSiblings(index: Int32(claimant.index)) let payload = PacketQRPayload( amount: claimant.amount, @@ -82,12 +61,10 @@ class ClaimManager { index: claimant.index, siblings: siblings.map { .init($0) }, commitmentRoot: .init(item.commitment.commitmentRoot)) - - return try! payload.json() + return payload } - func claim(payload: String) throws { - let qrModel = try! JSONDecoder().decode(PacketQRPayload.self, from: payload.data(using: .utf8)!) + func claim(qrModel: PacketQRPayload) async throws { let id = qrModel.commitmentRoot.data.toHexString() _ = DepositStorage.shared.generateFolder(fileName: id) @@ -112,24 +89,21 @@ class ClaimManager { let key = claimantFilname(id: id, claimantId: qrModel.index) - Task { - try? await withdraw(id: id, claimantId: qrModel.index, key: key) - } + try await withdraw(id: id, claimantId: qrModel.index, key: key) setClaimed(depositId: id, claimantId: qrModel.index, time: "\(takingTime)s") } + func claim(payload: String) async throws { + let qrModel = try! JSONDecoder().decode(PacketQRPayload.self, from: payload.data(using: .utf8)!) + try await claim(qrModel: qrModel) + } + func withdraw(id: String, claimantId: Int, key: String) async throws { let groth16Proof = self.claimantProofUrl(id: id, claimantId: claimantId) let proofUrl = claimantFileUrl(id: id, claimantId: claimantId) -// // Write cached to local first to test -// let str = """ -// {"proof":"{\\"pi_a\\":[\\"16068956485921469530737203956950635939610651158538926241067812116940736536955\\",\\"15769980265915195238520320188505708198147998630560418473050307534504073157284\\",\\"1\\"],\\"pi_b\\":[[\\"15586142478048135445302076550697084822142478968178558024126027446551111493152\\",\\"5710466679618415971957281185824162218745540164137880005050764272707165073994\\"],[\\"899819553480022892591262352262223305360248165893976061169057763587417122541\\",\\"18606137395472609282482331037813101005796237876896970348747292834642603272061\\"],[\\"1\\",\\"0\\"]],\\"pi_c\\":[\\"1536699564656438487831578065629068777995405095874815197186730863198663496721\\",\\"7190698198561421677632509978615770470809957234236605283004474137507992274346\\",\\"1\\"],\\"protocol\\":\\"groth16\\"}","pubData":"[\\"12\\",\\"38470927869452275\\",\\"32231875922556150\\",\\"112299335470969\\",\\"4369068071021374636\\",\\"12578636420854713956\\",\\"11833695223087924108\\",\\"10111098072790271979\\",\\"10851461719203003178\\",\\"1093883963525106749\\",\\"16417907512143530459\\",\\"4707576310080416558\\",\\"8390253412247674105\\",\\"12325039937889587705\\",\\"17894659314528908808\\",\\"675124672775417348\\"]","status":"success"} -// """ - - // try str.write(to: groth16Proof, atomically: true, encoding: .utf8) - + // Write cached to local first to test if let cached = try? Data(contentsOf: groth16Proof), let data = String(data: cached, encoding: .utf8) { do { @@ -137,28 +111,53 @@ class ClaimManager { try await withdraw(data: callData) } catch { print(error) + throw error } return } - + do { - let data = try await Groth16Service.shared.requestGroth16Proof(proofUrl, fileName: key) + let _ = try await Groth16Service.shared.requestRapidSnarkProof(proofUrl, fileName: key) + let cur = CFAbsoluteTimeGetCurrent() + let data = try await pollingStatusUntilDie() + print("RapidSnark taking \(CFAbsoluteTimeGetCurrent() - cur)") try data.write(to: groth16Proof) let callData = try getClaimTokenCallData(pwi: String(data: data, encoding: .utf8)!) try await withdraw(data: callData) + print("Withdraw success") } catch { print(error) + throw error } } + func pollingStatusUntilDie() async throws -> Data { + try? await Task.sleep(nanoseconds: 1_000_000_000) + let getStatusData = try await Groth16Service.shared.requestStatus() + let getStatusPayload = try JSONDecoder().decode(GetStatusResponse.self, from: getStatusData) + guard getStatusPayload.status == .success else { + return try await pollingStatusUntilDie() + } + + return getStatusData + } + @discardableResult func withdraw(data: Data) async throws -> String { try await WalletTransaction.shared.withdraw(callData: data, wallet: WalletService.shared.currentWallet) } } +struct GetStatusResponse: Codable { + enum Status: String, Codable { + case success = "success" + case busy = "busy" + } + let status: Status +} + extension URL { var filePath: String { absoluteString.replacingOccurrences(of: "file://", with: "") diff --git a/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/Service/Groth16Service.swift b/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/Service/Groth16Service.swift index 601aae8f..944dcf28 100644 --- a/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/Service/Groth16Service.swift +++ b/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/Service/Groth16Service.swift @@ -13,7 +13,8 @@ typealias ErrorBlock = (_ error: Error?) -> Void class Groth16Service { - let provider: MoyaProvider + let groth16Provider: MoyaProvider + let rapidSnarkProvider: MoyaProvider let localNW: LocalNetworkAuthorization = .init() static let shared = Groth16Service() @@ -24,17 +25,20 @@ class Groth16Service { sessionConfiguration.timeoutIntervalForRequest = 6000.0 sessionConfiguration.timeoutIntervalForResource = 6000.0 sessionConfiguration.httpShouldUsePipelining = true + let session = Session(configuration: sessionConfiguration) - provider = MoyaProvider(session: session) + groth16Provider = MoyaProvider(session: session) + rapidSnarkProvider = MoyaProvider(session: session) localNW.requestAuthorization { status in print(status) } } + - func requestGroth16Proof(_ fileUrl: URL, fileName: String) async throws -> Data { + func requestRapidSnarkProof(_ fileUrl: URL, fileName: String) async throws -> Data { try await withCheckedThrowingContinuation { continuation in - provider.request(.uploadFile(fileUrl: fileUrl, fileName: fileName)) { result in + rapidSnarkProvider.request(.requestProof(fileUrl: fileUrl, fileName: fileName)) { result in switch result { case let .success(response): let responseData = response.data @@ -48,13 +52,30 @@ class Groth16Service { } } } + + func requestStatus() async throws -> Data { + try await withCheckedThrowingContinuation { continuation in + rapidSnarkProvider.request(.getStatus) { result in + switch result { + case let .success(response): + let responseData = response.data + print("Get status: \(responseData)") + + continuation.resume(returning: responseData) + case let .failure(error): + print("Get status failed: \(error)") + continuation.resume(throwing: error) + } + } + } + } } -enum Groth16ServiceAPI { +enum Groth16APIService { case uploadFile(fileUrl: URL, fileName: String) } -extension Groth16ServiceAPI: TargetType { +extension Groth16APIService: TargetType { var baseURL: URL { return URL(string: AppConfig.baseGroth16URLString)! } @@ -93,57 +114,52 @@ extension Groth16ServiceAPI: TargetType { } } -@available(iOS 14.0, *) -public class LocalNetworkAuthorization: NSObject { - private var browser: NWBrowser? - private var netService: NetService? - private var completion: ((Bool) -> Void)? - - public func requestAuthorization(completion: @escaping (Bool) -> Void) { - self.completion = completion - - // Create parameters, and allow browsing over peer-to-peer link. - let parameters = NWParameters() - parameters.includePeerToPeer = true - - // Browse for a custom service type. - let browser = NWBrowser(for: .bonjour(type: "_bonjour._tcp", domain: nil), using: parameters) - self.browser = browser - browser.stateUpdateHandler = { newState in - switch newState { - case .failed(let error): - print(error.localizedDescription) - case .ready, .cancelled: - break - case let .waiting(error): - print("Local network permission has been denied: \(error)") - self.reset() - self.completion?(false) - default: - break - } +enum RapidSnarkAPIService { + case requestProof(fileUrl: URL, fileName: String) + case getStatus +} + +extension RapidSnarkAPIService: TargetType { + var baseURL: URL { + return URL(string: AppConfig.baseGroth16URLString)! + } + + var path: String { + switch self { + case .requestProof: + return "/input/plonky2" + case .getStatus: + return "/status" } - - self.netService = NetService(domain: "local.", type:"_lnp._tcp.", name: "LocalNetworkPrivacy", port: 1100) - self.netService?.delegate = self - - self.browser?.start(queue: .main) - self.netService?.publish() } - - private func reset() { - self.browser?.cancel() - self.browser = nil - self.netService?.stop() - self.netService = nil + + var method: Moya.Method { + switch self { + case .requestProof: + return .post + case .getStatus: + return .get + } + } + + var task: Task { + switch self { + case let .requestProof(fileUrl, _): + let data = try! Data(contentsOf: fileUrl) + return .requestData(data) + case .getStatus: + return .requestPlain + } + } + + var headers: [String: String]? { + return [ + "Content-type": "application/json", + "Connection": "keep-alive", + ] } -} -@available(iOS 14.0, *) -extension LocalNetworkAuthorization : NetServiceDelegate { - public func netServiceDidPublish(_ sender: NetService) { - self.reset() - print("Local network permission has been granted") - completion?(true) + var sampleData: Data { + return Data() } } diff --git a/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/Service/LocalNetworkAuthorization.swift b/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/Service/LocalNetworkAuthorization.swift new file mode 100644 index 00000000..23bc9d83 --- /dev/null +++ b/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/Service/LocalNetworkAuthorization.swift @@ -0,0 +1,64 @@ +// +// LocalNetworkAuthorization.swift +// ZKSimpleApp +// +// Created by Quach Ha Chan Thanh on 24/7/24. +// + +import Foundation +import Network + +@available(iOS 14.0, *) +public class LocalNetworkAuthorization: NSObject { + private var browser: NWBrowser? + private var netService: NetService? + private var completion: ((Bool) -> Void)? + + public func requestAuthorization(completion: @escaping (Bool) -> Void) { + self.completion = completion + + // Create parameters, and allow browsing over peer-to-peer link. + let parameters = NWParameters() + parameters.includePeerToPeer = true + + // Browse for a custom service type. + let browser = NWBrowser(for: .bonjour(type: "_bonjour._tcp", domain: nil), using: parameters) + self.browser = browser + browser.stateUpdateHandler = { newState in + switch newState { + case .failed(let error): + print(error.localizedDescription) + case .ready, .cancelled: + break + case let .waiting(error): + print("Local network permission has been denied: \(error)") + self.reset() + self.completion?(false) + default: + break + } + } + + self.netService = NetService(domain: "local.", type:"_lnp._tcp.", name: "LocalNetworkPrivacy", port: 1100) + self.netService?.delegate = self + + self.browser?.start(queue: .main) + self.netService?.publish() + } + + private func reset() { + self.browser?.cancel() + self.browser = nil + self.netService?.stop() + self.netService = nil + } +} + +@available(iOS 14.0, *) +extension LocalNetworkAuthorization : NetServiceDelegate { + public func netServiceDidPublish(_ sender: NetService) { + self.reset() + print("Local network permission has been granted") + completion?(true) + } +} diff --git a/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/Service/NFCDelegate.swift b/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/Service/NFCDelegate.swift new file mode 100644 index 00000000..2e207c44 --- /dev/null +++ b/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/Service/NFCDelegate.swift @@ -0,0 +1,63 @@ +// +// NFCDelegate.swift +// ZKSimpleApp +// +// Created by Quach Ha Chan Thanh on 25/7/24. +// + +import Foundation +import CoreNFC + +class NFCDelegate: NSObject, NFCNDEFReaderSessionDelegate { + var writeData: String? + var invalidBlock: (() -> Void)? + + func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) { + // Handle session invalidation (errors, user cancelled, etc.) + print("Session invalidated: \(error.localizedDescription)") + invalidBlock?() + } + + func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) { + // Handle detection of NDEF messages (not needed for writing) + print("didDetectNDEFs messages \(messages.map { $0.description })") + } + + func readerSession(_ session: NFCNDEFReaderSession, didDetect tags: [NFCNDEFTag]) { + print("Did detect tags") + + guard let tag = tags.first else { + session.invalidate(errorMessage: "No NFC tag detected.") + return + } + + session.connect(to: tag) { (error: Error?) in + if let error = error { + session.invalidate(errorMessage: "Connection failed: \(error.localizedDescription)") + return + } + + self.writeDataToTag(tag: tag, session: session) + } + } + + func readerSessionDidBecomeActive(_ session: NFCNDEFReaderSession) { + print("readerSessionDidBecomeActive") + } + + private func writeDataToTag(tag: NFCNDEFTag, session: NFCNDEFReaderSession) { + guard let writeData else { return } + + let payload = NFCNDEFPayload.wellKnownTypeURIPayload(string: writeData) + let message = NFCNDEFMessage(records: [payload!]) + + tag.writeNDEF(message) { (error: Error?) in + if let error = error { + session.invalidate(errorMessage: "Write failed: \(error.localizedDescription)") + } else { + session.alertMessage = "Write successful!" + session.invalidate() + } + } + } +} diff --git a/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/Service/Transaction/WalletTransaction.swift b/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/Service/Transaction/WalletTransaction.swift index af74f772..7458e2ce 100644 --- a/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/Service/Transaction/WalletTransaction.swift +++ b/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/Service/Transaction/WalletTransaction.swift @@ -55,7 +55,7 @@ class WalletTransaction { let tx = contract.approve(spender: address, value: try BigUInt(UInt64.max)) let txt = EthereumTransaction( nonce: nonce, - gasPrice: EthereumQuantity(quantity: 21.gwei), + gasPrice: EthereumQuantity(quantity: 50.gwei), gas: 100000, from: senderAddr, to: ContractAddress.token.ethAddress, @@ -106,8 +106,8 @@ class WalletTransaction { let tx = contract.depositERC20(commitment: commitment, owner: senderAddr, amount: amount) let txt = EthereumTransaction( nonce: nonce, - gasPrice: EthereumQuantity(quantity: 21.gwei), - gas: 100000, + gasPrice: EthereumQuantity(quantity: 100.gwei), + gas: 1000000, from: senderAddr, to: ContractAddress.commitment.ethAddress, value: 0, @@ -162,8 +162,8 @@ class WalletTransaction { let txt = EthereumTransaction( nonce: nonce, - gasPrice: EthereumQuantity(quantity: 21.gwei), - gas: 1000000, + gasPrice: EthereumQuantity(quantity: 50.gwei), + gas: 1_000_000, from: senderAddr, to: ContractAddress.commitment.ethAddress, value: 0, diff --git a/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/View/ClaimListView.swift b/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/View/ClaimListView.swift index 7f851fe9..ada64520 100644 --- a/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/View/ClaimListView.swift +++ b/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/View/ClaimListView.swift @@ -15,7 +15,6 @@ import Web3PromiseKit @available(iOS 16.0, *) struct ClaimListView: View { @StateObject var viewModel: ClaimListViewModel - @State private var qrCodeShowing: String? var body: some View { VStack { @@ -45,21 +44,6 @@ struct ClaimListView: View { } .padding() .navigationTitle("Claim list") - .popup(isPresented: qrCodeShowing != nil) { - qrCodeShowing = nil - } content: { - VStack { - QRCodeView(payload: qrCodeShowing!) { - qrCodeShowing = nil - } - .frame(width: 300, height: 500) - .padding() - .background(Color.green) - .foregroundColor(.white) - .cornerRadius(8) - - } - } .showHUD(.loading, isPresented: .init(get: { viewModel.isLoading }, set: { newValue in @@ -68,44 +52,37 @@ struct ClaimListView: View { } func builItem(_ info: ClaimListViewModel.ClaimInfo) -> some View { - HStack(alignment: .center) { - let item = info.item - - VStack(alignment: .leading, spacing: 8) { - HStack { - Text("ID:") - Text("\(item.index)") - .font(.caption) - } - - HStack { - Text("Amount-secret:") - Text("\(item.amount) - \(item.secret)") - .font(.caption) - } + NavigationLink(destination: QRCodeView(payload: viewModel.generatePacket(claimant: info.item))) { + HStack(alignment: .center) { + let item = info.item - if case .claimed(let duration) = info.status { + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("ID:") + Text("\(item.index)") + .font(.caption) + } + HStack { - Text("Duration:") - Text(duration) + Text("Amount-secret:") + Text("\(item.amount) - \(item.secret)") .font(.caption) } + + if case .claimed(let duration) = info.status { + HStack { + Text("Duration:") + Text(duration) + .font(.caption) + } + } } + + Spacer() + } - - Spacer() - - Button(action: { - qrCodeShowing = viewModel.generateQRCode(claimant: item) - }, label: { - Text("Show QR Code") - .padding() - .background(Color.green) - .foregroundColor(.white) - .cornerRadius(8) - }) + .padding(.vertical, 10) } - .padding(.vertical, 10) } } @@ -129,16 +106,7 @@ class ClaimListViewModel: ObservableObject { reloadAmount() } - - func claim(_ claimant: Claimant) async { - do { - try await ClaimManager.shared.claim(item: item, claimant: claimant) - rebuild() - } catch { - print(error) - } - } - + func rebuild() { claimants = item.claimants.map { .init(item: $0, status: ClaimManager.shared.getStatus(id: item.id, claimant: $0)) @@ -181,8 +149,8 @@ class ClaimListViewModel: ObservableObject { } } - func generateQRCode(claimant: Claimant) -> String { - ClaimManager.shared.generateQRCodePayload(item: item, claimant: claimant) + func generatePacket(claimant: Claimant) -> PacketQRPayload { + ClaimManager.shared.makeQRCodePayload(item: item, claimant: claimant) } } diff --git a/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/View/ContentView.swift b/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/View/ContentView.swift index 948b15b8..3d260ab3 100644 --- a/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/View/ContentView.swift +++ b/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/View/ContentView.swift @@ -14,6 +14,9 @@ struct ContentView: View { @Environment(\.presentationMode) var presentationMode @State private var isClearAllConfirming = false + @State private var qrPayload: PacketQRPayload? + @State private var navigateToDetail = false + var body: some View { if #available(iOS 16.0, *) { NavigationView { @@ -22,6 +25,12 @@ struct ContentView: View { buildDepositItem(item) } + if let qrPayload { + NavigationLink(destination: QRCodeView(payload: qrPayload), isActive: $navigateToDetail) { + EmptyView() + } + } + Spacer() } .toolbar { @@ -54,7 +63,6 @@ struct ContentView: View { } label: { Image(systemName: "trash") } - } .navigationTitle("Deposit") } @@ -69,6 +77,19 @@ struct ContentView: View { secondaryButton: .cancel(Text("No")) ) } + .onOpenURL(perform: { url in + guard let urlComponent = URLComponents(url: url, resolvingAgainstBaseURL: true), + urlComponent.scheme == "zksimpleapp", + urlComponent.host == "share", + let payload = urlComponent.queryItems?.first?.value, + let data = Data.fromBase64(payload), + let qrPayload = try? JSONDecoder().decode(PacketQRPayload.self, from: data) + else { return } + + print(qrPayload) + self.qrPayload = qrPayload + self.navigateToDetail = true + }) } else { VStack {} } @@ -155,3 +176,31 @@ class DepositListViewModel: ObservableObject, DepositCreatable { loadList() } } + +extension Data { + /// Same as ``Data(base64Encoded:)``, but adds padding automatically + /// (if missing, instead of returning `nil`). + public static func fromBase64(_ encoded: String) -> Data? { + // Prefixes padding-character(s) (if needed). + var encoded = encoded; + let remainder = encoded.count % 4 + if remainder > 0 { + encoded = encoded.padding( + toLength: encoded.count + 4 - remainder, + withPad: "=", startingAt: 0); + } + + // Finally, decode. + return Data(base64Encoded: encoded); + } +} + +extension String { + func encodeToBase64() -> String { + // Convert the string to Data + let data = self.data(using: .utf8)! + // Encode the Data to a Base64 encoded string + let base64String = data.base64EncodedString() + return base64String + } +} diff --git a/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/View/QRCodeView.swift b/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/View/QRCodeView.swift index c838af73..ea123f64 100644 --- a/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/View/QRCodeView.swift +++ b/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/View/QRCodeView.swift @@ -8,18 +8,30 @@ import Foundation import SwiftUI import CoreImage.CIFilterBuiltins +import CoreNFC @available(iOS 16.0, *) struct QRCodeView: View { - let payload: String - let dismiss: () -> Void + let payload: PacketQRPayload private let qrCodeGenerator = QRCodeGenerator() @State private var showTextHUD: String? + @State private var isLoading: Bool = false + @State private var nfcSession: NFCNDEFReaderSession? + let delegate = NFCDelegate() var body: some View { - VStack { - if let qrCodeImage = qrCodeGenerator.generateQRCode(from: payload) { + VStack { + + Text("Payload:") + .font(.title) + .padding(8) + + Text(payload.jsonString) + .font(.caption) + .padding(8) + + if let qrCodeImage = qrCodeGenerator.generateQRCode(from: payload.jsonString) { Image(uiImage: qrCodeImage) .interpolation(.none) .resizable() @@ -29,20 +41,35 @@ struct QRCodeView: View { } Button(action: { - do { - try ClaimManager.shared.claim(payload: payload) -// showTextHUD = "Claimed successful" - } catch { -// showTextHUD = "Claimed failed \(error)" + Task { + await claim() } }) { - Text("Claim it") + Text("Withdraw") .padding() .background(Color.white) .foregroundColor(.black) .cornerRadius(8) } + + Button(action: { + if nfcSession == nil { + startNFCSession() + } else { + nfcSession?.invalidate() + nfcSession = nil + } + }) { + Text(nfcSession == nil ? "Tap To Share" : "End sharing") + .padding() + .background(Color.cyan) + .foregroundColor(.white) + .cornerRadius(8) + } + + Spacer() } + .showHUD(.loading, isPresented: .init(get: { isLoading }, set: { isLoading = $0 }), timeout: 60) .showHUD( .text(showTextHUD ?? ""), isPresented: .init( @@ -50,11 +77,34 @@ struct QRCodeView: View { set: { _ in showTextHUD = nil } - ), timeout: 3) { - dismiss() + ), + timeout: 3) + .padding() + + } + + func claim() async { + isLoading = true + do { + try await ClaimManager.shared.claim(qrModel: payload) + isLoading = false + showTextHUD = "Claimed successful" + } catch { + isLoading = false + showTextHUD = "Claimed failed \(error)" + } + } + + private func startNFCSession() { + nfcSession = NFCNDEFReaderSession(delegate: delegate, queue: nil, invalidateAfterFirstRead: false) + nfcSession?.alertMessage = "To share Packet!! Hold your iPhone near the NFC tag " + delegate.writeData = "zksimpleapp://share?payload=\(payload.jsonString.encodeToBase64())" + delegate.invalidBlock = { + Task { @MainActor in + nfcSession = nil } - .padding() - + } + nfcSession?.begin() } } diff --git a/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/View/ScanCodeView.swift b/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/View/ScanCodeView.swift index 7396a35f..200f9c4c 100644 --- a/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/View/ScanCodeView.swift +++ b/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/View/ScanCodeView.swift @@ -39,6 +39,7 @@ struct ScanCodeView: View { } } .navigationTitle("Claim packet") + .showHUD(.loading, isPresented: .init(get: { isClaiming }, set: { isClaiming = $0 }), timeout: 60) .showHUD( .text(showTextHUD ?? ""), isPresented: .init( @@ -58,15 +59,18 @@ struct ScanCodeView: View { CameraView { code in guard isClaiming == false, isClaimed == false else { return } isClaiming = true - do { - AudioServicesPlaySystemSound(try! SystemSoundID(kSystemSoundID_Vibrate)) - try ClaimManager.shared.claim(payload: code) - showTextHUD = "Claimed successful" - isClaimed = true - } catch { - showTextHUD = "Claimed failed \(error)" + + Task { + do { + AudioServicesPlaySystemSound(try! SystemSoundID(kSystemSoundID_Vibrate)) + try await ClaimManager.shared.claim(payload: code) + showTextHUD = "Claimed successful" + isClaimed = true + } catch { + showTextHUD = "Claimed failed \(error)" + } + isClaiming = false } - isClaiming = false } .edgesIgnoringSafeArea(.all) } diff --git a/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/View/SettingView.swift b/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/View/SettingView.swift index 3f8cef9a..ca96af86 100644 --- a/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/View/SettingView.swift +++ b/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/View/SettingView.swift @@ -143,6 +143,8 @@ struct SettingView: View { .pickerStyle(MenuPickerStyle()) .padding() .onChange(of: isLocalIdx, perform: { value in + guard AppConfig.env != ContractEnv(rawValue: value) else { return } + AppConfig.changeEnv(value) userInput = AppConfig.rpcURL @@ -150,6 +152,15 @@ struct SettingView: View { currentAddressIdx = 0 }) }) + .onAppear(perform: { + if currentAddressIdx >= currentAddressDatasource.count { + currentAddressIdx = 0 + WalletService.shared.selectedIdx = 0 + } + userInput = AppConfig.rpcURL + groth16Input = AppConfig.baseGroth16URLString + isLocalIdx = AppConfig.env.rawValue + }) } func isValidURL(_ urlString: String) -> Bool { diff --git a/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/ZKSimpleAppApp.swift b/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/ZKSimpleAppApp.swift index 6c618b15..6fc12fea 100644 --- a/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/ZKSimpleAppApp.swift +++ b/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/ZKSimpleAppApp.swift @@ -17,8 +17,8 @@ struct ZKSimpleAppApp: App { WindowGroup { ContentView(viewModel: .init()) .onOpenURL { url in - Web3Modal.instance.handleDeeplink(url) - } + Web3Modal.instance.handleDeeplink(url) + } } } } diff --git a/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/zk_commit_mobile.swift b/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/zk_commit_mobile.swift index 0aa2f0fc..a9563850 100644 --- a/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/zk_commit_mobile.swift +++ b/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/zk_commit_mobile.swift @@ -769,6 +769,10 @@ public enum ZkCommitmentMobileError { case ProofError(message: String) + case TryFromIntError(message: String) + + case TryFromSliceError(message: String) + } @@ -798,6 +802,14 @@ public struct FfiConverterTypeZkCommitmentMobileError: FfiConverterRustBuffer { message: try FfiConverterString.read(from: &buf) ) + case 5: return .TryFromIntError( + message: try FfiConverterString.read(from: &buf) + ) + + case 6: return .TryFromSliceError( + message: try FfiConverterString.read(from: &buf) + ) + default: throw UniffiInternalError.unexpectedEnumCase } @@ -817,6 +829,10 @@ public struct FfiConverterTypeZkCommitmentMobileError: FfiConverterRustBuffer { writeInt(&buf, Int32(3)) case .ProofError(_ /* message is ignored*/): writeInt(&buf, Int32(4)) + case .TryFromIntError(_ /* message is ignored*/): + writeInt(&buf, Int32(5)) + case .TryFromSliceError(_ /* message is ignored*/): + writeInt(&buf, Int32(6)) } diff --git a/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/zk_commit_mobile.xcframework/ios-arm64/libzk_commit_mobile.dylib b/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/zk_commit_mobile.xcframework/ios-arm64/libzk_commit_mobile.dylib index 67bfc6cb..b31e617a 100755 Binary files a/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/zk_commit_mobile.xcframework/ios-arm64/libzk_commit_mobile.dylib and b/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/zk_commit_mobile.xcframework/ios-arm64/libzk_commit_mobile.dylib differ diff --git a/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/zk_commit_mobile.xcframework/ios-arm64_x86_64-simulator/libzk_commit_mobile.dylib b/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/zk_commit_mobile.xcframework/ios-arm64_x86_64-simulator/libzk_commit_mobile.dylib index 92dfddf2..25c36316 100755 Binary files a/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/zk_commit_mobile.xcframework/ios-arm64_x86_64-simulator/libzk_commit_mobile.dylib and b/crates/zk-commit-mobile/ios/ZKSimpleApp/ZKSimpleApp/zk_commit_mobile.xcframework/ios-arm64_x86_64-simulator/libzk_commit_mobile.dylib differ diff --git a/crates/zk-commit-mobile/src/blockchain.rs b/crates/zk-commit-mobile/src/blockchain.rs index 590274d0..85e976bb 100644 --- a/crates/zk-commit-mobile/src/blockchain.rs +++ b/crates/zk-commit-mobile/src/blockchain.rs @@ -1,8 +1,33 @@ +use serde::{Deserialize, Serialize}; + +use zk_commit_core::{ + blockchain::Blockchain, + groth16::{Groth16Proof, Groth16ProofWithPublicData}, +}; + use crate::ZkCommitmentMobileError; -use zk_commit_core::{blockchain::Blockchain, groth16::Groth16ProofWithPublicData}; + +#[derive(Deserialize, Serialize, Debug)] +pub struct RapidsnarkSuccessResponse { + pub status: String, + pub proof: String, + #[serde(rename = "pubData")] + pub pub_data: String, +} + +impl TryFrom for Groth16ProofWithPublicData { + type Error = serde_json::Error; + + fn try_from(value: RapidsnarkSuccessResponse) -> Result { + let proof: Groth16Proof = serde_json::from_str(&value.proof)?; + let public_data: Vec = serde_json::from_str(&value.pub_data)?; + Ok(Groth16ProofWithPublicData { proof, public_data }) + } +} #[uniffi::export] pub fn get_claim_token_call_data(pwi: &str) -> Result, ZkCommitmentMobileError> { - let pwi: Groth16ProofWithPublicData = serde_json::from_str(pwi)?; + let response: RapidsnarkSuccessResponse = serde_json::from_str(pwi)?; + let pwi = response.try_into()?; Ok(Blockchain::get_claim_token_call_data(&pwi)) } diff --git a/crates/zk-commit-mobile/src/lib.rs b/crates/zk-commit-mobile/src/lib.rs index c69d4057..a91be7d1 100644 --- a/crates/zk-commit-mobile/src/lib.rs +++ b/crates/zk-commit-mobile/src/lib.rs @@ -1,8 +1,9 @@ use std::{ + array::TryFromSliceError, io, + num::TryFromIntError, time::{Duration, Instant}, }; - use uniffi::deps::anyhow; use zk_commit_core::{examples::fibonacci::fibonacci as core_fibonacci, prover::ProofError}; @@ -25,6 +26,10 @@ pub enum ZkCommitmentMobileError { SerdeJson(#[from] serde_json::Error), #[error("ProofError: {0}")] ProofError(#[from] ProofError), + #[error("TryFromIntError: {0}")] + TryFromIntError(#[from] TryFromIntError), + #[error("TryFromSliceError: {0}")] + TryFromSliceError(#[from] TryFromSliceError), } #[derive(uniffi::Record)] diff --git a/crates/zk-commit-mobile/src/prover.rs b/crates/zk-commit-mobile/src/prover.rs index 25cb9947..68f84af9 100644 --- a/crates/zk-commit-mobile/src/prover.rs +++ b/crates/zk-commit-mobile/src/prover.rs @@ -3,7 +3,7 @@ use plonky2::{field::types::Field, hash::hash_types::HashOut, plonk::config::Gen use zk_commit_core::{ prover::{ generate_proof_of_claim as core_generate_proof_of_claim, - setup_commitment as core_setup_commitment, ProofError, + setup_commitment as core_setup_commitment, }, types::F, }; @@ -27,21 +27,14 @@ pub fn generate_proof_of_claim( path: &str, eth_addr: &[u8], ) -> Result<(), ZkCommitmentMobileError> { - if eth_addr.len() != 20 { - // return Err(ProofError::ProofError(String::from("the receipient address is not 20 bytes."))); - return Err(ZkCommitmentMobileError::ProofError(ProofError::Unknown)); - } - let mut eth_addr_array = [0u8; 20]; - eth_addr_array.copy_from_slice(eth_addr); - let _result = core_generate_proof_of_claim( F::from_canonical_u64(amount), F::from_canonical_u64(secret), - index as usize, + index.try_into()?, siblings.iter().map(|x| HashOut::from_bytes(x)).collect(), HashOut::from_bytes(commitment_root), path, - eth_addr_array, + eth_addr.try_into()?, )?; Ok(()) }