Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
Syn-McJ committed Feb 17, 2025
2 parents 1f514f1 + b8199eb commit afd58b9
Show file tree
Hide file tree
Showing 29 changed files with 451 additions and 107 deletions.
9 changes: 4 additions & 5 deletions Example/Sources/MainViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -149,17 +149,16 @@ class MainViewModel {

private func setupSmartContractAccount(credentials: EthereumAccount) {
do {
let connectionConfig = ConnectionConfig(apiKey: alchemyApiKey, jwt: nil, rpcUrl: nil)
let provider = try AlchemyProvider(
entryPointAddress: chain.getDefaultEntryPointAddress(),
config: AlchemyProviderConfig(
config: ProviderConfig(
chain: chain,
connectionConfig: ConnectionConfig(apiKey: alchemyApiKey,
jwt: nil,
rpcUrl: nil),
connectionConfig: connectionConfig,
opts: SmartAccountProviderOpts(txMaxRetries: 50, txRetryIntervalMs: 500)
)
).withAlchemyGasManager(
config: AlchemyGasManagerConfig(policyId: alchemyGasPolicyId)
config: AlchemyGasManagerConfig(policyId: alchemyGasPolicyId, connectionConfig: connectionConfig)
)

let account = try LightSmartContractAccount(
Expand Down
13 changes: 13 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ let package = Package(
.library(
name: "Alchemy",
targets: ["AASwiftAlchemy"]),
.library(
name: "Coinbase",
targets: ["AASwiftCoinbase"]),
],
dependencies: [
.package(url: "https://github.com/attaswift/BigInt.git", .upToNextMajor(from: "5.3.0")),
Expand All @@ -46,6 +49,16 @@ let package = Package(
path: "Sources",
sources: ["Alchemy"]
),
.target(
name: "AASwiftCoinbase",
dependencies: [
"AASwift",
.product(name: "BigInt", package: "BigInt"),
.product(name: "web3.swift", package: "web3.swift"),
],
path: "Sources",
sources: ["Coinbase"]
),
.testTarget(
name: "AASwiftTests",
dependencies: ["AASwift", "MockSwift"]),
Expand Down
2 changes: 1 addition & 1 deletion Sources/Alchemy/Account/Utils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ extension Chain {
Chain.BaseGoerli.id,
Chain.BaseSepolia.id: lightAccountVersions[version]!.factoryAddress

default: throw AlchemyError.noFactoryAddress("no default light account factory contract exists for \(name)")
default: throw ProviderError.noFactoryAddress("no default light account factory contract exists for \(name)")
}
}
}
5 changes: 0 additions & 5 deletions Sources/Alchemy/AlchemyError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,3 @@
// and is released under the MIT License: https://opensource.org/licenses/MIT
//

public enum AlchemyError: Error {
case unsupportedChain(String)
case rpcUrlNotFound(String)
case noFactoryAddress(String)
}
2 changes: 1 addition & 1 deletion Sources/Alchemy/Chain.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ extension Chain {
case Chain.BaseGoerli:
return "https://base-goerli.g.alchemy.com/v2"
case Chain.BaseSepolia:
return "https://base-sepolia.g.alchemy.com/v2/"
return "https://base-sepolia.g.alchemy.com/v2"
default:
return nil
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/Alchemy/Middleware/AlchemyClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ import BigInt

public protocol AlchemyClient: Erc4337Client {
func maxPriorityFeePerGas() async throws -> BigUInt
func requestPaymasterAndData(params: PaymasterAndDataParams) async throws -> AlchemyPaymasterAndData
func requestPaymasterAndData(params: PaymasterAndDataParams) async throws -> PaymasterAndData
func requestGasAndPaymasterAndData(params: PaymasterAndDataParams) async throws -> AlchemyGasAndPaymasterAndData
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// and is released under the MIT License: https://opensource.org/licenses/MIT
//

import Foundation
import AASwift
import web3
import BigInt

Expand Down
8 changes: 4 additions & 4 deletions Sources/Alchemy/Middleware/AlchemyRpcClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import BigInt
import web3

class AlchemyRpcClient: Erc4337RpcClient, AlchemyClient {
override func maxPriorityFeePerGas() async throws -> BigUInt {
public func maxPriorityFeePerGas() async throws -> BigUInt {
do {
let emptyParams: [Bool] = []
let data = try await networkProvider.send(method: "rundler_maxPriorityFeePerGas", params: emptyParams, receive: String.self)
Expand All @@ -26,10 +26,10 @@ class AlchemyRpcClient: Erc4337RpcClient, AlchemyClient {
}
}

public func requestPaymasterAndData(params: PaymasterAndDataParams) async throws -> AlchemyPaymasterAndData {
public func requestPaymasterAndData(params: PaymasterAndDataParams) async throws -> PaymasterAndData {
do {
let data = try await networkProvider.send(method: "alchemy_requestPaymasterAndData", params: [params], receive: AlchemyPaymasterAndData.self)
if let result = data as? AlchemyPaymasterAndData {
let data = try await networkProvider.send(method: "alchemy_requestPaymasterAndData", params: [params], receive: PaymasterAndData.self)
if let result = data as? PaymasterAndData {
return result
} else {
throw EthereumClientError.unexpectedReturnValue
Expand Down
12 changes: 10 additions & 2 deletions Sources/Alchemy/Middleware/CreateClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ public func createAlchemyClient(
url: String,
chain: Chain,
headers: [String: String] = [:]
) -> AlchemyClient {
return AlchemyRpcClient(url: URL(string: url)!, network: EthereumNetwork.custom(String(describing: chain.id)), headers: headers)
) throws -> AlchemyClient {
guard let validUrl = URL(string: url) else {
throw ProviderError.invalidUrl("Invalid URL format: \(url)")
}

return AlchemyRpcClient(
url: validUrl,
network: EthereumNetwork.custom(chain.id.description),
headers: headers
)
}
74 changes: 40 additions & 34 deletions Sources/Alchemy/Middleware/GasManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ import AASwift

public struct AlchemyGasManagerConfig {
public let policyId: String
public let connectionConfig: ConnectionConfig

public init(policyId: String) {
public init(policyId: String, connectionConfig: ConnectionConfig) {
self.policyId = policyId
self.connectionConfig = connectionConfig
}
}

Expand All @@ -33,7 +35,7 @@ public struct AlchemyGasEstimationOptions {
}
}

extension AlchemyProvider {
extension SmartAccountProvider {
/// This middleware wraps the Alchemy Gas Manager APIs to provide more flexible UserOperation gas sponsorship.
///
/// If `estimateGas` is true, it will use `alchemy_requestGasAndPaymasterAndData` to get all of the gas estimates + paymaster data
Expand All @@ -50,28 +52,11 @@ extension AlchemyProvider {
public func withAlchemyGasManager(
config: AlchemyGasManagerConfig,
gasEstimationOptions: AlchemyGasEstimationOptions? = nil
) -> Self {
) throws -> Self {
let fallbackFeeDataGetter = gasEstimationOptions?.fallbackFeeDataGetter ?? alchemyFeeEstimator
let fallbackGasEstimator = gasEstimationOptions?.fallbackGasEstimator ?? defaultGasEstimator
let disableGasEstimation = gasEstimationOptions?.disableGasEstimation ?? false

let gasEstimator: ClientMiddlewareFn = if disableGasEstimation {
fallbackGasEstimator
} else {
{ client, uoStruct, overrides in
uoStruct.callGasLimit = BigUInt(0)
uoStruct.preVerificationGas = BigUInt(0)
uoStruct.verificationGasLimit = BigUInt(0)

if overrides.paymasterAndData?.isEmpty == false {
return try await fallbackGasEstimator(client, &uoStruct, overrides)
} else {
return uoStruct
}
}
}
withGasEstimator(gasEstimator: gasEstimator)

let feeDataGetter: ClientMiddlewareFn = if disableGasEstimation {
fallbackFeeDataGetter
} else {
Expand All @@ -94,11 +79,32 @@ extension AlchemyProvider {
}
withFeeDataGetter(feeDataGetter: feeDataGetter)

if disableGasEstimation {
return requestPaymasterAndData(provider: self, config: config) as! Self
let gasEstimator: ClientMiddlewareFn = if disableGasEstimation {
fallbackGasEstimator
} else {
return requestGasAndPaymasterData(provider: self, config: config) as! Self
{ client, uoStruct, overrides in
uoStruct.callGasLimit = BigUInt(0)
uoStruct.preVerificationGas = BigUInt(0)
uoStruct.verificationGasLimit = BigUInt(0)

if overrides.paymasterAndData?.isEmpty == false {
return try await fallbackGasEstimator(client, &uoStruct, overrides)
} else {
return uoStruct
}
}
}
withGasEstimator(gasEstimator: gasEstimator)

let provider = if disableGasEstimation {
requestPaymasterAndData(provider: self, config: config) as! Self
} else {
requestGasAndPaymasterData(provider: self, config: config) as! Self
};

return provider.withMiddlewareRpcClient(
rpcClient: try AlchemyProvider.createRpcClient(config: ProviderConfig(chain: chain, connectionConfig: config.connectionConfig))
) as! Self
}
}

Expand All @@ -109,20 +115,20 @@ extension AlchemyProvider {
/// @param provider - the smart account provider to override to use the paymaster middleware
/// @param config - the alchemy gas manager configuration
/// @returns the provider augmented to use the paymaster middleware
func requestPaymasterAndData(provider: AlchemyProvider, config: AlchemyGasManagerConfig) -> AlchemyProvider {
func requestPaymasterAndData(provider: SmartAccountProvider, config: AlchemyGasManagerConfig) -> SmartAccountProvider {
provider.withPaymasterMiddleware(
dummyPaymasterDataMiddleware: { _, uoStruct, _ in
uoStruct.paymasterAndData = dummyPaymasterAndData(chainId: provider.chain.id)
return uoStruct
},
paymasterDataMiddleware: { _, uoStruct, _ in
paymasterDataMiddleware: { client, uoStruct, _ in
let entryPoint = try provider.getEntryPointAddress()
let params = PaymasterAndDataParams(
policyId: config.policyId,
entryPoint: entryPoint.asString(),
userOperation: uoStruct.toUserOperationRequest()
)
let alchemyClient = provider.rpcClient as! AlchemyClient
let alchemyClient = client as! AlchemyClient
uoStruct.paymasterAndData = try await alchemyClient.requestPaymasterAndData(params: params).paymasterAndData
return uoStruct
}
Expand All @@ -137,23 +143,23 @@ func requestPaymasterAndData(provider: AlchemyProvider, config: AlchemyGasManage
/// @param provider - the smart account provider to override to use the paymaster middleware
/// @param config - the alchemy gas manager configuration
/// @returns the provider augmented to use the paymaster middleware
func requestGasAndPaymasterData(provider: AlchemyProvider, config: AlchemyGasManagerConfig) -> AlchemyProvider {
func requestGasAndPaymasterData(provider: SmartAccountProvider, config: AlchemyGasManagerConfig) -> SmartAccountProvider {
provider.withPaymasterMiddleware(
dummyPaymasterDataMiddleware: { _, uoStruct, _ in
uoStruct.paymasterAndData = dummyPaymasterAndData(chainId: provider.chain.id)
return uoStruct
},
paymasterDataMiddleware: { _, uoStruct, overrides in
paymasterDataMiddleware: { client, uoStruct, overrides in
let userOperation = uoStruct.toUserOperationRequest()
let feeOverride = FeeOverride(
maxFeePerGas: overrides.maxFeePerGas?.web3.hexString,
maxPriorityFeePerGas: overrides.maxPriorityFeePerGas?.web3.hexString,
callGasLimit: overrides.callGasLimit?.web3.hexString,
verificationGasLimit: overrides.verificationGasLimit?.web3.hexString,
preVerificationGas: overrides.preVerificationGas?.web3.hexString
maxFeePerGas: overrides.maxFeePerGas?.properHexString,
maxPriorityFeePerGas: overrides.maxPriorityFeePerGas?.properHexString,
callGasLimit: overrides.callGasLimit?.properHexString,
verificationGasLimit: overrides.verificationGasLimit?.properHexString,
preVerificationGas: overrides.preVerificationGas?.properHexString
)

if let alchemyClient = provider.rpcClient as? AlchemyClient {
if let alchemyClient = client as? AlchemyClient {
let feeOverride: FeeOverride? = if feeOverride.isEmpty { nil } else { feeOverride }
let result = try await alchemyClient.requestGasAndPaymasterAndData(
params: PaymasterAndDataParams(
Expand Down
39 changes: 9 additions & 30 deletions Sources/Alchemy/Provider/AlchemyProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,51 +13,30 @@ import Foundation
public class AlchemyProvider: SmartAccountProvider {
static private var rpcUrl: String = ""

static private func createRpcClient(config: AlchemyProviderConfig) throws -> Erc4337Client {
static internal func createRpcClient(config: ProviderConfig) throws -> Erc4337Client {
guard let chain = SupportedChains[config.chain.id] else {
throw AlchemyError.unsupportedChain("Unsupported chain id: \(config.chain.id)")
throw ProviderError.unsupportedChain("Unsupported chain id: \(config.chain.id)")
}

guard let rpcUrl = config.connectionConfig.rpcUrl ?? (chain.alchemyRpcHttpUrl.map { "\($0)/\(config.connectionConfig.apiKey ?? "")" }) else {
throw AlchemyError.rpcUrlNotFound("No rpcUrl found for chain \(config.chain.id)")
guard let rpcUrl = config.connectionConfig.rpcUrl ?? (chain.alchemyRpcHttpUrl.map {
let apiKey = config.connectionConfig.apiKey ?? ""
return apiKey.isEmpty ? $0 : "\($0)/\(apiKey)"
}) else {
throw ProviderError.rpcUrlNotFound("No rpcUrl found for chain \(config.chain.id)")
}

let headers = config.connectionConfig.jwt.map {
["Authorization": "Bearer \($0)"]
} ?? [:]
let rpcClient = createAlchemyClient(url: rpcUrl, chain: config.chain, headers: headers)
let rpcClient = try createAlchemyClient(url: rpcUrl, chain: config.chain, headers: headers)
self.rpcUrl = rpcUrl

return rpcClient
}

public init(entryPointAddress: EthereumAddress?, config: AlchemyProviderConfig) throws {
public init(entryPointAddress: EthereumAddress?, config: ProviderConfig) throws {
let rpcClient = try AlchemyProvider.createRpcClient(config: config)
try super.init(client: rpcClient, rpcUrl: nil, entryPointAddress: entryPointAddress, chain: config.chain, opts: config.opts)
withGasEstimator(gasEstimator: alchemyFeeEstimator)
}

public override func defaultGasEstimator(
client: Erc4337Client,
operation: inout UserOperationStruct,
overrides: UserOperationOverrides
) async throws -> UserOperationStruct {
if (overrides.preVerificationGas != nil &&
overrides.verificationGasLimit != nil &&
overrides.callGasLimit != nil
) {
operation.preVerificationGas = overrides.preVerificationGas
operation.verificationGasLimit = overrides.verificationGasLimit
operation.callGasLimit = overrides.callGasLimit
} else {
let request = operation.toUserOperationRequest()
let estimates = try await rpcClient.estimateUserOperationGas(request: request, entryPoint: getEntryPointAddress().asString())

operation.preVerificationGas = overrides.preVerificationGas ?? estimates.preVerificationGas
operation.verificationGasLimit = overrides.verificationGasLimit ?? estimates.verificationGasLimit
operation.callGasLimit = overrides.callGasLimit ?? estimates.callGasLimit
}

return operation
}
}
26 changes: 26 additions & 0 deletions Sources/Coinbase/Chain.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//
// Copyright (c) 2025 aa-swift
//
// This file is part of the aa-swift project: https://github.com/syn-mcj/aa-swift,
// and is released under the MIT License: https://opensource.org/licenses/MIT
//

import AASwift

let SupportedChains: [Int64: Chain] = [
Chain.Base.id: Chain.Base,
Chain.BaseSepolia.id: Chain.BaseSepolia
]

extension Chain {
var coinbasePaymasterAndBundlerUrl: String? {
switch self {
case Chain.Base:
return "https://api.developer.coinbase.com/rpc/v1/base"
case Chain.BaseSepolia:
return "https://api.developer.coinbase.com/rpc/v1/base-sepolia"
default:
return nil
}
}
}
13 changes: 13 additions & 0 deletions Sources/Coinbase/Middleware/CoinbaseClient.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// Copyright (c) 2025 aa-swift
//
// This file is part of the aa-swift project: https://github.com/syn-mcj/aa-swift,
// and is released under the MIT License: https://opensource.org/licenses/MIT
//

import AASwift

public protocol CoinbaseClient: Erc4337Client {
func getPaymasterData(params: PaymasterDataParams) async throws -> PaymasterAndData
func sponsorUserOperation(userOp: UserOperationRequest, entryPoint: String) async throws -> SponsoredUserOperation
}
Loading

0 comments on commit afd58b9

Please sign in to comment.