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

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* 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.accounts

import android.content.Context
import com.circle.modularwallets.core.accounts.implementations.Web3jLocalAccount
import org.web3j.crypto.Credentials

/**
* Creates a [LocalAccount] instance from a hexadecimal private key string.
*
* The underlying account implementation is [Web3jLocalAccount].
*
* @param privateKey The private key as a hexadecimal string.
* @return A [LocalAccount] instance derived from the provided private key.
*/
fun privateKeyToAccount(privateKey: String): LocalAccount {
val credentials = Credentials.create(privateKey)
return LocalAccount(Web3jLocalAccount(credentials))
}

/**
* Represents a local account with signing capabilities.
*
* Instances are typically created via factory functions like [mnemonicToAccount] or [privateKeyToAccount].
*
* @param delegate The underlying [Account] instance that this [LocalAccount] will delegate
* its operations to. This delegate is responsible for the actual cryptographic
* operations and key management.
*/
class LocalAccount(
private val delegate: Account<String>
) : Account<String>() {

/**
* Retrieves the address of the local account.
*
* @return The address of the local account.
*/
override fun getAddress(): String {
return delegate.getAddress()
}

/**
* Signs the given hex string.
*
* @param context The context in which the signing operation is performed.
* @param hex The hex string to be signed.
* @return The signed hex string.
*/
override suspend fun sign(context: Context, hex: String): String {
return delegate.sign(context, hex)
}

/**
* Signs the given message.
*
* @param context The context in which the signing operation is performed.
* @param message The message to be signed.
* @return The signed message.
*/
override suspend fun signMessage(context: Context, message: String): String {
return delegate.signMessage(context, message)
}

/**
* Signs the given typed data.
*
* @param context The context in which the signing operation is performed.
* @param typedData The typed data to be signed.
* @return The signed typed data.
*/
override suspend fun signTypedData(context: Context, typedData: String): String {
return delegate.signTypedData(context, typedData)
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* 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.accounts.implementations

import android.content.Context
import com.circle.modularwallets.core.apis.modular.ModularWallet
import com.circle.modularwallets.core.clients.Client
import com.circle.modularwallets.core.transports.Transport

interface CircleSmartAccountDelegate {

suspend fun getModularWalletAddress(
transport: Transport, version: String, name: String? = null
): ModularWallet

suspend fun getComputeWallet(
client: Client,
version: String
): ModularWallet

fun getFactoryData(): String

suspend fun signAndWrap(
context: Context,
hash: String,
hasUserOpGas: Boolean
): String
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
/*
* 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.accounts.implementations

import android.content.Context
import com.circle.modularwallets.core.accounts.LocalAccount
import com.circle.modularwallets.core.apis.modular.ModularApiImpl
import com.circle.modularwallets.core.apis.modular.ModularWallet
import com.circle.modularwallets.core.apis.modular.ScaConfiguration
import com.circle.modularwallets.core.apis.modular.getCreateWalletReq
import com.circle.modularwallets.core.apis.util.UtilApiImpl
import com.circle.modularwallets.core.clients.Client
import com.circle.modularwallets.core.constants.CIRCLE_WEIGHTED_WEB_AUTHN_MULTISIG_PLUGIN
import com.circle.modularwallets.core.constants.FACTORY
import com.circle.modularwallets.core.constants.OWNER_WEIGHT
import com.circle.modularwallets.core.constants.SALT
import com.circle.modularwallets.core.constants.SIG_TYPE_FLAG_DIGEST
import com.circle.modularwallets.core.constants.THRESHOLD_WEIGHT
import com.circle.modularwallets.core.transports.Transport
import com.circle.modularwallets.core.utils.abi.encodeAbiParameters
import com.circle.modularwallets.core.utils.abi.encodePacked
import com.circle.modularwallets.core.utils.data.pad
import com.circle.modularwallets.core.utils.signature.deserializeSignature
import org.web3j.abi.FunctionEncoder
import org.web3j.abi.TypeReference
import org.web3j.abi.datatypes.Address
import org.web3j.abi.datatypes.DynamicArray
import org.web3j.abi.datatypes.DynamicBytes
import org.web3j.abi.datatypes.StaticStruct
import org.web3j.abi.datatypes.Type
import org.web3j.abi.datatypes.generated.Bytes32
import org.web3j.abi.datatypes.generated.Uint256
import org.web3j.abi.datatypes.generated.Uint8
import org.web3j.crypto.Sign
import org.web3j.utils.Numeric

internal class LocalCircleSmartAccountDelegate(val owner: LocalAccount) :
CircleSmartAccountDelegate {

override suspend fun getModularWalletAddress(
transport: Transport,
version: String,
name: String?
): ModularWallet {
return getModularWalletAddress(transport, owner.getAddress(), version, name)
}

override suspend fun getComputeWallet(client: Client, version: String): ModularWallet {
return ModularWallet(
address = getAddressFromLocalOwner(client.transport, owner.getAddress()),
scaConfiguration = ScaConfiguration(
scaCore = version,
),
)
}

override fun getFactoryData(): String {
return getFactoryData(owner.getAddress())
}

override suspend fun signAndWrap(
context: Context,
hash: String,
hasUserOpGas: Boolean
): String {
val signature =
if (hasUserOpGas) owner.signMessage(context, hash) else owner.sign(context, hash)
val signatureData = deserializeSignature(signature)
return encodePackedForSignature(signatureData, hasUserOpGas)
}

companion object {
const val WALLET_PREFIX = "wallet"
internal suspend fun getModularWalletAddress(
transport: Transport, address: String, version: String, name: String? = null
): ModularWallet {
val wallet =
ModularApiImpl.getAddress(
transport,
getCreateWalletReq(address, version, name)
)
return wallet
}

internal suspend fun getAddressFromLocalOwner(
transport: Transport,
address: String
): String {
val sender = pad(address)
val initializeUpgradableMSCAParams = getInitializeUpgradableMSCAParams(address)

/** address, mixedSalt */
val result = UtilApiImpl.getAddress(
transport,
FACTORY.address,
Numeric.hexStringToByteArray(sender),
SALT,
Numeric.hexStringToByteArray(initializeUpgradableMSCAParams)
)
return result.first
}

private fun getInitializeUpgradableMSCAParams(address: String): String {
val pluginInstallParams = getPluginInstallParams(address)
val encoded = encodeAbiParameters(
listOf(
DynamicArray(
Address::class.java,
Address(CIRCLE_WEIGHTED_WEB_AUTHN_MULTISIG_PLUGIN.address),
),
DynamicArray(
Bytes32::class.java,
Bytes32(CIRCLE_WEIGHTED_WEB_AUTHN_MULTISIG_PLUGIN.manifestHash),
),
DynamicArray(
DynamicBytes::class.java,
DynamicBytes(Numeric.hexStringToByteArray(pluginInstallParams)),
),
)
)
return encoded
}

private fun getPluginInstallParams(address: String): String {
val encoded = encodeAbiParameters(
listOf(
DynamicArray(Address::class.java, Address(address)),
DynamicArray(
Uint256::class.java, Uint256(OWNER_WEIGHT)
),
DynamicArray(StaticStruct::class.java),
DynamicArray(Uint256::class.java),
Uint256(THRESHOLD_WEIGHT)
)
)
return encoded
}

private fun getFactoryData(address: String): String {
val sender = pad(address)
val initializeUpgradableMSCAParams = getInitializeUpgradableMSCAParams(address)
val function = org.web3j.abi.datatypes.Function(
"createAccount", listOf(
Bytes32(Numeric.hexStringToByteArray(sender)),
Bytes32(SALT),
DynamicBytes(Numeric.hexStringToByteArray(initializeUpgradableMSCAParams)),
), listOf<TypeReference<*>>(object : TypeReference<Address>() {})
)
val factoryData = FunctionEncoder.encode(function)
return factoryData
}

/**
* Wraps a raw Secp256k1 signature into the ABI-encoded format expected by smart contract.
* */
internal fun encodePackedForSignature(
signatureData: Sign.SignatureData,
hasUserOpGas: Boolean
): String {
val sigType: Long =
if (hasUserOpGas) signatureData.v[0].toLong() + SIG_TYPE_FLAG_DIGEST else signatureData.v[0].toLong()
val encoded =
encodePacked(
listOf<Type<*>>(
Bytes32(signatureData.r),
Bytes32(signatureData.s),
Uint8(sigType),
)
)

return encoded
}
}
}
Loading