From 83c1390293d88c1d92d890708a8ee5db2df5f76c Mon Sep 17 00:00:00 2001 From: undefined Date: Wed, 9 Apr 2025 05:16:05 +0000 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=94=84=20synced=20local=20'./gradle/'?= =?UTF-8?q?=20with=20remote=20'./gradle/'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gradle/libs.versions.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8bf23d7..3ac34a5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -36,6 +36,7 @@ okhttp3-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging- kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinx-coroutines-test" } moshi-kotlin = { group = "com.squareup.moshi", name = "moshi-kotlin", version.ref = "moshi" } moshi-ksp = { group = "com.squareup.moshi", name = "moshi-kotlin-codegen", version.ref = "moshi" } +moshi-adapters = { group = "com.squareup.moshi", name = "moshi-adapters", version.ref = "moshi" } retrofit2-converter-moshi = { group = "com.squareup.retrofit2", name = "converter-moshi", version.ref = "retrofit2" } androidx-credentials = { group = "androidx.credentials", name = "credentials", version.ref = "androidx-credentials" } androidx-credentials-auth = { group = "androidx.credentials", name = "credentials-play-services-auth", version.ref = "androidx-credentials" } From 456d0cc14dd133fdbdad1f55ae961e61d905450f Mon Sep 17 00:00:00 2001 From: undefined Date: Wed, 9 Apr 2025 05:16:05 +0000 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=94=84=20synced=20local=20'./lib/'=20?= =?UTF-8?q?with=20remote=20'./lib/'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/build.gradle.kts | 7 +- .../core/accounts/CircleSmartAccount.kt | 70 +----------- .../core/apis/modular/ModularApi.kt | 7 ++ .../core/apis/modular/ModularApiImpl.kt | 49 ++++++++ .../core/apis/modular/ModularReqResp.kt | 12 +- .../modularwallets/core/apis/util/UtilApi.kt | 8 +- .../core/apis/util/UtilApiImpl.kt | 47 +++++++- .../circle/modularwallets/core/chains/Base.kt | 24 ++++ .../modularwallets/core/chains/BaseSepolia.kt | 24 ++++ .../modularwallets/core/chains/Optimism.kt | 24 ++++ .../core/chains/OptimismSepolia.kt | 24 ++++ .../modularwallets/core/chains/Unichain.kt | 24 ++++ .../core/chains/UnichainSepolia.kt | 24 ++++ .../core/clients/BundlerClient.kt | 19 ++++ .../core/constants/AbiConstants.kt | 7 ++ .../modularwallets/core/models/OwnerModels.kt | 107 ++++++++++++++++++ .../modularwallets/core/models/Token.kt | 9 +- .../core/utils/JsonConversionUtils.kt | 3 +- .../core/utils/abi/EncodeCallDataUtils.kt | 2 +- .../core/utils/rpc/ReqRespUtils.kt | 26 ++++- 20 files changed, 434 insertions(+), 83 deletions(-) create mode 100644 lib/src/main/java/com/circle/modularwallets/core/chains/Base.kt create mode 100644 lib/src/main/java/com/circle/modularwallets/core/chains/BaseSepolia.kt create mode 100644 lib/src/main/java/com/circle/modularwallets/core/chains/Optimism.kt create mode 100644 lib/src/main/java/com/circle/modularwallets/core/chains/OptimismSepolia.kt create mode 100644 lib/src/main/java/com/circle/modularwallets/core/chains/Unichain.kt create mode 100644 lib/src/main/java/com/circle/modularwallets/core/chains/UnichainSepolia.kt create mode 100644 lib/src/main/java/com/circle/modularwallets/core/models/OwnerModels.kt diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index b85b27e..5f2ef7e 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -15,8 +15,8 @@ plugins { extra.apply { set("versionMajor", 0) - set("versionMedium", 0) - set("versionMinorPublished", 204) // should increment after public release + set("versionMedium", 1) + set("versionMinorPublished", 0) set("libraryId", libraryId()) set("libraryGroupId", libraryId()) set("libraryArtifactId", libraryArtifactId()) @@ -147,6 +147,7 @@ dependencies { implementation(libs.androidx.credentials.auth) implementation(libs.retrofit2.converter.moshi) implementation(libs.moshi.kotlin) + implementation(libs.moshi.adapters) implementation(libs.web3j) implementation(libs.retrofit2.retrofit) implementation(libs.retrofit2.converter.gson) @@ -195,4 +196,4 @@ afterEvaluate { } } } -} \ No newline at end of file +} diff --git a/lib/src/main/java/com/circle/modularwallets/core/accounts/CircleSmartAccount.kt b/lib/src/main/java/com/circle/modularwallets/core/accounts/CircleSmartAccount.kt index 16a0bcf..b690ddb 100644 --- a/lib/src/main/java/com/circle/modularwallets/core/accounts/CircleSmartAccount.kt +++ b/lib/src/main/java/com/circle/modularwallets/core/accounts/CircleSmartAccount.kt @@ -286,9 +286,7 @@ class CircleSmartAccount( @Throws(Exception::class) override suspend fun sign(context: Context, hex: String): String { val digest = toSha3Bytes(hex) - val hash = getReplaySafeHash( - client.chain.chainId, getAddress(), bytesToHex(digest) - ) + val hash = UtilApiImpl.getReplaySafeMessageHash(client.transport, getAddress(), bytesToHex(digest)) val signResult = owner.sign(context, hash) val signature = encodePackedForSignature( signResult, @@ -309,9 +307,7 @@ class CircleSmartAccount( @Throws(Exception::class) override suspend fun signMessage(context: Context, message: String): String { val digest = toSha3Bytes(hashMessage(message.toByteArray())) - val hash = getReplaySafeHash( - client.chain.chainId, getAddress(), bytesToHex(digest) - ) + val hash = UtilApiImpl.getReplaySafeMessageHash(client.transport, getAddress(), bytesToHex(digest)) val signResult = owner.sign(context, hash) val signature = encodePackedForSignature( signResult, @@ -332,9 +328,7 @@ class CircleSmartAccount( @Throws(Exception::class) override suspend fun signTypedData(context: Context, typedData: String): String { val digest = toSha3Bytes(hashTypedData(typedData)) - val hash = getReplaySafeHash( - client.chain.chainId, getAddress(), bytesToHex(digest) - ) + val hash = UtilApiImpl.getReplaySafeMessageHash(client.transport, getAddress(), bytesToHex(digest)) val signResult = owner.sign(context, hash) val signature = encodePackedForSignature( signResult, @@ -380,64 +374,6 @@ class CircleSmartAccount( } -internal fun getReplaySafeHash( - chainId: Long, - account: String, - hash: String, - verifyingContract: String = CIRCLE_WEIGHTED_WEB_AUTHN_MULTISIG_PLUGIN.address, -): String { - val prefix = Numeric.hexStringToByteArray(EIP712_PREFIX) - val domainSeparatorTypeHash = - toSha3Bytes(REPLAY_SAFE_HASH_V1.domainSeparatorType) - - val domainSeparator = toSha3Bytes( - encodeAbiParameters( - listOf( - Bytes32(domainSeparatorTypeHash), - Bytes32(getModuleIdHash()), - Uint256(chainId), - Address(verifyingContract), - Bytes32(pad(toBytes(account), isRight = true)), - ) - ) - ) - - val structHash = toSha3Bytes( - encodeAbiParameters( - listOf( - Bytes32(getModuleTypeHash()), - Bytes32(Numeric.hexStringToByteArray(hash)) - ) - ) - ) - return bytesToHex( - Hash.sha3( - concat( - prefix, - domainSeparator, - structHash - ) - ) - ) -} - -internal fun getModuleIdHash(): ByteArray { - return toSha3Bytes( - encodePacked( - listOf>( - Utf8String(REPLAY_SAFE_HASH_V1.name), - Utf8String(REPLAY_SAFE_HASH_V1.version), - ) - ) - ) -} - -internal fun getModuleTypeHash(): ByteArray { - return toSha3Bytes( - REPLAY_SAFE_HASH_V1.moduleType - ) -} - internal fun encodePackedForSignature( signResult: SignResult, publicKey: String, diff --git a/lib/src/main/java/com/circle/modularwallets/core/apis/modular/ModularApi.kt b/lib/src/main/java/com/circle/modularwallets/core/apis/modular/ModularApi.kt index 2d78225..757b4b4 100644 --- a/lib/src/main/java/com/circle/modularwallets/core/apis/modular/ModularApi.kt +++ b/lib/src/main/java/com/circle/modularwallets/core/apis/modular/ModularApi.kt @@ -18,9 +18,16 @@ package com.circle.modularwallets.core.apis.modular +import com.circle.modularwallets.core.models.AddressMappingOwner +import com.circle.modularwallets.core.models.CreateAddressMappingResult import com.circle.modularwallets.core.transports.Transport internal interface ModularApi { suspend fun getAddress(transport: Transport, getAddressReq: GetAddressReq): ModularWallet + suspend fun createAddressMapping( + transport: Transport, + walletAddress: String, + owners: Array + ): Array } \ No newline at end of file diff --git a/lib/src/main/java/com/circle/modularwallets/core/apis/modular/ModularApiImpl.kt b/lib/src/main/java/com/circle/modularwallets/core/apis/modular/ModularApiImpl.kt index d1c7451..ebacb8a 100644 --- a/lib/src/main/java/com/circle/modularwallets/core/apis/modular/ModularApiImpl.kt +++ b/lib/src/main/java/com/circle/modularwallets/core/apis/modular/ModularApiImpl.kt @@ -18,9 +18,16 @@ package com.circle.modularwallets.core.apis.modular +import com.circle.modularwallets.core.errors.BaseError +import com.circle.modularwallets.core.models.AddressMappingOwner +import com.circle.modularwallets.core.models.CreateAddressMappingResult +import com.circle.modularwallets.core.models.EoaAddressMappingOwner +import com.circle.modularwallets.core.models.WebAuthnAddressMappingOwner import com.circle.modularwallets.core.transports.RpcRequest import com.circle.modularwallets.core.transports.Transport +import com.circle.modularwallets.core.utils.abi.isAddress import com.circle.modularwallets.core.utils.rpc.performJsonRpcRequest +import com.circle.modularwallets.core.utils.rpc.resultToTypeAndJson internal object ModularApiImpl : ModularApi { override suspend fun getAddress( @@ -31,5 +38,47 @@ internal object ModularApiImpl : ModularApi { val result = performJsonRpcRequest(transport, req, ModularWallet::class.java) return result.first } + + override suspend fun createAddressMapping( + transport: Transport, + walletAddress: String, + owners: Array + ): Array { + if (!isAddress(walletAddress)) { + throw BaseError("walletAddress is invalid") + } + if (owners.isEmpty()) { + throw BaseError("At least one owner must be provided") + } + owners.forEachIndexed { index, owner -> + when (owner) { + is EoaAddressMappingOwner -> { + if (!isAddress(owner.identifier.address)) { + throw BaseError("EOA owner at index $index has an invalid address") + } + } + + is WebAuthnAddressMappingOwner -> { + if (owner.identifier.publicKeyX.isBlank() || owner.identifier.publicKeyY.isBlank()) { + throw BaseError("Webauthn owner at index $index must have publicKeyX and publicKeyY") + } + } + + else -> { + throw BaseError("Owner at index $index has an invalid type") + } + } + } + + val req = RpcRequest( + "circle_createAddressMapping", + listOf(CreateAddressMappingReq(walletAddress, owners)) + ) + val rawList = performJsonRpcRequest(transport, req) as ArrayList<*> + val result: Array = rawList.mapNotNull { item -> + resultToTypeAndJson(item, CreateAddressMappingResult::class.java).first + }.toTypedArray() + return result + } } diff --git a/lib/src/main/java/com/circle/modularwallets/core/apis/modular/ModularReqResp.kt b/lib/src/main/java/com/circle/modularwallets/core/apis/modular/ModularReqResp.kt index e08d8e8..05c16bd 100644 --- a/lib/src/main/java/com/circle/modularwallets/core/apis/modular/ModularReqResp.kt +++ b/lib/src/main/java/com/circle/modularwallets/core/apis/modular/ModularReqResp.kt @@ -20,6 +20,7 @@ package com.circle.modularwallets.core.apis.modular import com.circle.modularwallets.core.annotation.ExcludeFromGeneratedCCReport +import com.circle.modularwallets.core.models.AddressMappingOwner import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @@ -39,7 +40,7 @@ data class ModularWallet( @Json(name = "state") val state: String? = null, @Json(name = "name") val name: String? = null, @Json(name = "scaConfiguration") val scaConfiguration: ScaConfiguration, -){ +) { /** * Gets the initialization code from the SCA configuration. * @@ -95,7 +96,6 @@ data class WebauthnOwner( @Json(name = "weight") val weight: Int, ) - internal fun getCreateWalletReq( publicKeyX: String, publicKeyY: String, @@ -117,4 +117,10 @@ internal fun getCreateWalletReq( ), Metadata(name) ) -} \ No newline at end of file +} + +@JsonClass(generateAdapter = true) +internal data class CreateAddressMappingReq( + @Json(name = "walletAddress") val walletAddress: String, + @Json(name = "owners") val owners: Array, +) diff --git a/lib/src/main/java/com/circle/modularwallets/core/apis/util/UtilApi.kt b/lib/src/main/java/com/circle/modularwallets/core/apis/util/UtilApi.kt index 256c9d3..8aeac7a 100644 --- a/lib/src/main/java/com/circle/modularwallets/core/apis/util/UtilApi.kt +++ b/lib/src/main/java/com/circle/modularwallets/core/apis/util/UtilApi.kt @@ -55,7 +55,13 @@ internal interface UtilApi { message: String, signature: String, from: String, - to: String = CIRCLE_WEIGHTED_WEB_AUTHN_MULTISIG_PLUGIN.address + to: String ): Boolean + suspend fun getReplaySafeMessageHash( + transport: Transport, + account: String, + hash: String, + ): String + } \ No newline at end of file diff --git a/lib/src/main/java/com/circle/modularwallets/core/apis/util/UtilApiImpl.kt b/lib/src/main/java/com/circle/modularwallets/core/apis/util/UtilApiImpl.kt index 708f39a..bad7789 100644 --- a/lib/src/main/java/com/circle/modularwallets/core/apis/util/UtilApiImpl.kt +++ b/lib/src/main/java/com/circle/modularwallets/core/apis/util/UtilApiImpl.kt @@ -25,6 +25,7 @@ import com.circle.modularwallets.core.errors.BaseError import com.circle.modularwallets.core.transports.RpcRequest import com.circle.modularwallets.core.transports.Transport import com.circle.modularwallets.core.utils.Logger +import com.circle.modularwallets.core.utils.encoding.bytesToHex import com.circle.modularwallets.core.utils.encoding.hexToBigInteger import com.circle.modularwallets.core.utils.encoding.toSha3Bytes import com.circle.modularwallets.core.utils.rpc.performJsonRpcRequest @@ -129,10 +130,54 @@ internal object UtilApiImpl : UtilApi { object : TypeReference() {}) ) val data = FunctionEncoder.encode(function) - Logger.d(msg = "isValidSignature > call") + Logger.d( + msg = """ + isValidSignature > call + Message: $message + Digest: ${bytesToHex(digest)} + Signature: $signature + From: $from + To: $to + """.trimIndent() + ) val resp = call(transport, from, to, data) val decoded = FunctionReturnDecoder.decode(resp, function.outputParameters) return EIP1271_VALID_SIGNATURE.contentEquals(decoded[0].value as ByteArray) } + + override suspend fun getReplaySafeMessageHash( + transport: Transport, + account: String, + hash: String + ): String { + val byte32Hash: Bytes32 + try { + byte32Hash = Bytes32(Numeric.hexStringToByteArray(hash)) + } catch (e: UnsupportedOperationException) { + throw BaseError("Invalid hash: $hash") + } + val function = Function( + "getReplaySafeMessageHash", + listOf>(Address(account), byte32Hash), + listOf>( + object : TypeReference() {}) + ) + val data = FunctionEncoder.encode(function) + val resp = call(transport, account, account, data) + val decoded = FunctionReturnDecoder.decode(resp, function.outputParameters) + if (decoded.isEmpty()) { + throw BaseError("Invalid account or empty response for: $account. Response: $resp") + } + val result = bytesToHex(decoded[0].value as ByteArray) + Logger.d( + msg = """ + getReplaySafeMessageHash > call + Account: $account + Hash: $hash + Result: $result + """.trimIndent() + ) + return result + } } diff --git a/lib/src/main/java/com/circle/modularwallets/core/chains/Base.kt b/lib/src/main/java/com/circle/modularwallets/core/chains/Base.kt new file mode 100644 index 0000000..e1b1b5c --- /dev/null +++ b/lib/src/main/java/com/circle/modularwallets/core/chains/Base.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2025 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at. + * + * Http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.circle.modularwallets.core.chains + +object Base : Chain() { + override val chainId: Long + get() = 8453 +} \ No newline at end of file diff --git a/lib/src/main/java/com/circle/modularwallets/core/chains/BaseSepolia.kt b/lib/src/main/java/com/circle/modularwallets/core/chains/BaseSepolia.kt new file mode 100644 index 0000000..d7fb439 --- /dev/null +++ b/lib/src/main/java/com/circle/modularwallets/core/chains/BaseSepolia.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2025 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at. + * + * Http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.circle.modularwallets.core.chains + +object BaseSepolia : Chain() { + override val chainId: Long + get() = 84532 +} \ No newline at end of file diff --git a/lib/src/main/java/com/circle/modularwallets/core/chains/Optimism.kt b/lib/src/main/java/com/circle/modularwallets/core/chains/Optimism.kt new file mode 100644 index 0000000..ba48761 --- /dev/null +++ b/lib/src/main/java/com/circle/modularwallets/core/chains/Optimism.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2025 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at. + * + * Http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.circle.modularwallets.core.chains + +object Optimism : Chain() { + override val chainId: Long + get() = 10 +} \ No newline at end of file diff --git a/lib/src/main/java/com/circle/modularwallets/core/chains/OptimismSepolia.kt b/lib/src/main/java/com/circle/modularwallets/core/chains/OptimismSepolia.kt new file mode 100644 index 0000000..8a1919d --- /dev/null +++ b/lib/src/main/java/com/circle/modularwallets/core/chains/OptimismSepolia.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2025 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at. + * + * Http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.circle.modularwallets.core.chains + +object OptimismSepolia : Chain() { + override val chainId: Long + get() = 11155420 +} \ No newline at end of file diff --git a/lib/src/main/java/com/circle/modularwallets/core/chains/Unichain.kt b/lib/src/main/java/com/circle/modularwallets/core/chains/Unichain.kt new file mode 100644 index 0000000..dfa7f5b --- /dev/null +++ b/lib/src/main/java/com/circle/modularwallets/core/chains/Unichain.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2025 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at. + * + * Http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.circle.modularwallets.core.chains + +object Unichain : Chain() { + override val chainId: Long + get() = 130 +} \ No newline at end of file diff --git a/lib/src/main/java/com/circle/modularwallets/core/chains/UnichainSepolia.kt b/lib/src/main/java/com/circle/modularwallets/core/chains/UnichainSepolia.kt new file mode 100644 index 0000000..5f87517 --- /dev/null +++ b/lib/src/main/java/com/circle/modularwallets/core/chains/UnichainSepolia.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2025 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at. + * + * Http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.circle.modularwallets.core.chains + +object UnichainSepolia : Chain() { + override val chainId: Long + get() = 1301 +} \ No newline at end of file diff --git a/lib/src/main/java/com/circle/modularwallets/core/clients/BundlerClient.kt b/lib/src/main/java/com/circle/modularwallets/core/clients/BundlerClient.kt index f0e7758..93ed258 100644 --- a/lib/src/main/java/com/circle/modularwallets/core/clients/BundlerClient.kt +++ b/lib/src/main/java/com/circle/modularwallets/core/clients/BundlerClient.kt @@ -25,12 +25,15 @@ import com.circle.modularwallets.core.apis.bundler.BundlerApi import com.circle.modularwallets.core.apis.bundler.BundlerApiImpl import com.circle.modularwallets.core.apis.bundler.toResult import com.circle.modularwallets.core.apis.bundler.toUserOperationReceipt +import com.circle.modularwallets.core.apis.modular.ModularApiImpl import com.circle.modularwallets.core.apis.public.PublicApi import com.circle.modularwallets.core.apis.public.PublicApiImpl import com.circle.modularwallets.core.apis.util.UtilApi import com.circle.modularwallets.core.apis.util.UtilApiImpl import com.circle.modularwallets.core.chains.Chain +import com.circle.modularwallets.core.models.AddressMappingOwner import com.circle.modularwallets.core.models.Block +import com.circle.modularwallets.core.models.CreateAddressMappingResult import com.circle.modularwallets.core.models.EncodeCallDataArg import com.circle.modularwallets.core.models.EntryPoint import com.circle.modularwallets.core.models.EstimateFeesPerGasResult @@ -388,4 +391,20 @@ class BundlerClient(chain: Chain, transport: Transport) : Client(chain, transpor ): Block { return pubApi.getBlock(transport, includeTransactions, blockTag) } + + /** + * Creates an address mapping for recovery. + * + * @param walletAddress The Circle smart wallet address. + * @param owners The owners of the wallet. + * @return The response from adding an address mapping. + */ + @Throws(Exception::class) + @JvmOverloads + suspend fun createAddressMapping( + walletAddress: String, + owners: Array + ): Array { + return ModularApiImpl.createAddressMapping(transport, walletAddress, owners) + } } \ No newline at end of file diff --git a/lib/src/main/java/com/circle/modularwallets/core/constants/AbiConstants.kt b/lib/src/main/java/com/circle/modularwallets/core/constants/AbiConstants.kt index 24a07b1..8ba1f03 100644 --- a/lib/src/main/java/com/circle/modularwallets/core/constants/AbiConstants.kt +++ b/lib/src/main/java/com/circle/modularwallets/core/constants/AbiConstants.kt @@ -27,8 +27,15 @@ internal val CONTRACT_ADDRESS: Map = mapOf( Token.Arbitrum_USDC.name to "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", Token.Arbitrum_ARB.name to "0x912CE59144191C1204E64559FE8253a0e49E6548", Token.ArbitrumSepolia_USDC.name to "0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d", + Token.Base_USDC.name to "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + Token.BaseSepolia_USDC.name to "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + Token.Optimism_USDC.name to "0x0b2c639c533813f4aa9d7837caf62653d097ff85", + Token.Optimism_OP.name to "0x4200000000000000000000000000000000000042", + Token.OptimismSepolia_USDC.name to "0x5fd84259d66Cd46123540766Be93DFE6D43130D7", Token.Polygon_USDC.name to "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", Token.PolygonAmoy_USDC.name to "0x41e94eb019c0762f9bfcf9fb1e58725bfb0e7582", + Token.Unichain_USDC.name to "0x078D782b760474a361dDA0AF3839290b0EF57AD6", + Token.UnichainSepolia_USDC.name to "0x31d0220469e10c4E71834a79b1f276d740d3768F", ) val CIRCLE_PLUGIN_ADD_OWNERS_ABI = """ [ diff --git a/lib/src/main/java/com/circle/modularwallets/core/models/OwnerModels.kt b/lib/src/main/java/com/circle/modularwallets/core/models/OwnerModels.kt new file mode 100644 index 0000000..a2ea5e4 --- /dev/null +++ b/lib/src/main/java/com/circle/modularwallets/core/models/OwnerModels.kt @@ -0,0 +1,107 @@ +/* + * Copyright 2025 Circle Internet Group, Inc. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at. + * + * Http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.circle.modularwallets.core.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * The owner identifier type for address mapping. + */ +enum class OwnerIdentifierType(val value: String) { + EOA("EOAOWNER"), + WebAuthn("WEBAUTHOWNER"); +} + +/** + * The EOA identifier for address mapping. + */ +@JsonClass(generateAdapter = true) +open class EOAIdentifier( + @Json(name = "address") + open val address: String +) + +/** + * The WebAuthn identifier for address mapping. + */ +@JsonClass(generateAdapter = true) +open class WebAuthnIdentifier( + @Json(name = "publicKeyX") + open val publicKeyX: String, + @Json(name = "publicKeyY") + open val publicKeyY: String +) + +/** + * The base case of owner for address mapping. + */ +@JsonClass(generateAdapter = true) +open class AddressMappingOwner( + /** + * The owner identifier type for address mapping. See [OwnerIdentifierType]. + */ + @Json(name = "type") val type: String +) + +/** + * The EOA owner for address mapping. + */ +@JsonClass(generateAdapter = true) +data class EoaAddressMappingOwner( + @Json(name = "identifier") val identifier: EOAIdentifier, +) : AddressMappingOwner(OwnerIdentifierType.EOA.value) + +/** + * The WebAuthn owner for address mapping. + */ +@JsonClass(generateAdapter = true) +data class WebAuthnAddressMappingOwner( + @Json(name = "identifier") val identifier: WebAuthnIdentifier, +) : AddressMappingOwner(OwnerIdentifierType.WebAuthn.value) + +/** + * The response from adding an address mapping. + */ +@JsonClass(generateAdapter = true) +data class CreateAddressMappingResult( + /** + * The mapping ID. + */ + @Json(name = "id") val id: String, + /** + * The blockchain identifier. + */ + @Json(name = "blockchain") val blockchain: String, + /** + * The owner information. + */ + @Json(name = "owner") val owner: AddressMappingOwner, + /** + * The wallet address. + */ + @Json(name = "walletAddress") val walletAddress: String, + /** + * The creation date (ISO 8601 format). + */ + @Json(name = "createDate") val createDate: String, + /** + * The last update date (ISO 8601 format). + */ + @Json(name = "updateDate") val updateDate: String, +) \ No newline at end of file diff --git a/lib/src/main/java/com/circle/modularwallets/core/models/Token.kt b/lib/src/main/java/com/circle/modularwallets/core/models/Token.kt index f03ea5e..75f79b7 100644 --- a/lib/src/main/java/com/circle/modularwallets/core/models/Token.kt +++ b/lib/src/main/java/com/circle/modularwallets/core/models/Token.kt @@ -27,6 +27,13 @@ enum class Token { Arbitrum_USDC, Arbitrum_ARB, ArbitrumSepolia_USDC, + Base_USDC, + BaseSepolia_USDC, + Optimism_USDC, + Optimism_OP, + OptimismSepolia_USDC, Polygon_USDC, - PolygonAmoy_USDC; + PolygonAmoy_USDC, + Unichain_USDC, + UnichainSepolia_USDC; } diff --git a/lib/src/main/java/com/circle/modularwallets/core/utils/JsonConversionUtils.kt b/lib/src/main/java/com/circle/modularwallets/core/utils/JsonConversionUtils.kt index ece1760..77b0b54 100644 --- a/lib/src/main/java/com/circle/modularwallets/core/utils/JsonConversionUtils.kt +++ b/lib/src/main/java/com/circle/modularwallets/core/utils/JsonConversionUtils.kt @@ -18,6 +18,7 @@ package com.circle.modularwallets.core.utils +import com.circle.modularwallets.core.utils.rpc.getMoshi import com.google.gson.Gson import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi @@ -33,7 +34,7 @@ internal fun toJson(obj: Any?): String { } internal fun fromJson(jsonString: String, type: Class): T? { - val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() + val moshi = getMoshi() val adapter: JsonAdapter = moshi.adapter(type) return adapter.fromJson(jsonString) } diff --git a/lib/src/main/java/com/circle/modularwallets/core/utils/abi/EncodeCallDataUtils.kt b/lib/src/main/java/com/circle/modularwallets/core/utils/abi/EncodeCallDataUtils.kt index 7f33a53..51f5472 100644 --- a/lib/src/main/java/com/circle/modularwallets/core/utils/abi/EncodeCallDataUtils.kt +++ b/lib/src/main/java/com/circle/modularwallets/core/utils/abi/EncodeCallDataUtils.kt @@ -55,7 +55,7 @@ internal fun encodeCallData(arg: EncodeCallDataArg): String { return FunctionEncoder.encode(function) } -private fun isAddress(to: String): Boolean { +internal fun isAddress(to: String): Boolean { val addressRegex = Regex("^0x[a-fA-F0-9]{40}$") return addressRegex.matches(to) } diff --git a/lib/src/main/java/com/circle/modularwallets/core/utils/rpc/ReqRespUtils.kt b/lib/src/main/java/com/circle/modularwallets/core/utils/rpc/ReqRespUtils.kt index 7f5de00..7d37cb4 100644 --- a/lib/src/main/java/com/circle/modularwallets/core/utils/rpc/ReqRespUtils.kt +++ b/lib/src/main/java/com/circle/modularwallets/core/utils/rpc/ReqRespUtils.kt @@ -44,6 +44,10 @@ import com.circle.modularwallets.core.errors.UnauthorizedProviderError import com.circle.modularwallets.core.errors.UnknownRpcError import com.circle.modularwallets.core.errors.UnsupportedProviderMethodError import com.circle.modularwallets.core.errors.UserRejectedRequestError +import com.circle.modularwallets.core.models.AddressMappingOwner +import com.circle.modularwallets.core.models.EoaAddressMappingOwner +import com.circle.modularwallets.core.models.OwnerIdentifierType +import com.circle.modularwallets.core.models.WebAuthnAddressMappingOwner import com.circle.modularwallets.core.transports.HttpError import com.circle.modularwallets.core.transports.RpcRequest import com.circle.modularwallets.core.transports.RpcResponse @@ -55,6 +59,7 @@ import com.squareup.moshi.FromJson import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi import com.squareup.moshi.ToJson +import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import okhttp3.Interceptor import okhttp3.Request @@ -64,9 +69,8 @@ import okio.Buffer import java.math.BigDecimal import java.math.BigInteger - internal fun resultToTypeAndJson(result: Any, type: Class): Pair { - val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() + val moshi = getMoshi() val jsonAdapter = moshi.adapter(Map::class.java) val jsonString = jsonAdapter.toJson(result as Map<*, *>?) val adapter: JsonAdapter = moshi.adapter(type) @@ -120,7 +124,7 @@ internal suspend fun performJsonRpcRequest( val body = call.body() body?.error?.let { val req = call.raw().request - val rpcRequestError= RpcRequestError( + val rpcRequestError = RpcRequestError( getBodyString(req.body), it, url = req.url.toString() ) @@ -137,14 +141,26 @@ internal suspend fun performJsonRpcRequest( ) } -internal fun getMoshi(): Moshi { - return Moshi.Builder() +private val moshiInstance: Moshi by lazy { + Moshi.Builder() .add(BigDecimalAdapter) .add(BigIntegerAdapter) + .add( + PolymorphicJsonAdapterFactory.of(AddressMappingOwner::class.java, "type") + .withSubtype(EoaAddressMappingOwner::class.java, OwnerIdentifierType.EOA.value) + .withSubtype( + WebAuthnAddressMappingOwner::class.java, + OwnerIdentifierType.WebAuthn.value + ) + ) .add(KotlinJsonAdapterFactory()) .build() } +internal fun getMoshi(): Moshi { + return moshiInstance +} + @ExcludeFromGeneratedCCReport internal object BigDecimalAdapter { @FromJson From 1b2fb8868bb0babbf96d0d17a51e9a8fb562de20 Mon Sep 17 00:00:00 2001 From: undefined Date: Wed, 9 Apr 2025 05:16:05 +0000 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=94=84=20synced=20local=20'./build.gr?= =?UTF-8?q?adle.kts'=20with=20remote=20'./build.gradle.kts'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 1fcd216..352b37c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -17,4 +17,4 @@ subprojects { println("jacoco file not found.") } } -} \ No newline at end of file +}