Skip to content

Commit 41828fb

Browse files
feat: support for eth_signTypedDataV4 payload parsing
1 parent c01f6a1 commit 41828fb

File tree

2 files changed

+287
-172
lines changed

2 files changed

+287
-172
lines changed

Sources/web3swift/Utils/EIP/EIP712/EIP712Parser.swift

Lines changed: 65 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -123,10 +123,31 @@ public struct EIP712TypeProperty: Codable {
123123
public let name: String
124124
/// Property type. A type that's ABI encodable.
125125
public let type: String
126+
/// Strips brackets (e.g. [] - denoting an array) and other characters augmenting the type.
127+
/// If ``type`` is an array of then ``coreType`` will return the type of the array.
128+
public let coreType: String
129+
130+
public let isArray: Bool
126131

127132
public init(name: String, type: String) {
128-
self.name = name
129-
self.type = type
133+
self.name = name.trimmingCharacters(in: .whitespacesAndNewlines)
134+
self.type = type.trimmingCharacters(in: .whitespacesAndNewlines)
135+
136+
var _coreType = self.type
137+
if _coreType.hasSuffix("[]") {
138+
_coreType.removeLast(2)
139+
isArray = true
140+
} else {
141+
isArray = false
142+
}
143+
self.coreType = _coreType
144+
}
145+
146+
public init(from decoder: Decoder) throws {
147+
let container = try decoder.container(keyedBy: CodingKeys.self)
148+
let name = try container.decode(String.self, forKey: .name)
149+
let type = try container.decode(String.self, forKey: .type)
150+
self.init(name: name, type: type)
130151
}
131152
}
132153

@@ -144,82 +165,9 @@ public struct EIP712TypedData {
144165
domain: [String : AnyObject],
145166
message: [String : AnyObject]) throws {
146167
self.types = types
147-
self.primaryType = primaryType
168+
self.primaryType = primaryType.trimmingCharacters(in: .whitespacesAndNewlines)
148169
self.domain = domain
149170
self.message = message
150-
if let problematicType = hasCircularDependency() {
151-
throw Web3Error.inputError(desc: "Created EIP712TypedData has a circular dependency amongst it's types. Cycle was first identified in '\(problematicType)'. Review it's uses in 'types'.")
152-
}
153-
}
154-
155-
/// Checks for a circular dependency among the given types.
156-
///
157-
/// If a circular dependency is detected, it returns the name of the type where the cycle was first identified.
158-
/// Otherwise, it returns `nil`.
159-
///
160-
/// - Returns: The type name where a circular dependency is detected, or `nil` if no circular dependency exists.
161-
/// - Note: The function utilizes depth-first search to identify the circular dependencies.
162-
func hasCircularDependency() -> String? {
163-
164-
/// Generates an adjacency list for the given types, representing their dependencies.
165-
///
166-
/// - Parameter types: A dictionary mapping type names to their property definitions.
167-
/// - Returns: An adjacency list representing type dependencies.
168-
func createAdjacencyList(types: [String: [EIP712TypeProperty]]) -> [String: [String]] {
169-
var adjList: [String: [String]] = [:]
170-
171-
for (typeName, fields) in types {
172-
adjList[typeName] = []
173-
for field in fields {
174-
if types.keys.contains(field.type) {
175-
adjList[typeName]?.append(field.type)
176-
}
177-
}
178-
}
179-
180-
return adjList
181-
}
182-
183-
let adjList = createAdjacencyList(types: types)
184-
185-
/// Depth-first search to check for circular dependencies.
186-
///
187-
/// - Parameters:
188-
/// - node: The current type being checked.
189-
/// - visited: A dictionary keeping track of the visited types.
190-
/// - stack: A dictionary used for checking the current path for cycles.
191-
///
192-
/// - Returns: `true` if a cycle is detected from the current node, `false` otherwise.
193-
func depthFirstSearch(node: String, visited: inout [String: Bool], stack: inout [String: Bool]) -> Bool {
194-
visited[node] = true
195-
stack[node] = true
196-
197-
for neighbor in adjList[node] ?? [] {
198-
if visited[neighbor] == nil {
199-
if depthFirstSearch(node: neighbor, visited: &visited, stack: &stack) {
200-
return true
201-
}
202-
} else if stack[neighbor] == true {
203-
return true
204-
}
205-
}
206-
207-
stack[node] = false
208-
return false
209-
}
210-
211-
var visited: [String: Bool] = [:]
212-
var stack: [String: Bool] = [:]
213-
214-
for typeName in adjList.keys {
215-
if visited[typeName] == nil {
216-
if depthFirstSearch(node: typeName, visited: &visited, stack: &stack) {
217-
return typeName
218-
}
219-
}
220-
}
221-
222-
return nil
223171
}
224172

225173
public func encodeType(_ type: String) throws -> String {
@@ -237,9 +185,11 @@ public struct EIP712TypedData {
237185
var typesCovered = typesCovered
238186
var encodedSubtypes: [String] = []
239187
let parameters = try typeData.map { attributeType in
240-
if let innerTypes = types[attributeType.type], !typesCovered.contains(attributeType.type) {
241-
encodedSubtypes.append(try encodeType(attributeType.type, innerTypes))
242-
typesCovered.append(attributeType.type)
188+
if let innerTypes = types[attributeType.coreType], !typesCovered.contains(attributeType.coreType) {
189+
typesCovered.append(attributeType.coreType)
190+
if attributeType.coreType != type {
191+
encodedSubtypes.append(try encodeType(attributeType.coreType, innerTypes))
192+
}
243193
}
244194
return "\(attributeType.type) \(attributeType.name)"
245195
}
@@ -261,9 +211,10 @@ public struct EIP712TypedData {
261211
throw Web3Error.processingError(desc: "EIP712. Attempting to encode data for type that doesn't exist in this payload. Given type: \(type). Available types: \(types.values).")
262212
}
263213

264-
// Add field contents
265-
for field in typeData {
266-
let value = data[field.name]
214+
func encodeField(_ field: EIP712TypeProperty,
215+
value: AnyObject?) throws -> (encTypes: [ABI.Element.ParameterType], encValues: [Any]) {
216+
var encTypes: [ABI.Element.ParameterType] = []
217+
var encValues: [Any] = []
267218
if field.type == "string" {
268219
guard let value = value as? String else {
269220
throw Web3Error.processingError(desc: "EIP712. Type metadata '\(field)' and actual value '\(String(describing: value))' type doesn't match. Cannot cast value to String.")
@@ -276,16 +227,44 @@ public struct EIP712TypedData {
276227
}
277228
encTypes.append(.bytes(length: 32))
278229
encValues.append(value.sha3(.keccak256))
279-
} else if types[field.type] != nil {
280-
guard let value = value as? [String : AnyObject] else {
281-
throw Web3Error.processingError(desc: "EIP712. Custom type metadata '\(field)' and actual value '\(String(describing: value))' type doesn't match. Cannot cast value to [String : AnyObject].")
230+
} else if field.isArray {
231+
guard let values = value as? [AnyObject] else {
232+
throw Web3Error.processingError(desc: "EIP712. Custom type metadata '\(field)' and actual value '\(String(describing: value))' type doesn't match. Cannot cast value to [AnyObject].")
282233
}
283234
encTypes.append(.bytes(length: 32))
284-
encValues.append(try encodeData(field.type, data: value).sha3(.keccak256))
235+
let subField = EIP712TypeProperty(name: field.name, type: field.coreType)
236+
var encodedSubTypes: [ABI.Element.ParameterType] = []
237+
var encodedSubValues: [Any] = []
238+
try values.forEach { value in
239+
let encoded = try encodeField(subField, value: value)
240+
encodedSubTypes.append(contentsOf: encoded.encTypes)
241+
encodedSubValues.append(contentsOf: encoded.encValues)
242+
}
243+
244+
guard let encodedValue = ABIEncoder.encode(types: encodedSubTypes, values: encodedSubValues) else {
245+
throw Web3Error.processingError(desc: "EIP712. Failed to encode an array of custom type. Field: '\(field)'; value: '\(String(describing: value))'.")
246+
}
247+
248+
encValues.append(encodedValue.sha3(.keccak256))
249+
} else if types[field.coreType] != nil {
250+
encTypes.append(.bytes(length: 32))
251+
if let value = value as? [String : AnyObject] {
252+
encValues.append(try encodeData(field.type, data: value).sha3(.keccak256))
253+
} else {
254+
encValues.append(Data(count: 32))
255+
}
285256
} else {
286257
encTypes.append(try ABITypeParser.parseTypeString(field.type))
287258
encValues.append(value as Any)
288259
}
260+
return (encTypes, encValues)
261+
}
262+
263+
// Add field contents
264+
for field in typeData {
265+
let (_encTypes, _encValues) = try encodeField(field, value: data[field.name])
266+
encTypes.append(contentsOf: _encTypes)
267+
encValues.append(contentsOf: _encValues)
289268
}
290269

291270
guard let encodedData = ABIEncoder.encode(types: encTypes, values: encValues) else {

0 commit comments

Comments
 (0)