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 CircleModularWalletsCore/Resources/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<plist version="1.0">
<dict>
<key>CFBundleShortVersionString</key>
<string>1.0.9</string>
<string>1.1.0</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleName</key>
Expand Down
44 changes: 44 additions & 0 deletions CircleModularWalletsCore/Sources/APIs/Modular/ModularRpcApi.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ import Foundation
protocol ModularRpcApi {

func getAddress(transport: Transport, req: GetAddressReq) async throws -> ModularWallet
func createAddressMapping(
transport: Transport,
walletAddress: String,
owners: [AddressMappingOwner]
) async throws -> [CreateAddressMappingResult]
}

extension ModularRpcApi {
Expand All @@ -30,4 +35,43 @@ extension ModularRpcApi {
let response = try await transport.request(req) as RpcResponse<ModularWallet>
return response.result
}

func createAddressMapping(
transport: Transport,
walletAddress: String,
owners: [AddressMappingOwner]
) async throws -> [CreateAddressMappingResult] {
if !Utils.isAddress(walletAddress) {
throw BaseError(shortMessage: "walletAddress is invalid")
}

if owners.isEmpty {
throw BaseError(shortMessage: "At least one owner must be provided")
}

for (index, owner) in owners.enumerated() {
switch owner {
case let eoaOwner as EoaAddressMappingOwner:
if !Utils.isAddress(eoaOwner.identifier.address) {
throw BaseError(shortMessage: "EOA owner at index \(index) has an invalid address")
}

case let webAuthnOwner as WebAuthnAddressMappingOwner:
if webAuthnOwner.identifier.publicKeyX.isEmpty || webAuthnOwner.identifier.publicKeyY.isEmpty {
throw BaseError(shortMessage: "Webauthn owner at index \(index) must have publicKeyX and publicKeyY")
}

default:
throw BaseError(shortMessage: "Owner at index \(index) has an invalid type")
}
}

let req = RpcRequest(
method: "circle_createAddressMapping",
params: [CreateAddressMappingReq(walletAddress: walletAddress, owners: owners)]
)

let response = try await transport.request(req) as RpcResponse<[CreateAddressMappingResult]>
return response.result
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,8 @@ struct ScaConfiguration: Codable {
struct Metadata: Codable {
let name: String?
}

struct CreateAddressMappingReq: Encodable {
let walletAddress: String
let owners: [AddressMappingOwner]
}
78 changes: 6 additions & 72 deletions CircleModularWalletsCore/Sources/Accounts/CircleSmartAccount.swift
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,8 @@ public class CircleSmartAccount<A: Account>: SmartAccount, @unchecked Sendable w

public func sign(hex: String) async throws -> String {
let digest = Utils.toSha3Data(message: hex)
let hash = getReplaySafeHash(
chainId: client.chain.chainId,
let hash = try await Utils.getReplaySafeMessageHash(
transport: client.transport,
account: getAddress(),
hash: HexUtils.dataToHex(digest)
)
Expand Down Expand Up @@ -189,8 +189,8 @@ public class CircleSmartAccount<A: Account>: SmartAccount, @unchecked Sendable w
let messageHashHex = HexUtils.dataToHex(messageHash)

let digest = Utils.toSha3Data(message: messageHashHex)
let hash = getReplaySafeHash(
chainId: client.chain.chainId,
let hash = try await Utils.getReplaySafeMessageHash(
transport: client.transport,
account: getAddress(),
hash: HexUtils.dataToHex(digest)
)
Expand Down Expand Up @@ -221,8 +221,8 @@ public class CircleSmartAccount<A: Account>: SmartAccount, @unchecked Sendable w
let typedDataHashHex = HexUtils.dataToHex(typedDataHash)

let digest = Utils.toSha3Data(message: typedDataHashHex)
let hash = getReplaySafeHash(
chainId: client.chain.chainId,
let hash = try await Utils.getReplaySafeMessageHash(
transport: client.transport,
account: getAddress(),
hash: HexUtils.dataToHex(digest)
)
Expand Down Expand Up @@ -322,62 +322,6 @@ extension CircleSmartAccount: PublicRpcApi {
}
}

/// Remove the private access control for unit testing
func getReplaySafeHash(
chainId: Int,
account: String,
hash: String,
verifyingContract: String = CIRCLE_WEIGHTED_WEB_AUTHN_MULTISIG_PLUGIN
) -> String {
// Get the prefix
let messagePrefix = "0x1901"
let prefix = HexUtils.hexToData(hex: messagePrefix) ?? .init()

// Get the domainSeparatorHash
let domainSeparatorTypeHash =
Utils.toSha3Data(message: "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)")

var types: [ABI.Element.ParameterType] = [
.bytes(length: 32),
.bytes(length: 32),
.uint(bits: 256),
.address,
.bytes(length: 32)
]
var values: [Any] = [
domainSeparatorTypeHash,
Self.getModuleIdHash(),
chainId,
verifyingContract,
Utils.pad(data: Utils.toData(value: account), isRight: true)
]

var domainSeparator = Data()
if let encoded = ABIEncoder.encode(types: types, values: values) {
domainSeparator = encoded
}
let domainSeparatorHash = domainSeparator.sha3(.keccak256)

// Get the structHash
guard let bytes = try? HexUtils.hexToBytes(hex: hash) else {
logger.passkeyAccount.error("Failed to decode the hash of getReplaySafeHash into UInt8 array.")
return ""
}

types = [.bytes(length: 32), .bytes(length: 32)]
values = [Self.getModuleTypeHash(), bytes]
var structData = Data()
if let encoded = ABIEncoder.encode(types: types, values: values) {
structData = encoded
}
let structHash = structData.sha3(.keccak256)

// Concat the prefix, domainSeparatorHash and domainSeparatorHash
let replaySafeHash = (prefix + domainSeparatorHash + structHash).sha3(.keccak256)

return HexUtils.dataToHex(replaySafeHash)
}

/// Remove the private access control for unit testing
func encodePackedForSignature(
signResult: SignResult,
Expand Down Expand Up @@ -428,16 +372,6 @@ extension CircleSmartAccount: PublicRpcApi {
return encoded
}

static func getModuleIdHash() -> Data {
let message = Utils.encodePacked(["Weighted Multisig Webauthn Plugin", "1.0.0"])

return Utils.toSha3Data(message: message)
}

static func getModuleTypeHash() -> Data {
return Utils.toSha3Data(message: "CircleWeightedWebauthnMultisigMessage(bytes32 hash)")
}

static func encodeParametersWebAuthnSigDynamicPart(
authenticatorDataString: String, // Hex
clientDataJSON: String, // Base64URL decoded
Expand Down
29 changes: 29 additions & 0 deletions CircleModularWalletsCore/Sources/Chains/Base.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// Copyright (c) 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.
//

import Foundation

public let Base = _Base()

public struct _Base: Chain {

public let chainId: Int = 8453

public let blockchain: String = "BASE"

}
29 changes: 29 additions & 0 deletions CircleModularWalletsCore/Sources/Chains/BaseSepolia.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// Copyright (c) 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.
//

import Foundation

public let BaseSepolia = _BaseSepolia()

public struct _BaseSepolia: Chain {

public let chainId: Int = 84532

public let blockchain: String = "BASE-SEPOLIA"

}
29 changes: 29 additions & 0 deletions CircleModularWalletsCore/Sources/Chains/Optimism.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// Copyright (c) 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.
//

import Foundation

public let Optimism = _Optimism()

public struct _Optimism: Chain {

public let chainId: Int = 10

public let blockchain: String = "OP"

}
29 changes: 29 additions & 0 deletions CircleModularWalletsCore/Sources/Chains/OptimismSepolia.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// Copyright (c) 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.
//

import Foundation

public let OptimismSepolia = _OptimismSepolia()

public struct _OptimismSepolia: Chain {

public let chainId: Int = 11155420

public let blockchain: String = "OP-SEPOLIA"

}
29 changes: 29 additions & 0 deletions CircleModularWalletsCore/Sources/Chains/Unichain.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// Copyright (c) 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.
//

import Foundation

public let Unichain = _Unichain()

public struct _Unichain: Chain {

public let chainId: Int = 130

public let blockchain: String = "UNI"

}
29 changes: 29 additions & 0 deletions CircleModularWalletsCore/Sources/Chains/UnichainSepolia.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// Copyright (c) 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.
//

import Foundation

public let UnichainSepolia = _UnichainSepolia()

public struct _UnichainSepolia: Chain {

public let chainId: Int = 1301

public let blockchain: String = "UNI-SEPOLIA"

}
23 changes: 23 additions & 0 deletions CircleModularWalletsCore/Sources/Clients/BundlerClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -261,4 +261,27 @@ public class BundlerClient: Client, BundlerRpcApi, PublicRpcApi {
includeTransactions: includeTransactions,
blockNumber: blockNumber)
}

/// Creates an address mapping for recovery.
///
/// - Note: This feature is only available in Circle Buidl Wallets service.
///
/// - Parameters:
/// - walletAddress: The Circle smart wallet address.
/// - owners: The owners of the wallet.
/// - Returns: The response from adding an address mapping.
public func createAddressMapping(
walletAddress: String,
owners: [AddressMappingOwner]
) async throws -> [CreateAddressMappingResult] {
guard let buidlTransport = transport as? ModularTransport else {
throw BaseError(shortMessage: "The property transport is not the ModularTransport")
}

return try await buidlTransport.createAddressMapping(
transport: buidlTransport,
walletAddress: walletAddress,
owners: owners
)
}
}
Loading