Skip to content

Commit 987ec70

Browse files
🔄 synced file(s) with circlefin/modularwallets-android-sdk-internal (#6)
synced local file(s) with [circlefin/modularwallets-android-sdk-internal](https://github.com/circlefin/modularwallets-android-sdk-internal). <details> <summary>Changed files</summary> <ul> <li>synced local directory <code>./gradle/</code> with remote directory <code>./gradle/</code></li><li>synced local directory <code>./lib/</code> with remote directory <code>./lib/</code></li><li>synced local <code>./build.gradle.kts</code> with remote <code>./build.gradle.kts</code></li> </ul> </details> --- This PR was created automatically by the [repo-file-sync-action](https://github.com/BetaHuhn/repo-file-sync-action) workflow run [#14349535640](https://github.com/circlefin/modularwallets-android-sdk-internal/actions/runs/14349535640)
1 parent 9dfecfb commit 987ec70

File tree

22 files changed

+436
-84
lines changed

22 files changed

+436
-84
lines changed

‎build.gradle.kts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,4 @@ subprojects {
1717
println("jacoco file not found.")
1818
}
1919
}
20-
}
20+
}

‎gradle/libs.versions.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ okhttp3-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-
3636
kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinx-coroutines-test" }
3737
moshi-kotlin = { group = "com.squareup.moshi", name = "moshi-kotlin", version.ref = "moshi" }
3838
moshi-ksp = { group = "com.squareup.moshi", name = "moshi-kotlin-codegen", version.ref = "moshi" }
39+
moshi-adapters = { group = "com.squareup.moshi", name = "moshi-adapters", version.ref = "moshi" }
3940
retrofit2-converter-moshi = { group = "com.squareup.retrofit2", name = "converter-moshi", version.ref = "retrofit2" }
4041
androidx-credentials = { group = "androidx.credentials", name = "credentials", version.ref = "androidx-credentials" }
4142
androidx-credentials-auth = { group = "androidx.credentials", name = "credentials-play-services-auth", version.ref = "androidx-credentials" }

‎lib/build.gradle.kts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ plugins {
1515

1616
extra.apply {
1717
set("versionMajor", 0)
18-
set("versionMedium", 0)
19-
set("versionMinorPublished", 204) // should increment after public release
18+
set("versionMedium", 1)
19+
set("versionMinorPublished", 0)
2020
set("libraryId", libraryId())
2121
set("libraryGroupId", libraryId())
2222
set("libraryArtifactId", libraryArtifactId())
@@ -147,6 +147,7 @@ dependencies {
147147
implementation(libs.androidx.credentials.auth)
148148
implementation(libs.retrofit2.converter.moshi)
149149
implementation(libs.moshi.kotlin)
150+
implementation(libs.moshi.adapters)
150151
implementation(libs.web3j)
151152
implementation(libs.retrofit2.retrofit)
152153
implementation(libs.retrofit2.converter.gson)
@@ -195,4 +196,4 @@ afterEvaluate {
195196
}
196197
}
197198
}
198-
}
199+
}

‎lib/src/main/java/com/circle/modularwallets/core/accounts/CircleSmartAccount.kt‎

Lines changed: 3 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -286,9 +286,7 @@ class CircleSmartAccount(
286286
@Throws(Exception::class)
287287
override suspend fun sign(context: Context, hex: String): String {
288288
val digest = toSha3Bytes(hex)
289-
val hash = getReplaySafeHash(
290-
client.chain.chainId, getAddress(), bytesToHex(digest)
291-
)
289+
val hash = UtilApiImpl.getReplaySafeMessageHash(client.transport, getAddress(), bytesToHex(digest))
292290
val signResult = owner.sign(context, hash)
293291
val signature = encodePackedForSignature(
294292
signResult,
@@ -309,9 +307,7 @@ class CircleSmartAccount(
309307
@Throws(Exception::class)
310308
override suspend fun signMessage(context: Context, message: String): String {
311309
val digest = toSha3Bytes(hashMessage(message.toByteArray()))
312-
val hash = getReplaySafeHash(
313-
client.chain.chainId, getAddress(), bytesToHex(digest)
314-
)
310+
val hash = UtilApiImpl.getReplaySafeMessageHash(client.transport, getAddress(), bytesToHex(digest))
315311
val signResult = owner.sign(context, hash)
316312
val signature = encodePackedForSignature(
317313
signResult,
@@ -332,9 +328,7 @@ class CircleSmartAccount(
332328
@Throws(Exception::class)
333329
override suspend fun signTypedData(context: Context, typedData: String): String {
334330
val digest = toSha3Bytes(hashTypedData(typedData))
335-
val hash = getReplaySafeHash(
336-
client.chain.chainId, getAddress(), bytesToHex(digest)
337-
)
331+
val hash = UtilApiImpl.getReplaySafeMessageHash(client.transport, getAddress(), bytesToHex(digest))
338332
val signResult = owner.sign(context, hash)
339333
val signature = encodePackedForSignature(
340334
signResult,
@@ -380,64 +374,6 @@ class CircleSmartAccount(
380374

381375
}
382376

383-
internal fun getReplaySafeHash(
384-
chainId: Long,
385-
account: String,
386-
hash: String,
387-
verifyingContract: String = CIRCLE_WEIGHTED_WEB_AUTHN_MULTISIG_PLUGIN.address,
388-
): String {
389-
val prefix = Numeric.hexStringToByteArray(EIP712_PREFIX)
390-
val domainSeparatorTypeHash =
391-
toSha3Bytes(REPLAY_SAFE_HASH_V1.domainSeparatorType)
392-
393-
val domainSeparator = toSha3Bytes(
394-
encodeAbiParameters(
395-
listOf(
396-
Bytes32(domainSeparatorTypeHash),
397-
Bytes32(getModuleIdHash()),
398-
Uint256(chainId),
399-
Address(verifyingContract),
400-
Bytes32(pad(toBytes(account), isRight = true)),
401-
)
402-
)
403-
)
404-
405-
val structHash = toSha3Bytes(
406-
encodeAbiParameters(
407-
listOf(
408-
Bytes32(getModuleTypeHash()),
409-
Bytes32(Numeric.hexStringToByteArray(hash))
410-
)
411-
)
412-
)
413-
return bytesToHex(
414-
Hash.sha3(
415-
concat(
416-
prefix,
417-
domainSeparator,
418-
structHash
419-
)
420-
)
421-
)
422-
}
423-
424-
internal fun getModuleIdHash(): ByteArray {
425-
return toSha3Bytes(
426-
encodePacked(
427-
listOf<Type<*>>(
428-
Utf8String(REPLAY_SAFE_HASH_V1.name),
429-
Utf8String(REPLAY_SAFE_HASH_V1.version),
430-
)
431-
)
432-
)
433-
}
434-
435-
internal fun getModuleTypeHash(): ByteArray {
436-
return toSha3Bytes(
437-
REPLAY_SAFE_HASH_V1.moduleType
438-
)
439-
}
440-
441377
internal fun encodePackedForSignature(
442378
signResult: SignResult,
443379
publicKey: String,

‎lib/src/main/java/com/circle/modularwallets/core/apis/modular/ModularApi.kt‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,16 @@
1818

1919
package com.circle.modularwallets.core.apis.modular
2020

21+
import com.circle.modularwallets.core.models.AddressMappingOwner
22+
import com.circle.modularwallets.core.models.CreateAddressMappingResult
2123
import com.circle.modularwallets.core.transports.Transport
2224

2325
internal interface ModularApi {
2426
suspend fun getAddress(transport: Transport, getAddressReq: GetAddressReq): ModularWallet
27+
suspend fun createAddressMapping(
28+
transport: Transport,
29+
walletAddress: String,
30+
owners: Array<AddressMappingOwner>
31+
): Array<CreateAddressMappingResult>
2532

2633
}

‎lib/src/main/java/com/circle/modularwallets/core/apis/modular/ModularApiImpl.kt‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,16 @@
1818

1919
package com.circle.modularwallets.core.apis.modular
2020

21+
import com.circle.modularwallets.core.errors.BaseError
22+
import com.circle.modularwallets.core.models.AddressMappingOwner
23+
import com.circle.modularwallets.core.models.CreateAddressMappingResult
24+
import com.circle.modularwallets.core.models.EoaAddressMappingOwner
25+
import com.circle.modularwallets.core.models.WebAuthnAddressMappingOwner
2126
import com.circle.modularwallets.core.transports.RpcRequest
2227
import com.circle.modularwallets.core.transports.Transport
28+
import com.circle.modularwallets.core.utils.abi.isAddress
2329
import com.circle.modularwallets.core.utils.rpc.performJsonRpcRequest
30+
import com.circle.modularwallets.core.utils.rpc.resultToTypeAndJson
2431

2532
internal object ModularApiImpl : ModularApi {
2633
override suspend fun getAddress(
@@ -31,5 +38,47 @@ internal object ModularApiImpl : ModularApi {
3138
val result = performJsonRpcRequest(transport, req, ModularWallet::class.java)
3239
return result.first
3340
}
41+
42+
override suspend fun createAddressMapping(
43+
transport: Transport,
44+
walletAddress: String,
45+
owners: Array<AddressMappingOwner>
46+
): Array<CreateAddressMappingResult> {
47+
if (!isAddress(walletAddress)) {
48+
throw BaseError("walletAddress is invalid")
49+
}
50+
if (owners.isEmpty()) {
51+
throw BaseError("At least one owner must be provided")
52+
}
53+
owners.forEachIndexed { index, owner ->
54+
when (owner) {
55+
is EoaAddressMappingOwner -> {
56+
if (!isAddress(owner.identifier.address)) {
57+
throw BaseError("EOA owner at index $index has an invalid address")
58+
}
59+
}
60+
61+
is WebAuthnAddressMappingOwner -> {
62+
if (owner.identifier.publicKeyX.isBlank() || owner.identifier.publicKeyY.isBlank()) {
63+
throw BaseError("Webauthn owner at index $index must have publicKeyX and publicKeyY")
64+
}
65+
}
66+
67+
else -> {
68+
throw BaseError("Owner at index $index has an invalid type")
69+
}
70+
}
71+
}
72+
73+
val req = RpcRequest(
74+
"circle_createAddressMapping",
75+
listOf(CreateAddressMappingReq(walletAddress, owners))
76+
)
77+
val rawList = performJsonRpcRequest(transport, req) as ArrayList<*>
78+
val result: Array<CreateAddressMappingResult> = rawList.mapNotNull { item ->
79+
resultToTypeAndJson(item, CreateAddressMappingResult::class.java).first
80+
}.toTypedArray()
81+
return result
82+
}
3483
}
3584

‎lib/src/main/java/com/circle/modularwallets/core/apis/modular/ModularReqResp.kt‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
package com.circle.modularwallets.core.apis.modular
2121

2222
import com.circle.modularwallets.core.annotation.ExcludeFromGeneratedCCReport
23+
import com.circle.modularwallets.core.models.AddressMappingOwner
2324
import com.squareup.moshi.Json
2425
import com.squareup.moshi.JsonClass
2526

@@ -39,7 +40,7 @@ data class ModularWallet(
3940
@Json(name = "state") val state: String? = null,
4041
@Json(name = "name") val name: String? = null,
4142
@Json(name = "scaConfiguration") val scaConfiguration: ScaConfiguration,
42-
){
43+
) {
4344
/**
4445
* Gets the initialization code from the SCA configuration.
4546
*
@@ -95,7 +96,6 @@ data class WebauthnOwner(
9596
@Json(name = "weight") val weight: Int,
9697
)
9798

98-
9999
internal fun getCreateWalletReq(
100100
publicKeyX: String,
101101
publicKeyY: String,
@@ -117,4 +117,10 @@ internal fun getCreateWalletReq(
117117
),
118118
Metadata(name)
119119
)
120-
}
120+
}
121+
122+
@JsonClass(generateAdapter = true)
123+
internal data class CreateAddressMappingReq(
124+
@Json(name = "walletAddress") val walletAddress: String,
125+
@Json(name = "owners") val owners: Array<AddressMappingOwner>,
126+
)

‎lib/src/main/java/com/circle/modularwallets/core/apis/util/UtilApi.kt‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,13 @@ internal interface UtilApi {
5555
message: String,
5656
signature: String,
5757
from: String,
58-
to: String = CIRCLE_WEIGHTED_WEB_AUTHN_MULTISIG_PLUGIN.address
58+
to: String
5959
): Boolean
6060

61+
suspend fun getReplaySafeMessageHash(
62+
transport: Transport,
63+
account: String,
64+
hash: String,
65+
): String
66+
6167
}

‎lib/src/main/java/com/circle/modularwallets/core/apis/util/UtilApiImpl.kt‎

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import com.circle.modularwallets.core.errors.BaseError
2525
import com.circle.modularwallets.core.transports.RpcRequest
2626
import com.circle.modularwallets.core.transports.Transport
2727
import com.circle.modularwallets.core.utils.Logger
28+
import com.circle.modularwallets.core.utils.encoding.bytesToHex
2829
import com.circle.modularwallets.core.utils.encoding.hexToBigInteger
2930
import com.circle.modularwallets.core.utils.encoding.toSha3Bytes
3031
import com.circle.modularwallets.core.utils.rpc.performJsonRpcRequest
@@ -129,10 +130,54 @@ internal object UtilApiImpl : UtilApi {
129130
object : TypeReference<Bytes4>() {})
130131
)
131132
val data = FunctionEncoder.encode(function)
132-
Logger.d(msg = "isValidSignature > call")
133+
Logger.d(
134+
msg = """
135+
isValidSignature > call
136+
Message: $message
137+
Digest: ${bytesToHex(digest)}
138+
Signature: $signature
139+
From: $from
140+
To: $to
141+
""".trimIndent()
142+
)
133143
val resp = call(transport, from, to, data)
134144
val decoded = FunctionReturnDecoder.decode(resp, function.outputParameters)
135145
return EIP1271_VALID_SIGNATURE.contentEquals(decoded[0].value as ByteArray)
136146
}
147+
148+
override suspend fun getReplaySafeMessageHash(
149+
transport: Transport,
150+
account: String,
151+
hash: String
152+
): String {
153+
val byte32Hash: Bytes32
154+
try {
155+
byte32Hash = Bytes32(Numeric.hexStringToByteArray(hash))
156+
} catch (e: UnsupportedOperationException) {
157+
throw BaseError("Invalid hash: $hash")
158+
}
159+
val function = Function(
160+
"getReplaySafeMessageHash",
161+
listOf<Type<*>>(Address(account), byte32Hash),
162+
listOf<TypeReference<*>>(
163+
object : TypeReference<Bytes32>() {})
164+
)
165+
val data = FunctionEncoder.encode(function)
166+
val resp = call(transport, account, account, data)
167+
val decoded = FunctionReturnDecoder.decode(resp, function.outputParameters)
168+
if (decoded.isEmpty()) {
169+
throw BaseError("Invalid account or empty response for: $account. Response: $resp")
170+
}
171+
val result = bytesToHex(decoded[0].value as ByteArray)
172+
Logger.d(
173+
msg = """
174+
getReplaySafeMessageHash > call
175+
Account: $account
176+
Hash: $hash
177+
Result: $result
178+
""".trimIndent()
179+
)
180+
return result
181+
}
137182
}
138183

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright 2025 Circle Internet Group, Inc. All rights reserved.
3+
*
4+
* SPDX-License-Identifier: Apache-2.0.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at.
9+
*
10+
* Http://www.apache.org/licenses/LICENSE-2.0.
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package com.circle.modularwallets.core.chains
20+
21+
object Base : Chain() {
22+
override val chainId: Long
23+
get() = 8453
24+
}

0 commit comments

Comments
 (0)