From 83f81cb6805d47938c23c14757049d81ec1e7991 Mon Sep 17 00:00:00 2001 From: Nick Raienko Date: Tue, 3 Oct 2023 21:56:38 +0200 Subject: [PATCH] ENS resolution --- .gitignore | 4 +- README.md | 1 - .../Configurations.swift | 26 +- .../Helpers/Types.swift | 1 + .../NamingServices/ENS.swift | 177 +++++++++ .../Resolution.swift | 17 +- .../Resources/ENS/ensRegistry.json | 108 ++++++ .../Resources/ENS/ensResolver.json | 357 ++++++++++++++++++ .../Support/Base58Swift/File.swift | 107 ++++++ 9 files changed, 785 insertions(+), 13 deletions(-) create mode 100644 Sources/UnstoppableDomainsResolution/NamingServices/ENS.swift create mode 100644 Sources/UnstoppableDomainsResolution/Resources/ENS/ensRegistry.json create mode 100644 Sources/UnstoppableDomainsResolution/Resources/ENS/ensResolver.json create mode 100644 Sources/UnstoppableDomainsResolution/Support/Base58Swift/File.swift diff --git a/.gitignore b/.gitignore index 980ef11..0b3b65f 100644 --- a/.gitignore +++ b/.gitignore @@ -89,4 +89,6 @@ fastlane/test_output iOSInjectionProject/ -.idea/ \ No newline at end of file +.idea/ +.DS_Store +tmp diff --git a/README.md b/README.md index 68db8b2..9d38c45 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ # UnstoppableDomainsResolution -[![Get help on Discord](https://img.shields.io/badge/Get%20help%20on-Discord-blueviolet)](https://discord.gg/b6ZVxSZ9Hn) [![Unstoppable Domains Documentation](https://img.shields.io/badge/Documentation-unstoppabledomains.com-blue)](https://docs.unstoppabledomains.com/) Resolution is a library for interacting with blockchain domain names. It can be used to retrieve payment addresses and IPFS hashes for decentralized websites. diff --git a/Sources/UnstoppableDomainsResolution/Configurations.swift b/Sources/UnstoppableDomainsResolution/Configurations.swift index 5244989..31fa993 100644 --- a/Sources/UnstoppableDomainsResolution/Configurations.swift +++ b/Sources/UnstoppableDomainsResolution/Configurations.swift @@ -50,41 +50,49 @@ let UD_RPC_PROXY_BASE_URL = "https://api.unstoppabledomains.com/resolve" public struct Configurations { let uns: UnsLocations + let ens: NamingServiceConfig let apiKey: String? = nil public init( - uns: UnsLocations + uns: UnsLocations, + ens: NamingServiceConfig ) { self.uns = uns + self.ens = ens } - + public init( apiKey: String, znsLayer: NamingServiceConfig = NamingServiceConfig( providerUrl: "https://api.zilliqa.com", + network: "mainnet"), + ens: NamingServiceConfig = NamingServiceConfig( + providerUrl: "https://mainnet.infura.io/v3/", network: "mainnet") ) { var networking = DefaultNetworkingLayer(); networking.addHeader(header: "Authorization", value: "Bearer \(apiKey)") networking.addHeader(header: "X-Lib-Agent", value: Configurations.getLibVersion()) - + let layer1NamingService = NamingServiceConfig( - providerUrl: "\(UD_RPC_PROXY_BASE_URL)/chains/eth/rpc", - network: "mainnet", - networking: networking) - + providerUrl: "\(UD_RPC_PROXY_BASE_URL)/chains/eth/rpc", + network: "mainnet", + networking: networking) + let layer2NamingService = NamingServiceConfig( providerUrl: "\(UD_RPC_PROXY_BASE_URL)/chains/matic/rpc", network: "polygon-mainnet", networking: networking) - + self.uns = UnsLocations( layer1: layer1NamingService, layer2: layer2NamingService, znsLayer: znsLayer ) + + self.ens = ens } - + static public func getLibVersion() -> String { return "UnstoppableDomains/resolution-swift/6.1.0" } diff --git a/Sources/UnstoppableDomainsResolution/Helpers/Types.swift b/Sources/UnstoppableDomainsResolution/Helpers/Types.swift index 8720595..bf2825a 100644 --- a/Sources/UnstoppableDomainsResolution/Helpers/Types.swift +++ b/Sources/UnstoppableDomainsResolution/Helpers/Types.swift @@ -21,6 +21,7 @@ internal typealias AsyncConsumer = (T?, Error?) public enum NamingServiceName: String { case uns + case ens case zns } diff --git a/Sources/UnstoppableDomainsResolution/NamingServices/ENS.swift b/Sources/UnstoppableDomainsResolution/NamingServices/ENS.swift new file mode 100644 index 0000000..844edf9 --- /dev/null +++ b/Sources/UnstoppableDomainsResolution/NamingServices/ENS.swift @@ -0,0 +1,177 @@ +import Foundation + +internal class ENS: CommonNamingService, NamingService { + let network: String + let registryAddress: String + let registryMap: [String: String] = [ + "mainnet": "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e", + "ropsten": "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e", + "rinkeby": "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e", + "goerli": "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e" + ] + + init(_ config: NamingServiceConfig) throws { + self.network = config.network.isEmpty + ? try Self.getNetworkName(providerUrl: config.providerUrl, networking: config.networking) + : config.network + + var registryAddress: String? = registryMap[self.network] + if config.registryAddresses != nil && !config.registryAddresses!.isEmpty { + registryAddress = config.registryAddresses![0] + } + + guard registryAddress != nil else { + throw ResolutionError.registryAddressIsNotProvided + } + self.registryAddress = registryAddress! + super.init(name: .ens, providerUrl: config.providerUrl, networking: config.networking) + } + + func isSupported(domain: String) -> Bool { + return domain ~= "^[^-]*[^-]*\\.(eth|luxe|xyz|kred|addr\\.reverse)$" + } + + func owner(domain: String) throws -> String { + let tokenId = super.namehash(domain: domain) + guard let ownerAddress = try askRegistryContract(for: "owner", with: [tokenId]), + Utillities.isNotEmpty(ownerAddress) else { + throw ResolutionError.unregisteredDomain + } + return ownerAddress + } + + func batchOwners(domains: [String]) throws -> [String: String?] { + throw ResolutionError.methodNotSupported + } + + func addr(domain: String, ticker: String) throws -> String { + guard ticker.uppercased() == "ETH" else { + throw ResolutionError.recordNotSupported + } + let tokenId = super.namehash(domain: domain) + let resolverAddress = try resolver(tokenId: tokenId) + let resolverContract = try super.buildContract(address: resolverAddress, type: .resolver) + + guard let dict = try resolverContract.callMethod(methodName: "addr", args: [tokenId, ethCoinIndex]) as? [String: Data], + let dataAddress = dict["0"], + let address = EthereumAddress(dataAddress), + Utillities.isNotEmpty(address.address) else { + throw ResolutionError.recordNotFound(self.name.rawValue) + } + return address.address + } + + func addr(domain: String, network: String, token: String) throws -> String { + return "NA" + } + + // MARK: - Get Record + func record(domain: String, key: String) throws -> String { + let tokenId = super.namehash(domain: domain) + return try self.record(tokenId: tokenId, key: key) + } + + func record(tokenId: String, key: String) throws -> String { + if key == "ipfs.html.value" { + let hash = try self.getContentHash(tokenId: tokenId) + return hash + } + + let resolverAddress = try resolver(tokenId: tokenId) + let resolverContract = try super.buildContract(address: resolverAddress, type: .resolver) + + let ensKeyName = self.fromUDNameToEns(record: key) + + guard let dict = try resolverContract.callMethod(methodName: "text", args: [tokenId, ensKeyName]) as? [String: String], + let result = dict["0"], + Utillities.isNotEmpty(result) else { + throw ResolutionError.recordNotFound(self.name.rawValue) + } + return result + } + + func records(keys: [String], for domain: String) throws -> [String: String] { + throw ResolutionError.methodNotSupported + } + + func allRecords(domain: String) throws -> [String: String] { + throw ResolutionError.methodNotSupported + } + + func getTokenUri(tokenId: String) throws -> String { + throw ResolutionError.methodNotSupported + } + + func getDomainName(tokenId: String) throws -> String { + throw ResolutionError.methodNotSupported + } + + func locations(domains: [String]) throws -> [String: Location] { + throw ResolutionError.methodNotSupported + } + + // MARK: - get Resolver + func resolver(domain: String) throws -> String { + let tokenId = super.namehash(domain: domain) + return try self.resolver(tokenId: tokenId) + } + + func resolver(tokenId: String) throws -> String { + guard let resolverAddress = try askRegistryContract(for: "resolver", with: [tokenId]), + Utillities.isNotEmpty(resolverAddress) else { + throw ResolutionError.unspecifiedResolver(self.name.rawValue) + } + return resolverAddress + } + + // MARK: - Helper functions + private func askRegistryContract(for methodName: String, with args: [String]) throws -> String? { + let registryContract: Contract = try super.buildContract(address: self.registryAddress, type: .ensRegistry) + guard let ethereumAddress = try registryContract.callMethod(methodName: methodName, args: args) as? [String: EthereumAddress], + let address = ethereumAddress["0"] else { + return nil + } + return address.address + } + + private func fromUDNameToEns(record: String) -> String { + let mapper: [String: String] = [ + "ipfs.redirect_domain.value": "url", + "whois.email.value": "email", + "gundb.username.value": "gundb_username", + "gundb.public_key.value": "gundb_public_key" + ] + return mapper[record] ?? record + } + + /* + //https://ethereum.stackexchange.com/questions/17094/how-to-store-ipfs-hash-using-bytes32 + getIpfsHashFromBytes32(bytes32Hex) { + // Add our default ipfs values for first 2 bytes: + // function:0x12=sha2, size:0x20=256 bits + // and cut off leading "0x" + const hashHex = "1220" + bytes32Hex.slice(2) + const hashBytes = Buffer.from(hashHex, 'hex'); + const hashStr = bs58.encode(hashBytes) + return hashStr + } + */ + private func getContentHash(tokenId: String) throws -> String { + let resolverAddress = try resolver(tokenId: tokenId) + let resolverContract = try super.buildContract(address: resolverAddress, type: .resolver) + + let hash = try resolverContract.callMethod(methodName: "contenthash", args: [tokenId]) as? [String: Any] + guard let data = hash?["0"] as? Data else { + throw ResolutionError.recordNotFound(self.name.rawValue) + } + + let contentHash = [UInt8](data) + guard let codec = Array(contentHash[0..<1]).last, + codec == 0xE3 // 'ipfs-ns' + else { + throw ResolutionError.recordNotFound(self.name.rawValue) + } + + return Base58.base58Encode(Array(contentHash[4.. [NamingService] { var networkServices: [NamingService] = [] var errorService: Error? + do { networkServices.append(try UNS(configs)) } catch { errorService = error } - + + do { + networkServices.append(try ENS(configs.ens)) + } catch { + errorService = error + } + if let error = errorService { throw error } + return networkServices } /// This returns the naming service private func getServiceOf(domain: String) throws -> NamingService { + if domain ~= "^[^-]*[^-]*\\.(eth|luxe|xyz|kred|addr\\.reverse)$" { + return try self.findService(name: .ens) + } + return try self.findService(name: .uns) } diff --git a/Sources/UnstoppableDomainsResolution/Resources/ENS/ensRegistry.json b/Sources/UnstoppableDomainsResolution/Resources/ENS/ensRegistry.json new file mode 100644 index 0000000..14c82dc --- /dev/null +++ b/Sources/UnstoppableDomainsResolution/Resources/ENS/ensRegistry.json @@ -0,0 +1,108 @@ +[ + { + "constant": true, + "inputs": [{ "name": "node", "type": "bytes32" }], + "name": "resolver", + "outputs": [{ "name": "", "type": "address" }], + "payable": false, + "type": "function", + }, + { + "constant": true, + "inputs": [{ "name": "node", "type": "bytes32" }], + "name": "owner", + "outputs": [{ "name": "", "type": "address" }], + "payable": false, + "type": "function", + }, + { + "constant": false, + "inputs": [ + { "name": "node", "type": "bytes32" }, + { "name": "label", "type": "bytes32" }, + { "name": "owner", "type": "address" }, + ], + "name": "setSubnodeOwner", + "outputs": [], + "payable": false, + "type": "function", + }, + { + "constant": false, + "inputs": [ + { "name": "node", "type": "bytes32" }, + { "name": "ttl", "type": "uint64" }, + ], + "name": "setTTL", + "outputs": [], + "payable": false, + "type": "function", + }, + { + "constant": true, + "inputs": [{ "name": "node", "type": "bytes32" }], + "name": "ttl", + "outputs": [{ "name": "", "type": "uint64" }], + "payable": false, + "type": "function", + }, + { + "constant": false, + "inputs": [ + { "name": "node", "type": "bytes32" }, + { "name": "resolver", "type": "address" }, + ], + "name": "setResolver", + "outputs": [], + "payable": false, + "type": "function", + }, + { + "constant": false, + "inputs": [ + { "name": "node", "type": "bytes32" }, + { "name": "owner", "type": "address" }, + ], + "name": "setOwner", + "outputs": [], + "payable": false, + "type": "function", + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "name": "node", "type": "bytes32" }, + { "indexed": false, "name": "owner", "type": "address" }, + ], + "name": "Transfer", + "type": "event", + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "name": "node", "type": "bytes32" }, + { "indexed": true, "name": "label", "type": "bytes32" }, + { "indexed": false, "name": "owner", "type": "address" }, + ], + "name": "NewOwner", + "type": "event", + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "name": "node", "type": "bytes32" }, + { "indexed": false, "name": "resolver", "type": "address" }, + ], + "name": "NewResolver", + "type": "event", + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "name": "node", "type": "bytes32" }, + { "indexed": false, "name": "ttl", "type": "uint64" }, + ], + "name": "NewTTL", + "type": "event", + }, +] diff --git a/Sources/UnstoppableDomainsResolution/Resources/ENS/ensResolver.json b/Sources/UnstoppableDomainsResolution/Resources/ENS/ensResolver.json new file mode 100644 index 0000000..40d27a1 --- /dev/null +++ b/Sources/UnstoppableDomainsResolution/Resources/ENS/ensResolver.json @@ -0,0 +1,357 @@ +[ + { + "constant": true, + "inputs": [{ "internalType": "bytes4", "name": "interfaceID", "type": "bytes4" }], + "name": "supportsInterface", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "pure", + "type": "function", + }, + { + "constant": false, + "inputs": [ + { "internalType": "bytes32", "name": "node", "type": "bytes32" }, + { "internalType": "string", "name": "key", "type": "string" }, + { "internalType": "string", "name": "value", "type": "string" }, + ], + "name": "setText", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function", + }, + { + "constant": true, + "inputs": [ + { "internalType": "bytes32", "name": "node", "type": "bytes32" }, + { "internalType": "bytes4", "name": "interfaceID", "type": "bytes4" }, + ], + "name": "interfaceImplementer", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "payable": false, + "stateMutability": "view", + "type": "function", + }, + { + "constant": true, + "inputs": [ + { "internalType": "bytes32", "name": "node", "type": "bytes32" }, + { "internalType": "uint256", "name": "contentTypes", "type": "uint256" }, + ], + "name": "ABI", + "outputs": [ + { "internalType": "uint256", "name": "", "type": "uint256" }, + { "internalType": "bytes", "name": "", "type": "bytes" }, + ], + "payable": false, + "stateMutability": "view", + "type": "function", + }, + { + "constant": false, + "inputs": [ + { "internalType": "bytes32", "name": "node", "type": "bytes32" }, + { "internalType": "bytes32", "name": "x", "type": "bytes32" }, + { "internalType": "bytes32", "name": "y", "type": "bytes32" }, + ], + "name": "setPubkey", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function", + }, + { + "constant": false, + "inputs": [ + { "internalType": "bytes32", "name": "node", "type": "bytes32" }, + { "internalType": "bytes", "name": "hash", "type": "bytes" }, + ], + "name": "setContenthash", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function", + }, + { + "constant": false, + "inputs": [ + { "internalType": "bytes32", "name": "node", "type": "bytes32" }, + { "internalType": "address", "name": "target", "type": "address" }, + { "internalType": "bool", "name": "isAuthorised", "type": "bool" }, + ], + "name": "setAuthorisation", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function", + }, + { + "constant": true, + "inputs": [ + { "internalType": "bytes32", "name": "node", "type": "bytes32" }, + { "internalType": "string", "name": "key", "type": "string" }, + ], + "name": "text", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "payable": false, + "stateMutability": "view", + "type": "function", + }, + { + "constant": false, + "inputs": [ + { "internalType": "bytes32", "name": "node", "type": "bytes32" }, + { "internalType": "uint256", "name": "contentType", "type": "uint256" }, + { "internalType": "bytes", "name": "data", "type": "bytes" }, + ], + "name": "setABI", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function", + }, + { + "constant": true, + "inputs": [{ "internalType": "bytes32", "name": "node", "type": "bytes32" }], + "name": "name", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "payable": false, + "stateMutability": "view", + "type": "function", + }, + { + "constant": false, + "inputs": [ + { "internalType": "bytes32", "name": "node", "type": "bytes32" }, + { "internalType": "string", "name": "name", "type": "string" }, + ], + "name": "setName", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function", + }, + { + "constant": false, + "inputs": [ + { "internalType": "bytes32", "name": "node", "type": "bytes32" }, + { "internalType": "uint256", "name": "coinType", "type": "uint256" }, + { "internalType": "bytes", "name": "a", "type": "bytes" }, + ], + "name": "setAddr", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function", + }, + { + "constant": true, + "inputs": [{ "internalType": "bytes32", "name": "node", "type": "bytes32" }], + "name": "contenthash", + "outputs": [{ "internalType": "bytes", "name": "", "type": "bytes" }], + "payable": false, + "stateMutability": "view", + "type": "function", + }, + { + "constant": true, + "inputs": [{ "internalType": "bytes32", "name": "node", "type": "bytes32" }], + "name": "pubkey", + "outputs": [ + { "internalType": "bytes32", "name": "x", "type": "bytes32" }, + { "internalType": "bytes32", "name": "y", "type": "bytes32" }, + ], + "payable": false, + "stateMutability": "view", + "type": "function", + }, + { + "constant": false, + "inputs": [ + { "internalType": "bytes32", "name": "node", "type": "bytes32" }, + { "internalType": "address", "name": "a", "type": "address" }, + ], + "name": "setAddr", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function", + }, + { + "constant": false, + "inputs": [ + { "internalType": "bytes32", "name": "node", "type": "bytes32" }, + { "internalType": "bytes4", "name": "interfaceID", "type": "bytes4" }, + { "internalType": "address", "name": "implementer", "type": "address" }, + ], + "name": "setInterface", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function", + }, + { + "constant": true, + "inputs": [ + { "internalType": "bytes32", "name": "node", "type": "bytes32" }, + { "internalType": "uint256", "name": "coinType", "type": "uint256" }, + ], + "name": "addr", + "outputs": [{ "internalType": "bytes", "name": "", "type": "bytes" }], + "payable": false, + "stateMutability": "view", + "type": "function", + }, + { + "constant": true, + "inputs": [ + { "internalType": "bytes32", "name": "", "type": "bytes32" }, + { "internalType": "address", "name": "", "type": "address" }, + { "internalType": "address", "name": "", "type": "address" }, + ], + "name": "authorisations", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [{ "internalType": "contract ENS", "name": "_ens", "type": "address" }], + "payable": false, + "stateMutability": "nonpayable", + "type": "constructor", + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "bytes32", "name": "node", "type": "bytes32" }, + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address", + }, + { + "indexed": true, + "internalType": "address", + "name": "target", + "type": "address", + }, + { + "indexed": false, + "internalType": "bool", + "name": "isAuthorised", + "type": "bool", + }, + ], + "name": "AuthorisationChanged", + "type": "event", + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "bytes32", "name": "node", "type": "bytes32" }, + { + "indexed": false, + "internalType": "string", + "name": "indexedKey", + "type": "string", + }, + { "indexed": false, "internalType": "string", "name": "key", "type": "string" }, + ], + "name": "TextChanged", + "type": "event", + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "bytes32", "name": "node", "type": "bytes32" }, + { "indexed": false, "internalType": "bytes32", "name": "x", "type": "bytes32" }, + { "indexed": false, "internalType": "bytes32", "name": "y", "type": "bytes32" }, + ], + "name": "PubkeyChanged", + "type": "event", + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "bytes32", "name": "node", "type": "bytes32" }, + { "indexed": false, "internalType": "string", "name": "name", "type": "string" }, + ], + "name": "NameChanged", + "type": "event", + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "bytes32", "name": "node", "type": "bytes32" }, + { + "indexed": true, + "internalType": "bytes4", + "name": "interfaceID", + "type": "bytes4", + }, + { + "indexed": false, + "internalType": "address", + "name": "implementer", + "type": "address", + }, + ], + "name": "InterfaceChanged", + "type": "event", + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "bytes32", "name": "node", "type": "bytes32" }, + { "indexed": false, "internalType": "bytes", "name": "hash", "type": "bytes" }, + ], + "name": "ContenthashChanged", + "type": "event", + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "bytes32", "name": "node", "type": "bytes32" }, + { "indexed": false, "internalType": "address", "name": "a", "type": "address" }, + ], + "name": "AddrChanged", + "type": "event", + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "bytes32", "name": "node", "type": "bytes32" }, + { + "indexed": false, + "internalType": "uint256", + "name": "coinType", + "type": "uint256", + }, + { + "indexed": false, + "internalType": "bytes", + "name": "newAddress", + "type": "bytes", + }, + ], + "name": "AddressChanged", + "type": "event", + }, + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "bytes32", "name": "node", "type": "bytes32" }, + { + "indexed": true, + "internalType": "uint256", + "name": "contentType", + "type": "uint256", + }, + ], + "name": "ABIChanged", + "type": "event", + }, +] diff --git a/Sources/UnstoppableDomainsResolution/Support/Base58Swift/File.swift b/Sources/UnstoppableDomainsResolution/Support/Base58Swift/File.swift new file mode 100644 index 0000000..f8687a3 --- /dev/null +++ b/Sources/UnstoppableDomainsResolution/Support/Base58Swift/File.swift @@ -0,0 +1,107 @@ +// Copyright Keefer Taylor, 2019. + +import BigInt +import CommonCrypto +import Foundation + +/// A static utility class which provides Base58 encoding and decoding functionality. +public enum Base58 { + /// Length of checksum appended to Base58Check encoded strings. + private static let checksumLength = 4 + + private static let alphabet = [UInt8]("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz".utf8) + private static let zero = BigUInt(0) + private static let radix = BigUInt(alphabet.count) + + /// Encode the given bytes into a Base58Check encoded string. + /// - Parameter bytes: The bytes to encode. + /// - Returns: A base58check encoded string representing the given bytes, or nil if encoding failed. + public static func base58CheckEncode(_ bytes: [UInt8]) -> String { + let checksum = calculateChecksum(bytes) + let checksummedBytes = bytes + checksum + return Base58.base58Encode(checksummedBytes) + } + + /// Decode the given Base58Check encoded string to bytes. + /// - Parameter input: A base58check encoded input string to decode. + /// - Returns: Bytes representing the decoded input, or nil if decoding failed. + public static func base58CheckDecode(_ input: String) -> [UInt8]? { + guard let decodedChecksummedBytes = base58Decode(input) else { + return nil + } + + let decodedChecksum = decodedChecksummedBytes.suffix(checksumLength) + let decodedBytes = decodedChecksummedBytes.prefix(upTo: decodedChecksummedBytes.count - checksumLength) + let calculatedChecksum = calculateChecksum([UInt8](decodedBytes)) + + guard decodedChecksum.elementsEqual(calculatedChecksum, by: { $0 == $1 }) else { + return nil + } + return Array(decodedBytes) + } + + /// Encode the given bytes to a Base58 encoded string. + /// - Parameter bytes: The bytes to encode. + /// - Returns: A base58 encoded string representing the given bytes, or nil if encoding failed. + public static func base58Encode(_ bytes: [UInt8]) -> String { + var answer: [UInt8] = [] + var integerBytes = BigUInt(Data(bytes)) + + while integerBytes > 0 { + let (quotient, remainder) = integerBytes.quotientAndRemainder(dividingBy: radix) + answer.insert(alphabet[Int(remainder)], at: 0) + integerBytes = quotient + } + + let prefix = Array(bytes.prefix { $0 == 0 }).map { _ in alphabet[0] } + answer.insert(contentsOf: prefix, at: 0) + + // swiftlint:disable force_unwrapping + // Force unwrap as the given alphabet will always decode to UTF8. + return String(bytes: answer, encoding: String.Encoding.utf8)! + // swiftlint:enable force_unwrapping + } + + /// Decode the given base58 encoded string to bytes. + /// - Parameter input: The base58 encoded input string to decode. + /// - Returns: Bytes representing the decoded input, or nil if decoding failed. + public static func base58Decode(_ input: String) -> [UInt8]? { + var answer = zero + var i = BigUInt(1) + let byteString = [UInt8](input.utf8) + + for char in byteString.reversed() { + guard let alphabetIndex = alphabet.firstIndex(of: char) else { + return nil + } + answer += (i * BigUInt(alphabetIndex)) + i *= radix + } + + let bytes = answer.serialize() + return Array(byteString.prefix { i in i == alphabet[0] }) + bytes + } + + /// Calculate a checksum for a given input by hashing twice and then taking the first four bytes. + /// - Parameter input: The input bytes. + /// - Returns: A byte array representing the checksum of the input bytes. + private static func calculateChecksum(_ input: [UInt8]) -> [UInt8] { + let hashedData = sha256(input) + let doubleHashedData = sha256(hashedData) + let doubleHashedArray = Array(doubleHashedData) + return Array(doubleHashedArray.prefix(checksumLength)) + } + + /// Create a sha256 hash of the given data. + /// - Parameter data: Input data to hash. + /// - Returns: A sha256 hash of the input data. + private static func sha256(_ data: [UInt8]) -> [UInt8] { + let res = NSMutableData(length: Int(CC_SHA256_DIGEST_LENGTH))! + CC_SHA256( + (Data(data) as NSData).bytes, + CC_LONG(data.count), + res.mutableBytes.assumingMemoryBound(to: UInt8.self) + ) + return [UInt8](res as Data) + } +}