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.1.2</string>
<string>1.1.3</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleName</key>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@ public struct EstimateUserOperationGasResult: Codable {
/// The amount of gas to allocate for the paymaster post-operation code.
public let paymasterPostOpGasLimit: BigInt?

init(preVerificationGas: BigInt?, verificationGasLimit: BigInt?, callGasLimit: BigInt?, paymasterVerificationGasLimit: BigInt?, paymasterPostOpGasLimit: BigInt?) {
init(preVerificationGas: BigInt?,
verificationGasLimit: BigInt?,
callGasLimit: BigInt?,
paymasterVerificationGasLimit: BigInt?,
paymasterPostOpGasLimit: BigInt?) {
self.preVerificationGas = preVerificationGas
self.verificationGasLimit = verificationGasLimit
self.callGasLimit = callGasLimit
Expand Down
14 changes: 14 additions & 0 deletions CircleModularWalletsCore/Sources/APIs/Modular/ModularRpcApi.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,16 @@ import Foundation
protocol ModularRpcApi {

func getAddress(transport: Transport, req: GetAddressReq) async throws -> ModularWallet

func createAddressMapping(
transport: Transport,
walletAddress: String,
owners: [AddressMappingOwner]
) async throws -> [CreateAddressMappingResult]

func getUserOperationGasPrice(
transport: Transport
) async throws -> GetUserOperationGasPriceResult
}

extension ModularRpcApi {
Expand Down Expand Up @@ -74,4 +79,13 @@ extension ModularRpcApi {
let response = try await transport.request(req) as RpcResponse<[CreateAddressMappingResult]>
return response.result
}

func getUserOperationGasPrice(
transport: Transport
) async throws -> GetUserOperationGasPriceResult {
let params = [AnyEncodable]()
let req = RpcRequest(method: "circle_getUserOperationGasPrice", params: params)
let response = try await transport.request(req) as RpcResponse<GetUserOperationGasPriceResult>
return response.result
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
//

import Foundation
import BigInt

struct GetAddressReq: Encodable {
let scaConfiguration: ScaConfiguration
Expand Down Expand Up @@ -102,3 +103,75 @@ struct CreateAddressMappingReq: Encodable {
let walletAddress: String
let owners: [AddressMappingOwner]
}

/// Represents the response from the circle_getUserOperationGasPrice RPC method.
/// This structure provides different gas price options (low, medium, high) for
/// user operations along with verification gas limits for both
/// deployed and non-deployed smart accounts.
public struct GetUserOperationGasPriceResult: Decodable {

/// The low-priority, medium-priority and high-priority gas price option.
public let low, medium, high: GasPriceOption

/// The optional deployed verification gas.
public let deployed: BigInt?

/// The optional non-deployed verification gas.
public let notDeployed: BigInt?

enum CodingKeys: CodingKey {
case low, medium, high
case verificationGasLimit
case deployed
case notDeployed
}

init(low: GasPriceOption,
medium: GasPriceOption,
high: GasPriceOption,
deployed: BigInt? = nil,
notDeployed: BigInt? = nil) {
self.low = low
self.medium = medium
self.high = high
self.deployed = deployed
self.notDeployed = notDeployed
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.low = try container.decode(GasPriceOption.self, forKey: .low)
self.medium = try container.decode(GasPriceOption.self, forKey: .medium)
self.high = try container.decode(GasPriceOption.self, forKey: .high)
self.deployed = try container.decodeToBigInt(forKey: .deployed, isHex: false)
self.notDeployed = try container.decodeToBigInt(forKey: .notDeployed, isHex: false)
}
}

/// Represents a gas price option.
/// Contains the maximum fee per gas and maximum priority fee per gas for
/// a specific priority level (low, medium, or high).
public struct GasPriceOption: Decodable {

/// The maximum fee per gas.
public let maxFeePerGas: BigInt

/// The maximum priority fee per gas.
public let maxPriorityFeePerGas: BigInt

enum CodingKeys: CodingKey {
case maxFeePerGas
case maxPriorityFeePerGas
}

init(maxFeePerGas: BigInt, maxPriorityFeePerGas: BigInt) {
self.maxFeePerGas = maxFeePerGas
self.maxPriorityFeePerGas = maxPriorityFeePerGas
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.maxFeePerGas = try container.decodeToBigInt(forKey: .maxFeePerGas, isHex: false) ?? .zero
self.maxPriorityFeePerGas = try container.decodeToBigInt(forKey: .maxPriorityFeePerGas, isHex: false) ?? .zero
}
}
31 changes: 16 additions & 15 deletions CircleModularWalletsCore/Sources/Accounts/CircleSmartAccount.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,15 @@ public class CircleSmartAccount<A: Account>: SmartAccount, @unchecked Sendable w
}

convenience init(client: Client, owner: A, version: String, name: String?) async throws {
guard let buidlTransport = client.transport as? ModularTransport else {
guard let bundlerTransport = client.transport as? ModularTransport else {
throw BaseError(shortMessage: "The property client.transport is not the ModularTransport")
}
guard let webAuthnAccount = owner as? WebAuthnAccount else {
throw BaseError(shortMessage: "The property owner is not the WebAuthnAccount")
}

let wallet = try await Self.createWallet(
transport: buidlTransport,
transport: bundlerTransport,
hexPublicKey: webAuthnAccount.credential.publicKey,
version: version,
name: name
Expand All @@ -85,20 +85,21 @@ public class CircleSmartAccount<A: Account>: SmartAccount, @unchecked Sendable w
/// Configuration for the user operation.
public var userOperation: UserOperationConfiguration? {
get async {
let minimumVerificationGasLimit = SmartAccountUtils.getMinimumVerificationGasLimit(
deployed: await self.isDeployed(),
chainId: client.chain.chainId
)

let config = UserOperationConfiguration { userOperation in
let verificationGasLimit = BigInt(minimumVerificationGasLimit)
let maxGasLimit = max(verificationGasLimit, userOperation.verificationGasLimit ?? BigInt(0))

return EstimateUserOperationGasResult(preVerificationGas: nil,
verificationGasLimit: maxGasLimit,
callGasLimit: nil,
paymasterVerificationGasLimit: nil,
paymasterPostOpGasLimit: nil)
// Only call getDefaultVerificationGasLimit if verificationGasLimit is not provided
let verificationGasLimit = userOperation.verificationGasLimit != nil ?
userOperation.verificationGasLimit : await SmartAccountUtils.getDefaultVerificationGasLimit(
client: self.client,
deployed: await self.isDeployed()
)

return EstimateUserOperationGasResult(
preVerificationGas: nil,
verificationGasLimit: verificationGasLimit,
callGasLimit: nil,
paymasterVerificationGasLimit: nil,
paymasterPostOpGasLimit: nil
)
}

return config
Expand Down
19 changes: 16 additions & 3 deletions CircleModularWalletsCore/Sources/Clients/BundlerClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -274,14 +274,27 @@ public class BundlerClient: Client, BundlerRpcApi, PublicRpcApi {
walletAddress: String,
owners: [AddressMappingOwner]
) async throws -> [CreateAddressMappingResult] {
guard let buidlTransport = transport as? ModularTransport else {
guard let bundlerTransport = transport as? ModularTransport else {
throw BaseError(shortMessage: "The property transport is not the ModularTransport")
}

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

/// Gets the gas price options for a user operation with optional SDK version parameter.
///
/// - Returns: The gas price options with low, medium, high tiers and optional verificationGasLimit.
public func getUserOperationGasPrice() async throws -> GetUserOperationGasPriceResult {
guard let bundlerTransport = transport as? ModularTransport else {
throw BaseError(shortMessage: "The property transport is not the ModularTransport")
}

return try await bundlerTransport.getUserOperationGasPrice(
transport: bundlerTransport
)
}
}
4 changes: 0 additions & 4 deletions CircleModularWalletsCore/Sources/Helpers/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,3 @@ let THRESHOLD_WEIGHT = 1

let MINIMUM_VERIFICATION_GAS_LIMIT = 100_000
let MINIMUM_UNDEPLOY_VERIFICATION_GAS_LIMIT = 1_500_000
let SEPOLIA_MINIMUM_VERIFICATION_GAS_LIMIT = 600_000
let SEPOLIA_MINIMUM_UNDEPLOY_VERIFICATION_GAS_LIMIT = 2_000_000
let MAINNET_MINIMUM_VERIFICATION_GAS_LIMIT = 1_000_000
let MAINNET_MINIMUM_UNDEPLOY_VERIFICATION_GAS_LIMIT = 2_500_000
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import Foundation
#if SWIFT_PACKAGE
extension Bundle {
public enum SDK {
public static let version = "1.1.2"
public static let version = "1.1.3"
}
}
#else
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@ import BigInt

extension KeyedDecodingContainer {

func decodeToBigInt(forKey key: KeyedDecodingContainer<K>.Key) throws -> BigInt? {
let hexString = try? self.decodeIfPresent(String.self, forKey: key)
return HexUtils.hexToBigInt(hex: hexString)
func decodeToBigInt(forKey key: KeyedDecodingContainer<K>.Key,
isHex: Bool = true) throws -> BigInt? {
let string = try? self.decodeIfPresent(String.self, forKey: key)
return isHex ?
HexUtils.hexToBigInt(hex: string) : (string != nil ? BigInt(string ?? "") : nil)
}

// func decodeBytesFromURLEncodedBase64(forKey key: KeyedDecodingContainer.Key) throws -> [UInt8] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,33 @@
//

import Foundation
import BigInt

struct SmartAccountUtils {

static func getMinimumVerificationGasLimit(deployed: Bool,
chainId: Int) -> Int {
switch chainId {
case Sepolia.chainId:
return deployed ?
SEPOLIA_MINIMUM_VERIFICATION_GAS_LIMIT : SEPOLIA_MINIMUM_UNDEPLOY_VERIFICATION_GAS_LIMIT

case Mainnet.chainId:
return deployed ?
MAINNET_MINIMUM_VERIFICATION_GAS_LIMIT : MAINNET_MINIMUM_UNDEPLOY_VERIFICATION_GAS_LIMIT
default:
return deployed ?
MINIMUM_VERIFICATION_GAS_LIMIT : MINIMUM_UNDEPLOY_VERIFICATION_GAS_LIMIT
static func getDefaultVerificationGasLimit(client: Client,
deployed: Bool) async -> BigInt? {
var verificationGasLimit: BigInt = deployed ?
BigInt(MINIMUM_VERIFICATION_GAS_LIMIT) : BigInt(MINIMUM_UNDEPLOY_VERIFICATION_GAS_LIMIT)

guard let bundlerClient = client as? BundlerClient,
client.transport is ModularTransport else {
logger.transport.error("Client is not BundlerClient / Client transport is not ModularTransport")
return nil
}

guard let result = try? await bundlerClient.getUserOperationGasPrice() else {
logger.bundler.error("Failed to get gas prices from RPC, falling back to hardcoded values: \(verificationGasLimit)")
return verificationGasLimit
}

if deployed, let deployedVerificationGasLimit = result.deployed {
verificationGasLimit = deployedVerificationGasLimit
} else if !deployed, let notDeployedVerificationGasLimit = result.notDeployed {
verificationGasLimit = notDeployedVerificationGasLimit
}

return verificationGasLimit
}

}