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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ subprojects {
println("jacoco file not found.")
}
}
}
}
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
7 changes: 4 additions & 3 deletions lib/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -195,4 +196,4 @@ afterEvaluate {
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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<Type<*>>(
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<AddressMappingOwner>
): Array<CreateAddressMappingResult>

}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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<AddressMappingOwner>
): Array<CreateAddressMappingResult> {
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<CreateAddressMappingResult> = rawList.mapNotNull { item ->
resultToTypeAndJson(item, CreateAddressMappingResult::class.java).first
}.toTypedArray()
return result
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.
*
Expand Down Expand Up @@ -95,7 +96,6 @@ data class WebauthnOwner(
@Json(name = "weight") val weight: Int,
)


internal fun getCreateWalletReq(
publicKeyX: String,
publicKeyY: String,
Expand All @@ -117,4 +117,10 @@ internal fun getCreateWalletReq(
),
Metadata(name)
)
}
}

@JsonClass(generateAdapter = true)
internal data class CreateAddressMappingReq(
@Json(name = "walletAddress") val walletAddress: String,
@Json(name = "owners") val owners: Array<AddressMappingOwner>,
)
Original file line number Diff line number Diff line change
Expand Up @@ -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

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -129,10 +130,54 @@ internal object UtilApiImpl : UtilApi {
object : TypeReference<Bytes4>() {})
)
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<Type<*>>(Address(account), byte32Hash),
listOf<TypeReference<*>>(
object : TypeReference<Bytes32>() {})
)
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
}
}

24 changes: 24 additions & 0 deletions lib/src/main/java/com/circle/modularwallets/core/chains/Base.kt
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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
}
Loading