Skip to content

Commit 9cf1807

Browse files
🔄 synced file(s) with circlefin/modularwallets-android-sdk-internal (#8)
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>./lib/</code> with remote directory <code>./lib/</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 [#15059729997](https://github.com/circlefin/modularwallets-android-sdk-internal/actions/runs/15059729997)
1 parent e07d2ed commit 9cf1807

File tree

14 files changed

+1031
-264
lines changed

14 files changed

+1031
-264
lines changed

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

Lines changed: 64 additions & 252 deletions
Large diffs are not rendered by default.
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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.accounts
20+
21+
import android.content.Context
22+
import com.circle.modularwallets.core.accounts.implementations.Web3jLocalAccount
23+
import org.web3j.crypto.Credentials
24+
25+
/**
26+
* Creates a [LocalAccount] instance from a hexadecimal private key string.
27+
*
28+
* The underlying account implementation is [Web3jLocalAccount].
29+
*
30+
* @param privateKey The private key as a hexadecimal string.
31+
* @return A [LocalAccount] instance derived from the provided private key.
32+
*/
33+
fun privateKeyToAccount(privateKey: String): LocalAccount {
34+
val credentials = Credentials.create(privateKey)
35+
return LocalAccount(Web3jLocalAccount(credentials))
36+
}
37+
38+
/**
39+
* Represents a local account with signing capabilities.
40+
*
41+
* Instances are typically created via factory functions like [mnemonicToAccount] or [privateKeyToAccount].
42+
*
43+
* @param delegate The underlying [Account] instance that this [LocalAccount] will delegate
44+
* its operations to. This delegate is responsible for the actual cryptographic
45+
* operations and key management.
46+
*/
47+
class LocalAccount(
48+
private val delegate: Account<String>
49+
) : Account<String>() {
50+
51+
/**
52+
* Retrieves the address of the local account.
53+
*
54+
* @return The address of the local account.
55+
*/
56+
override fun getAddress(): String {
57+
return delegate.getAddress()
58+
}
59+
60+
/**
61+
* Signs the given hex string.
62+
*
63+
* @param context The context in which the signing operation is performed.
64+
* @param hex The hex string to be signed.
65+
* @return The signed hex string.
66+
*/
67+
override suspend fun sign(context: Context, hex: String): String {
68+
return delegate.sign(context, hex)
69+
}
70+
71+
/**
72+
* Signs the given message.
73+
*
74+
* @param context The context in which the signing operation is performed.
75+
* @param message The message to be signed.
76+
* @return The signed message.
77+
*/
78+
override suspend fun signMessage(context: Context, message: String): String {
79+
return delegate.signMessage(context, message)
80+
}
81+
82+
/**
83+
* Signs the given typed data.
84+
*
85+
* @param context The context in which the signing operation is performed.
86+
* @param typedData The typed data to be signed.
87+
* @return The signed typed data.
88+
*/
89+
override suspend fun signTypedData(context: Context, typedData: String): String {
90+
return delegate.signTypedData(context, typedData)
91+
}
92+
}
93+
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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.accounts.implementations
20+
21+
import android.content.Context
22+
import com.circle.modularwallets.core.apis.modular.ModularWallet
23+
import com.circle.modularwallets.core.clients.Client
24+
import com.circle.modularwallets.core.transports.Transport
25+
26+
interface CircleSmartAccountDelegate {
27+
28+
suspend fun getModularWalletAddress(
29+
transport: Transport, version: String, name: String? = null
30+
): ModularWallet
31+
32+
suspend fun getComputeWallet(
33+
client: Client,
34+
version: String
35+
): ModularWallet
36+
37+
fun getFactoryData(): String
38+
39+
suspend fun signAndWrap(
40+
context: Context,
41+
hash: String,
42+
hasUserOpGas: Boolean
43+
): String
44+
}
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
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.accounts.implementations
20+
21+
import android.content.Context
22+
import com.circle.modularwallets.core.accounts.LocalAccount
23+
import com.circle.modularwallets.core.apis.modular.ModularApiImpl
24+
import com.circle.modularwallets.core.apis.modular.ModularWallet
25+
import com.circle.modularwallets.core.apis.modular.ScaConfiguration
26+
import com.circle.modularwallets.core.apis.modular.getCreateWalletReq
27+
import com.circle.modularwallets.core.apis.util.UtilApiImpl
28+
import com.circle.modularwallets.core.clients.Client
29+
import com.circle.modularwallets.core.constants.CIRCLE_WEIGHTED_WEB_AUTHN_MULTISIG_PLUGIN
30+
import com.circle.modularwallets.core.constants.FACTORY
31+
import com.circle.modularwallets.core.constants.OWNER_WEIGHT
32+
import com.circle.modularwallets.core.constants.SALT
33+
import com.circle.modularwallets.core.constants.SIG_TYPE_FLAG_DIGEST
34+
import com.circle.modularwallets.core.constants.THRESHOLD_WEIGHT
35+
import com.circle.modularwallets.core.transports.Transport
36+
import com.circle.modularwallets.core.utils.abi.encodeAbiParameters
37+
import com.circle.modularwallets.core.utils.abi.encodePacked
38+
import com.circle.modularwallets.core.utils.data.pad
39+
import com.circle.modularwallets.core.utils.signature.deserializeSignature
40+
import org.web3j.abi.FunctionEncoder
41+
import org.web3j.abi.TypeReference
42+
import org.web3j.abi.datatypes.Address
43+
import org.web3j.abi.datatypes.DynamicArray
44+
import org.web3j.abi.datatypes.DynamicBytes
45+
import org.web3j.abi.datatypes.StaticStruct
46+
import org.web3j.abi.datatypes.Type
47+
import org.web3j.abi.datatypes.generated.Bytes32
48+
import org.web3j.abi.datatypes.generated.Uint256
49+
import org.web3j.abi.datatypes.generated.Uint8
50+
import org.web3j.crypto.Sign
51+
import org.web3j.utils.Numeric
52+
53+
internal class LocalCircleSmartAccountDelegate(val owner: LocalAccount) :
54+
CircleSmartAccountDelegate {
55+
56+
override suspend fun getModularWalletAddress(
57+
transport: Transport,
58+
version: String,
59+
name: String?
60+
): ModularWallet {
61+
return getModularWalletAddress(transport, owner.getAddress(), version, name)
62+
}
63+
64+
override suspend fun getComputeWallet(client: Client, version: String): ModularWallet {
65+
return ModularWallet(
66+
address = getAddressFromLocalOwner(client.transport, owner.getAddress()),
67+
scaConfiguration = ScaConfiguration(
68+
scaCore = version,
69+
),
70+
)
71+
}
72+
73+
override fun getFactoryData(): String {
74+
return getFactoryData(owner.getAddress())
75+
}
76+
77+
override suspend fun signAndWrap(
78+
context: Context,
79+
hash: String,
80+
hasUserOpGas: Boolean
81+
): String {
82+
val signature =
83+
if (hasUserOpGas) owner.signMessage(context, hash) else owner.sign(context, hash)
84+
val signatureData = deserializeSignature(signature)
85+
return encodePackedForSignature(signatureData, hasUserOpGas)
86+
}
87+
88+
companion object {
89+
const val WALLET_PREFIX = "wallet"
90+
internal suspend fun getModularWalletAddress(
91+
transport: Transport, address: String, version: String, name: String? = null
92+
): ModularWallet {
93+
val wallet =
94+
ModularApiImpl.getAddress(
95+
transport,
96+
getCreateWalletReq(address, version, name)
97+
)
98+
return wallet
99+
}
100+
101+
internal suspend fun getAddressFromLocalOwner(
102+
transport: Transport,
103+
address: String
104+
): String {
105+
val sender = pad(address)
106+
val initializeUpgradableMSCAParams = getInitializeUpgradableMSCAParams(address)
107+
108+
/** address, mixedSalt */
109+
val result = UtilApiImpl.getAddress(
110+
transport,
111+
FACTORY.address,
112+
Numeric.hexStringToByteArray(sender),
113+
SALT,
114+
Numeric.hexStringToByteArray(initializeUpgradableMSCAParams)
115+
)
116+
return result.first
117+
}
118+
119+
private fun getInitializeUpgradableMSCAParams(address: String): String {
120+
val pluginInstallParams = getPluginInstallParams(address)
121+
val encoded = encodeAbiParameters(
122+
listOf(
123+
DynamicArray(
124+
Address::class.java,
125+
Address(CIRCLE_WEIGHTED_WEB_AUTHN_MULTISIG_PLUGIN.address),
126+
),
127+
DynamicArray(
128+
Bytes32::class.java,
129+
Bytes32(CIRCLE_WEIGHTED_WEB_AUTHN_MULTISIG_PLUGIN.manifestHash),
130+
),
131+
DynamicArray(
132+
DynamicBytes::class.java,
133+
DynamicBytes(Numeric.hexStringToByteArray(pluginInstallParams)),
134+
),
135+
)
136+
)
137+
return encoded
138+
}
139+
140+
private fun getPluginInstallParams(address: String): String {
141+
val encoded = encodeAbiParameters(
142+
listOf(
143+
DynamicArray(Address::class.java, Address(address)),
144+
DynamicArray(
145+
Uint256::class.java, Uint256(OWNER_WEIGHT)
146+
),
147+
DynamicArray(StaticStruct::class.java),
148+
DynamicArray(Uint256::class.java),
149+
Uint256(THRESHOLD_WEIGHT)
150+
)
151+
)
152+
return encoded
153+
}
154+
155+
private fun getFactoryData(address: String): String {
156+
val sender = pad(address)
157+
val initializeUpgradableMSCAParams = getInitializeUpgradableMSCAParams(address)
158+
val function = org.web3j.abi.datatypes.Function(
159+
"createAccount", listOf(
160+
Bytes32(Numeric.hexStringToByteArray(sender)),
161+
Bytes32(SALT),
162+
DynamicBytes(Numeric.hexStringToByteArray(initializeUpgradableMSCAParams)),
163+
), listOf<TypeReference<*>>(object : TypeReference<Address>() {})
164+
)
165+
val factoryData = FunctionEncoder.encode(function)
166+
return factoryData
167+
}
168+
169+
/**
170+
* Wraps a raw Secp256k1 signature into the ABI-encoded format expected by smart contract.
171+
* */
172+
internal fun encodePackedForSignature(
173+
signatureData: Sign.SignatureData,
174+
hasUserOpGas: Boolean
175+
): String {
176+
val sigType: Long =
177+
if (hasUserOpGas) signatureData.v[0].toLong() + SIG_TYPE_FLAG_DIGEST else signatureData.v[0].toLong()
178+
val encoded =
179+
encodePacked(
180+
listOf<Type<*>>(
181+
Bytes32(signatureData.r),
182+
Bytes32(signatureData.s),
183+
Uint8(sigType),
184+
)
185+
)
186+
187+
return encoded
188+
}
189+
}
190+
}

0 commit comments

Comments
 (0)