Skip to content

Commit 520afb5

Browse files
committed
feat: add ethSignTransactionAndSendByCadence
1 parent f278a9d commit 520afb5

File tree

5 files changed

+169
-4
lines changed

5 files changed

+169
-4
lines changed

Package.resolved

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ let package = Package(
1616
],
1717
dependencies: [
1818
.package(url: "https://github.com/kishikawakatsumi/KeychainAccess", from: "4.2.2"),
19-
.package(url: "https://github.com/Outblock/flow-swift", .revisionItem("0d72e52bca7e8c6aeb4b594b9a31b1f35af770fb")),
19+
.package(url: "https://github.com/Outblock/flow-swift", .revisionItem("013e8537d1c9654c6be3c9c673fbdcb13ce62ffa")),
2020
.package(url: "https://github.com/trustwallet/wallet-core", .upToNextMajor(from: "4.3.2")),
2121
],
2222
targets: [
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
//
2+
// EVMModels.swift
3+
// FlowWalletKit
4+
//
5+
6+
import Foundation
7+
import Flow
8+
9+
/// Represents supported EVM chains (Flow EVM plus custom extensions).
10+
public enum EVMChain: Equatable {
11+
case flowMainnet
12+
case flowTestnet
13+
case custom(UInt64)
14+
15+
/// Numeric chain ID.
16+
public var chainId: UInt64 {
17+
switch self {
18+
case .flowMainnet: return 747
19+
case .flowTestnet: return 545
20+
case let .custom(id): return id
21+
}
22+
}
23+
24+
/// Encoded chain ID for WalletCore signing input.
25+
public var chainIdData: Data {
26+
var value = chainId
27+
var bytes: [UInt8] = []
28+
repeat {
29+
bytes.insert(UInt8(value & 0xff), at: 0)
30+
value >>= 8
31+
} while value > 0
32+
return Data(bytes)
33+
}
34+
35+
/// Whether this chain is Flow EVM (mainnet/testnet).
36+
public var isFlowEVM: Bool {
37+
switch self {
38+
case .flowMainnet, .flowTestnet:
39+
return true
40+
default:
41+
return false
42+
}
43+
}
44+
45+
/// Corresponding Flow chain ID when applicable (nil for non-Flow EVM).
46+
public var flowChainID: Flow.ChainID? {
47+
switch self {
48+
case .flowMainnet:
49+
return .mainnet
50+
case .flowTestnet:
51+
return .testnet
52+
case .custom:
53+
return nil
54+
}
55+
}
56+
}
57+
58+
/// Result model for submitting an EVM transaction via Flow.
59+
public struct FlowEVMSubmitResult {
60+
public let flowTxId: String
61+
public let evmTxId: String
62+
63+
public init(flowTxId: String, evmTxId: String) {
64+
self.flowTxId = flowTxId
65+
self.evmTxId = evmTxId
66+
}
67+
}
68+
69+
public extension Flow.ChainID {
70+
/// Map Flow network to its corresponding EVM chain when available.
71+
var evmChain: EVMChain? {
72+
switch self {
73+
case .mainnet:
74+
return .flowMainnet
75+
case .testnet:
76+
return .flowTestnet
77+
default:
78+
return nil
79+
}
80+
}
81+
}

iOS/FlowWalletKit/Sources/Wallet/Wallet+EOA.swift

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
//
77

88
import Foundation
9+
import Flow
910
import WalletCore
1011

1112
// MARK: - Support EOA
@@ -72,7 +73,7 @@ extension Wallet {
7273
guard let publicKey = PublicKey.recover(signature: normalizedSignature, message: digest) else {
7374
throw FWKError.invalidEthereumSignature
7475
}
75-
let address = AnyAddress(publicKey: publicKey, coin: .ethereum)
76+
let address = AnyAddress(publicKey: publicKey, coin: .ethereum)
7677
return address.description
7778
}
7879

@@ -82,7 +83,7 @@ extension Wallet {
8283
var signingInput = input
8384
signingInput.privateKey = try key.ethPrivateKey(index: index)
8485
defer { signingInput.privateKey = Data() }
85-
var output: EthereumSigningOutput = AnySigner.sign(input: signingInput, coin: .ethereum)
86+
var output: EthereumSigningOutput = AnySigner.sign(input: signingInput, coin: .ethereum)
8687
let transactionHash = Hash.keccak256(data: output.encoded)
8788
output.preHash = transactionHash
8889
return output
@@ -131,4 +132,83 @@ extension Wallet {
131132
}
132133
return ethereumKey
133134
}
135+
136+
/// Sends an EOA-signed Ethereum transaction to Flow EVM through Cadence.
137+
/// - Parameters:
138+
/// - account: Flow account used as proposer/payer/authorizer.
139+
/// - rlpEncodedTransaction: Signed Ethereum transaction payload.
140+
/// - coinbaseAddr: EOA coinbase address.
141+
/// - Returns: Flow transaction ID after submission.
142+
public func ethSendSignedTransactionByCadence(chainId: Flow.ChainID = .mainnet,
143+
account: Flow.Address,
144+
rlpEncodedTransaction: Data,
145+
coinbaseAddr: String,
146+
signers: [FlowSigner],
147+
payer: Flow.Address? = nil
148+
) async throws -> Flow.ID {
149+
try await flow.runEVMTransaction(
150+
chainID: chainId,
151+
proposer: account,
152+
payer: payer ?? account,
153+
rlpEncodedTransaction: Array(rlpEncodedTransaction),
154+
coinbaseAddress: coinbaseAddr,
155+
signers: signers
156+
)
157+
}
158+
159+
/// Sign an Ethereum transaction (WalletCore input) and submit it to Flow EVM via Cadence.
160+
/// Returns both the Flow transaction ID and the EVM transaction hash.
161+
/// - Parameters:
162+
/// - chain: Flow EVM chain (mainnet/testnet only).
163+
/// - input: Unsigned Ethereum signing input; chainId is set automatically based on `chain`.
164+
/// - fromAddress: Expected EOA sender/coinbase; must match the wallet's derived address for `index`.
165+
/// - signers: Flow signers (proposer/authorizers/payer).
166+
/// - flowAddress: Optional Flow address for proposer/payer; defaults to the first signer address.
167+
/// - payer: Optional custom payer; defaults to proposer.
168+
/// - index: HD derivation index for EVM key (defaults to 0).
169+
/// - Returns: `FlowEVMSubmitResult` containing Flow tx id and EVM tx hash (0x-prefixed).
170+
public func ethSignTransactionAndSendByCadence(
171+
chain: EVMChain = .flowMainnet,
172+
input: EthereumSigningInput,
173+
fromAddress: String,
174+
signers: [FlowSigner],
175+
flowAddress: Flow.Address? = nil,
176+
payer: Flow.Address? = nil,
177+
index: UInt32 = 0
178+
) async throws -> FlowEVMSubmitResult {
179+
guard case .key = type else {
180+
throw FWKError.invaildWalletType
181+
}
182+
guard !signers.isEmpty else {
183+
throw FWKError.emptySignKey
184+
}
185+
186+
var signingInput = input
187+
signingInput.chainID = chain.chainIdData
188+
guard let fromAddr = AnyAddress(string: fromAddress, coin: .ethereum) else {
189+
throw FWKError.invaildEVMAddress
190+
}
191+
let derivedAddr = try ethAddress(index: index)
192+
guard fromAddr.description.lowercased() == derivedAddr.lowercased() else {
193+
throw FWKError.invaildEVMAddress
194+
}
195+
196+
let signed = try ethSignTransaction(signingInput, index: index)
197+
guard let flowChainID = chain.flowChainID else {
198+
throw FWKError.unsupportedEVMChain
199+
}
200+
guard let proposer = flowAddress ?? signers.first?.address else {
201+
throw FWKError.emptyFlowAddress
202+
}
203+
let flowTxId = try await ethSendSignedTransactionByCadence(
204+
chainId: flowChainID,
205+
account: proposer,
206+
rlpEncodedTransaction: signed.encoded,
207+
coinbaseAddr: fromAddr.description,
208+
signers: signers,
209+
payer: payer ?? proposer
210+
)
211+
let flowTxIdString = String(describing: flowTxId)
212+
return FlowEVMSubmitResult(flowTxId: flowTxIdString, evmTxId: signed.txIdHex())
213+
}
134214
}

iOS/FlowWalletKit/Sources/WalletError.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ public enum FWKError: String, Error, CaseIterable, CustomStringConvertible {
7979
/// Invalid EVM address
8080
/// Thrown when an unknown or invalid EVM address is provided
8181
case invaildEVMAddress
82+
/// Empty FLowAddress
83+
case emptyFlowAddress
84+
/// Unsupported EVM chain for Flow submission
85+
case unsupportedEVMChain
8286

8387
// MARK: - Authentication Errors
8488

0 commit comments

Comments
 (0)