Skip to content

Commit

Permalink
Address as a literal implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
rkohl committed Jul 31, 2021
1 parent 5cd3791 commit 278ac48
Showing 1 changed file with 170 additions and 167 deletions.
337 changes: 170 additions & 167 deletions Sources/Core/Transaction/Address.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,215 +8,218 @@

import Foundation


public struct Address {
// MARK: - Properties

/// The raw address bytes
public let rawAddress: Bytes

// MARK: - Initialization

/**
* Initializes this instance of `EthereumAddress` with the given `hex` String.
*
* `hex` must be either 40 characters (20 bytes) or 42 characters (with the 0x hex prefix) long.
*
* If `eip55` is set to `true`, a checksum check will be done over the given hex string as described
* in https://github.com/ethereum/EIPs/blob/master/EIPS/eip-55.md
*
* - parameter hex: The ethereum address as a hex string. Case sensitive iff `eip55` is set to true.
* - parameter eip55: Whether to check the checksum as described in eip 55 or not.
*
* - throws: EthereumAddress.Error.addressMalformed if the given hex string doesn't fulfill the conditions described above.
* EthereumAddress.Error.checksumWrong iff `eip55` is set to true and the checksum is wrong.
*/
public init(hex: String, eip55: Bool) throws {
// Check length
guard hex.count == 40 || hex.count == 42 else {
throw Error.addressMalformed
}

// MARK: - Properties

/// The raw address bytes
public let rawAddress: Bytes

// MARK: - Initialization

/**
* Initializes this instance of `EthereumAddress` with the given `hex` String.
*
* `hex` must be either 40 characters (20 bytes) or 42 characters (with the 0x hex prefix) long.
*
* If `eip55` is set to `true`, a checksum check will be done over the given hex string as described
* in https://github.com/ethereum/EIPs/blob/master/EIPS/eip-55.md
*
* - parameter hex: The ethereum address as a hex string. Case sensitive iff `eip55` is set to true.
* - parameter eip55: Whether to check the checksum as described in eip 55 or not.
*
* - throws: EthereumAddress.Error.addressMalformed if the given hex string doesn't fulfill the conditions described above.
* EthereumAddress.Error.checksumWrong iff `eip55` is set to true and the checksum is wrong.
*/
public init(hex: String, eip55: Bool) throws {
// Check length
guard hex.count == 40 || hex.count == 42 else {
throw Error.addressMalformed
}
var hex = hex

// Check prefix
if hex.count == 42 {
let s = hex.index(hex.startIndex, offsetBy: 0)
let e = hex.index(hex.startIndex, offsetBy: 2)

var hex = hex
guard String(hex[s..<e]) == "0x" else {
throw Error.addressMalformed
}

// Check prefix
if hex.count == 42 {
let s = hex.index(hex.startIndex, offsetBy: 0)
let e = hex.index(hex.startIndex, offsetBy: 2)
// Remove prefix
let hexStart = hex.index(hex.startIndex, offsetBy: 2)
hex = String(hex[hexStart...])
}

guard String(hex[s..<e]) == "0x" else {
throw Error.addressMalformed
}
// Check hex
guard hex.rangeOfCharacter(from: CharacterSet.hexadecimals.inverted) == nil else {
throw Error.addressMalformed
}

// Remove prefix
let hexStart = hex.index(hex.startIndex, offsetBy: 2)
hex = String(hex[hexStart...])
}
// Create address bytes
var addressBytes = Bytes()
for i in stride(from: 0, to: hex.count, by: 2) {
let s = hex.index(hex.startIndex, offsetBy: i)
let e = hex.index(hex.startIndex, offsetBy: i + 2)

// Check hex
guard hex.rangeOfCharacter(from: CharacterSet.hexadecimals.inverted) == nil else {
throw Error.addressMalformed
}
guard let b = Byte(String(hex[s..<e]), radix: 16) else {
throw Error.addressMalformed
}
addressBytes.append(b)
}
self.rawAddress = addressBytes

// Create address bytes
var addressBytes = Bytes()
for i in stride(from: 0, to: hex.count, by: 2) {
let s = hex.index(hex.startIndex, offsetBy: i)
let e = hex.index(hex.startIndex, offsetBy: i + 2)
// EIP 55 checksum
// See: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-55.md
if eip55 {
let hash = SHA3(variant: .keccak256).calculate(for: Array(hex.lowercased().utf8))

guard let b = Byte(String(hex[s..<e]), radix: 16) else {
throw Error.addressMalformed
}
addressBytes.append(b)
for i in 0..<hex.count {
let charString = String(hex[hex.index(hex.startIndex, offsetBy: i)])
if charString.rangeOfCharacter(from: CharacterSet.hexadecimalNumbers) != nil {
continue
}
self.rawAddress = addressBytes

// EIP 55 checksum
// See: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-55.md
if eip55 {
let hash = SHA3(variant: .keccak256).calculate(for: Array(hex.lowercased().utf8))

for i in 0..<hex.count {
let charString = String(hex[hex.index(hex.startIndex, offsetBy: i)])
if charString.rangeOfCharacter(from: CharacterSet.hexadecimalNumbers) != nil {
continue
}

let bytePos = (4 * i) / 8
let bitPos = (4 * i) % 8
guard bytePos < hash.count && bitPos < 8 else {
throw Error.addressMalformed
}
let bit = (hash[bytePos] >> (7 - UInt8(bitPos))) & 0x01

if charString.lowercased() == charString && bit == 1 {
throw Error.checksumWrong
} else if charString.uppercased() == charString && bit == 0 {
throw Error.checksumWrong
}
}

let bytePos = (4 * i) / 8
let bitPos = (4 * i) % 8
guard bytePos < hash.count, bitPos < 8 else {
throw Error.addressMalformed
}
}
let bit = (hash[bytePos] >> (7 - UInt8(bitPos))) & 0x01

/**
* Initializes a new instance of `EthereumAddress` with the given raw Bytes array.
*
* `rawAddress` must be exactly 20 bytes long.
*
* - parameter rawAddress: The raw address as a byte array.
*
* - throws: EthereumAddress.Error.addressMalformed if the rawAddress array is not 20 bytes long.
*/
public init(rawAddress: Bytes) throws {
guard rawAddress.count == 20 else {
throw Error.addressMalformed
if charString.lowercased() == charString, bit == 1 {
throw Error.checksumWrong
} else if charString.uppercased() == charString, bit == 0 {
throw Error.checksumWrong
}
self.rawAddress = rawAddress
}
}
}

/**
* Initializes a new instance of `EthereumAddress` with the given raw Bytes array.
*
* `rawAddress` must be exactly 20 bytes long.
*
* - parameter rawAddress: The raw address as a byte array.
*
* - throws: EthereumAddress.Error.addressMalformed if the rawAddress array is not 20 bytes long.
*/
public init(rawAddress: Bytes) throws {
guard rawAddress.count == 20 else {
throw Error.addressMalformed
}
self.rawAddress = rawAddress
}

// MARK: - Convenient functions

/**
* Returns this ethereum address as a hex string.
*
* Adds the EIP 55 mixed case checksum if `eip55` is set to true.
*
* - parameter eip55: Whether to add the mixed case checksum as described in eip 55.
*
* - returns: The hex string representing this `EthereumAddress`.
* Either lowercased or mixed case (checksumed) depending on the parameter `eip55`.
*/
public func hex(eip55: Bool) -> String {
var hex = "0x"
if !eip55 {
for b in rawAddress {
hex += String(format: "%02x", b)
}
} else {
var address = ""
for b in rawAddress {
address += String(format: "%02x", b)
}
let hash = SHA3(variant: .keccak256).calculate(for: Array(address.utf8))

for i in 0..<address.count {
let charString = String(address[address.index(address.startIndex, offsetBy: i)])

if charString.rangeOfCharacter(from: CharacterSet.hexadecimalNumbers) != nil {
hex += charString
continue
}

let bytePos = (4 * i) / 8
let bitPos = (4 * i) % 8
let bit = (hash[bytePos] >> (7 - UInt8(bitPos))) & 0x01

// MARK: - Convenient functions

/**
* Returns this ethereum address as a hex string.
*
* Adds the EIP 55 mixed case checksum if `eip55` is set to true.
*
* - parameter eip55: Whether to add the mixed case checksum as described in eip 55.
*
* - returns: The hex string representing this `EthereumAddress`.
* Either lowercased or mixed case (checksumed) depending on the parameter `eip55`.
*/
public func hex(eip55: Bool) -> String {
var hex = "0x"
if !eip55 {
for b in rawAddress {
hex += String(format: "%02x", b)
}
if bit == 1 {
hex += charString.uppercased()
} else {
var address = ""
for b in rawAddress {
address += String(format: "%02x", b)
}
let hash = SHA3(variant: .keccak256).calculate(for: Array(address.utf8))

for i in 0..<address.count {
let charString = String(address[address.index(address.startIndex, offsetBy: i)])

if charString.rangeOfCharacter(from: CharacterSet.hexadecimalNumbers) != nil {
hex += charString
continue
}

let bytePos = (4 * i) / 8
let bitPos = (4 * i) % 8
let bit = (hash[bytePos] >> (7 - UInt8(bitPos))) & 0x01

if bit == 1 {
hex += charString.uppercased()
} else {
hex += charString.lowercased()
}
}
hex += charString.lowercased()
}

return hex
}
}

// MARK: - Errors
return hex
}

public enum Error: Swift.Error {
// MARK: - Errors

case addressMalformed
case checksumWrong
}
public enum Error: Swift.Error {
case addressMalformed
case checksumWrong
}
}

// MARK: - valueConvertible
// MARK: - init

extension Address: ValueConvertible {
extension Address: ExpressibleByStringLiteral {
public typealias StringLiteralType = String

public init(stringLiteral address: String) {
self = try! Address(hex: address, eip55: true)
}
}

public init(value: Value) throws {
guard let str = value.string else {
throw ValueInitializableError.notInitializable
}
// MARK: - valueConvertible

try self.init(hex: str, eip55: false)
extension Address: ValueConvertible {
public init(value: Value) throws {
guard let str = value.string else {
throw ValueInitializableError.notInitializable
}

public func value() -> Value {
return Value(stringLiteral: hex(eip55: false))
}
try self.init(hex: str, eip55: false)
}

public func value() -> Value {
return Value(stringLiteral: hex(eip55: false))
}
}

// MARK: - Equatable

extension Address: Equatable {

public static func ==(_ lhs: Address, _ rhs: Address) -> Bool {
return lhs.rawAddress == rhs.rawAddress
}
public static func ==(_ lhs: Address, _ rhs: Address) -> Bool {
return lhs.rawAddress == rhs.rawAddress
}
}

// MARK: - BytesConvertible

extension Address: BytesConvertible {
public init(_ bytes: Bytes) throws {
try self.init(rawAddress: bytes)
}

public init(_ bytes: Bytes) throws {
try self.init(rawAddress: bytes)
}

public func makeBytes() throws -> Bytes {
return rawAddress
}
public func makeBytes() throws -> Bytes {
return rawAddress
}
}

// MARK: - Hashable

extension Address: Hashable {

public func hash(into hasher: inout Hasher) {
// TODO: Is throwing deterministic here?
try? hasher.combine(makeBytes())
}
public func hash(into hasher: inout Hasher) {
// TODO: Is throwing deterministic here?
try? hasher.combine(makeBytes())
}
}

0 comments on commit 278ac48

Please sign in to comment.