diff --git a/Adamant/Models/DashTransaction.swift b/Adamant/Models/DashTransaction.swift index 38d613954..1f9a5d6b1 100644 --- a/Adamant/Models/DashTransaction.swift +++ b/Adamant/Models/DashTransaction.swift @@ -50,8 +50,7 @@ struct DashUnspentTransaction: Decodable { case height } - func asUnspentTransaction(with publicKeyHash: Data) -> UnspentTransaction { - let lockScript = Script.buildPublicKeyHashOut(pubKeyHash: publicKeyHash) + func asUnspentTransaction(lockScript: Data) -> UnspentTransaction { let txHash = Data(hex: txid).map { Data($0.reversed()) } ?? Data() let unspentOutput = TransactionOutput(value: amount, lockingScript: lockScript) diff --git a/Adamant/SwinjectDependencies.swift b/Adamant/SwinjectDependencies.swift index ce402170a..169b1b00b 100644 --- a/Adamant/SwinjectDependencies.swift +++ b/Adamant/SwinjectDependencies.swift @@ -7,6 +7,7 @@ // import Swinject +import BitcoinKit // MARK: - Services extension Container { @@ -233,5 +234,10 @@ extension Container { accountService: r.resolve(AccountService.self)! ) }.inObjectScope(.container) + + // MARK: Bitcoin AddressConverterFactory + self.register(AddressConverterFactory.self) { r in + AddressConverterFactory() + }.inObjectScope(.container) } } diff --git a/Adamant/Wallets/Bitcoin/BtcWallet.swift b/Adamant/Wallets/Bitcoin/BtcWallet.swift index 17674874f..09314e21d 100644 --- a/Adamant/Wallets/Bitcoin/BtcWallet.swift +++ b/Adamant/Wallets/Bitcoin/BtcWallet.swift @@ -9,8 +9,8 @@ import Foundation import BitcoinKit -class BtcWallet: WalletAccount { - let address: String +final class BtcWallet: WalletAccount { + let addressEntity: Address let privateKey: PrivateKey let publicKey: PublicKey var balance: Decimal = 0.0 @@ -19,10 +19,11 @@ class BtcWallet: WalletAccount { var minAmount: Decimal = 546e-8 var isBalanceInitialized: Bool = false - init(privateKey: PrivateKey) { + var address: String { addressEntity.stringValue } + + init(privateKey: PrivateKey, addressConverter: AddressConverter) throws { self.privateKey = privateKey self.publicKey = privateKey.publicKey() - self.address = publicKey.toCashaddr().base58 + self.addressEntity = try addressConverter.convert(publicKey: publicKey, type: .p2pkh) } - } diff --git a/Adamant/Wallets/Bitcoin/BtcWalletService+Send.swift b/Adamant/Wallets/Bitcoin/BtcWalletService+Send.swift index 5d140121a..07d89f33c 100644 --- a/Adamant/Wallets/Bitcoin/BtcWalletService+Send.swift +++ b/Adamant/Wallets/Bitcoin/BtcWalletService+Send.swift @@ -29,10 +29,9 @@ extension BtcWalletService: WalletServiceTwoStepSend { throw WalletServiceError.notLogged } - let changeAddress = wallet.publicKey.toCashaddr() let key = wallet.privateKey - guard let toAddress = try? LegacyAddress(recipient, for: self.network) else { + guard let toAddress = try? addressConverter.convert(address: recipient) else { throw WalletServiceError.accountNotFound } @@ -56,7 +55,7 @@ extension BtcWalletService: WalletServiceTwoStepSend { toAddress: toAddress, amount: rawAmount, fee: fee, - changeAddress: changeAddress, + changeAddress: wallet.addressEntity, utxos: utxos, keys: [key] ) @@ -133,7 +132,7 @@ extension BtcWalletService: WalletServiceTwoStepSend { let value = NSDecimalNumber(decimal: item.value).uint64Value - let lockScript = wallet.publicKey.toCashaddr().lockingScript + let lockScript = wallet.addressEntity.lockingScript let txHash = Data(hex: item.txId).map { Data($0.reversed()) } ?? Data() let txIndex = item.vout diff --git a/Adamant/Wallets/Bitcoin/BtcWalletService.swift b/Adamant/Wallets/Bitcoin/BtcWalletService.swift index a3a512671..6ae76e72c 100644 --- a/Adamant/Wallets/Bitcoin/BtcWalletService.swift +++ b/Adamant/Wallets/Bitcoin/BtcWalletService.swift @@ -111,6 +111,7 @@ class BtcWalletService: WalletService { var dialogService: DialogService! var router: Router! var increaseFeeService: IncreaseFeeService! + var addressConverter: AddressConverter! // MARK: - Constants static var currencyLogo = #imageLiteral(resourceName: "bitcoin_wallet") @@ -385,7 +386,7 @@ extension BtcWalletService: InitiatedWithPassphraseService { let privateKeyData = passphrase.data(using: .utf8)!.sha256() let privateKey = PrivateKey(data: privateKeyData, network: self.network, isPublicKeyCompressed: true) - let eWallet = BtcWallet(privateKey: privateKey) + let eWallet = try BtcWallet(privateKey: privateKey, addressConverter: addressConverter) self.btcWallet = eWallet if !self.enabled { @@ -444,6 +445,7 @@ extension BtcWalletService: SwinjectDependentService { dialogService = container.resolve(DialogService.self) router = container.resolve(Router.self) increaseFeeService = container.resolve(IncreaseFeeService.self) + addressConverter = container.resolve(AddressConverterFactory.self)?.make(network: network) } } diff --git a/Adamant/Wallets/Dash/DashWallet.swift b/Adamant/Wallets/Dash/DashWallet.swift index 64d892bf4..df50db49a 100644 --- a/Adamant/Wallets/Dash/DashWallet.swift +++ b/Adamant/Wallets/Dash/DashWallet.swift @@ -10,7 +10,7 @@ import Foundation import BitcoinKit class DashWallet: WalletAccount { - let address: String + let addressEntity: Address let privateKey: PrivateKey let publicKey: PublicKey var balance: Decimal = 0.0 @@ -19,9 +19,15 @@ class DashWallet: WalletAccount { var minAmount: Decimal = 0.00002 var isBalanceInitialized: Bool = false - init(privateKey: PrivateKey) { + var address: String { addressEntity.stringValue } + + init(privateKey: PrivateKey, addressConverter: AddressConverter) throws { self.privateKey = privateKey self.publicKey = privateKey.publicKey() - self.address = publicKey.toCashaddr().base58 + + self.addressEntity = try addressConverter.convert( + publicKey: publicKey, + type: .p2pkh + ) } } diff --git a/Adamant/Wallets/Dash/DashWalletService+Send.swift b/Adamant/Wallets/Dash/DashWalletService+Send.swift index 2b899495f..dbbc91830 100644 --- a/Adamant/Wallets/Dash/DashWalletService+Send.swift +++ b/Adamant/Wallets/Dash/DashWalletService+Send.swift @@ -45,10 +45,9 @@ extension DashWalletService: WalletServiceTwoStepSend { throw WalletServiceError.notLogged } - let changeAddress = wallet.publicKey.toCashaddr() let key = wallet.privateKey - guard let toAddress = try? LegacyAddress(recipient, for: self.network) else { + guard let toAddress = try? addressConverter.convert(address: recipient) else { throw WalletServiceError.accountNotFound } @@ -66,7 +65,14 @@ extension DashWalletService: WalletServiceTwoStepSend { } // MARK: 4. Create local transaction - let transaction = BitcoinKit.Transaction.createNewTransaction(toAddress: toAddress, amount: rawAmount, fee: fee, changeAddress: changeAddress, utxos: utxos, keys: [key]) + let transaction = BitcoinKit.Transaction.createNewTransaction( + toAddress: toAddress, + amount: rawAmount, + fee: fee, + changeAddress: wallet.addressEntity, + utxos: utxos, + keys: [key] + ) return transaction } diff --git a/Adamant/Wallets/Dash/DashWalletService+Transactions.swift b/Adamant/Wallets/Dash/DashWalletService+Transactions.swift index 071643ea3..302ceac25 100644 --- a/Adamant/Wallets/Dash/DashWalletService+Transactions.swift +++ b/Adamant/Wallets/Dash/DashWalletService+Transactions.swift @@ -169,8 +169,9 @@ extension DashWalletService { ) if let result = response.result { - let transactions = result.map { $0.asUnspentTransaction(with: wallet.publicKey.toCashaddr().data) } - return transactions + return result.map { + $0.asUnspentTransaction(lockScript: wallet.addressEntity.lockingScript) + } } else if let error = response.error?.message { throw WalletServiceError.internalError(message: error, error: nil) } diff --git a/Adamant/Wallets/Dash/DashWalletService.swift b/Adamant/Wallets/Dash/DashWalletService.swift index 0d4bbacf5..6b8ed93a4 100644 --- a/Adamant/Wallets/Dash/DashWalletService.swift +++ b/Adamant/Wallets/Dash/DashWalletService.swift @@ -12,7 +12,7 @@ import Alamofire import BitcoinKit import Combine -class DashWalletService: WalletService { +final class DashWalletService: WalletService { var tokenSymbol: String { return type(of: self).currencySymbol @@ -62,6 +62,7 @@ class DashWalletService: WalletService { var securedStore: SecuredStore! var dialogService: DialogService! var router: Router! + var addressConverter: AddressConverter! // MARK: - Constants static var currencyLogo = #imageLiteral(resourceName: "dash_wallet") @@ -239,7 +240,9 @@ class DashWalletService: WalletService { } func validate(address: String) -> AddressValidationResult { - return AddressFactory.isValid(bitcoinAddress: address) ? .valid : .invalid + (try? addressConverter.convert(address: address)) != nil + ? .valid + : .invalid } } @@ -266,7 +269,11 @@ extension DashWalletService: InitiatedWithPassphraseService { let privateKeyData = passphrase.data(using: .utf8)!.sha256() let privateKey = PrivateKey(data: privateKeyData, network: self.network, isPublicKeyCompressed: true) - let eWallet = DashWallet(privateKey: privateKey) + let eWallet = try DashWallet( + privateKey: privateKey, + addressConverter: addressConverter + ) + self.dashWallet = eWallet if !self.enabled { @@ -322,6 +329,8 @@ extension DashWalletService: SwinjectDependentService { securedStore = container.resolve(SecuredStore.self) dialogService = container.resolve(DialogService.self) router = container.resolve(Router.self) + addressConverter = container.resolve(AddressConverterFactory.self)? + .make(network: network) } } diff --git a/Adamant/Wallets/Doge/DogeWallet.swift b/Adamant/Wallets/Doge/DogeWallet.swift index 329c45e7b..20451d1f5 100644 --- a/Adamant/Wallets/Doge/DogeWallet.swift +++ b/Adamant/Wallets/Doge/DogeWallet.swift @@ -9,8 +9,8 @@ import Foundation import BitcoinKit -class DogeWallet: WalletAccount { - let address: String +final class DogeWallet: WalletAccount { + let addressEntity: Address let privateKey: PrivateKey let publicKey: PublicKey var balance: Decimal = 0.0 @@ -19,17 +19,24 @@ class DogeWallet: WalletAccount { var minAmount: Decimal = 0 var isBalanceInitialized: Bool = false - init(privateKey: PrivateKey) { + var address: String { addressEntity.stringValue } + + init(privateKey: PrivateKey, addressConverter: AddressConverter) throws { self.privateKey = privateKey self.publicKey = privateKey.publicKey() - self.address = publicKey.toCashaddr().base58 + self.addressEntity = try addressConverter.convert(publicKey: publicKey, type: .p2pkh) } - init(address: String, privateKey: PrivateKey, balance: Decimal, notifications: Int) { + init( + privateKey: PrivateKey, + balance: Decimal, + notifications: Int, + addressConverter: AddressConverter + ) throws { self.privateKey = privateKey self.balance = balance self.notifications = notifications self.publicKey = privateKey.publicKey() - self.address = publicKey.toCashaddr().base58 + self.addressEntity = try addressConverter.convert(publicKey: publicKey, type: .p2pkh) } } diff --git a/Adamant/Wallets/Doge/DogeWalletService+Send.swift b/Adamant/Wallets/Doge/DogeWalletService+Send.swift index 737ff1351..f2247c483 100644 --- a/Adamant/Wallets/Doge/DogeWalletService+Send.swift +++ b/Adamant/Wallets/Doge/DogeWalletService+Send.swift @@ -35,10 +35,9 @@ extension DogeWalletService: WalletServiceTwoStepSend { throw WalletServiceError.notLogged } - let changeAddress = wallet.publicKey.toCashaddr() let key = wallet.privateKey - guard let toAddress = try? LegacyAddress(recipient, for: self.network) else { + guard let toAddress = try? addressConverter.convert(address: recipient) else { throw WalletServiceError.accountNotFound } @@ -56,7 +55,14 @@ extension DogeWalletService: WalletServiceTwoStepSend { } // Create local transaction - let transaction = BitcoinKit.Transaction.createNewTransaction(toAddress: toAddress, amount: rawAmount, fee: fee, changeAddress: changeAddress, utxos: utxos, keys: [key]) + let transaction = BitcoinKit.Transaction.createNewTransaction( + toAddress: toAddress, + amount: rawAmount, + fee: fee, + changeAddress: wallet.addressEntity, + utxos: utxos, + keys: [key] + ) return transaction } catch { throw WalletServiceError.notEnoughMoney diff --git a/Adamant/Wallets/Doge/DogeWalletService.swift b/Adamant/Wallets/Doge/DogeWalletService.swift index f44becc1d..19fb434ce 100644 --- a/Adamant/Wallets/Doge/DogeWalletService.swift +++ b/Adamant/Wallets/Doge/DogeWalletService.swift @@ -58,6 +58,7 @@ class DogeWalletService: WalletService { var accountService: AccountService! var dialogService: DialogService! var router: Router! + var addressConverter: AddressConverter! // MARK: - Constants static var currencyLogo = #imageLiteral(resourceName: "doge_wallet") @@ -225,7 +226,9 @@ class DogeWalletService: WalletService { } func validate(address: String) -> AddressValidationResult { - return AddressFactory.isValid(bitcoinAddress: address) ? .valid : .invalid + (try? addressConverter.convert(address: address)) != nil + ? .valid + : .invalid } } @@ -251,7 +254,7 @@ extension DogeWalletService: InitiatedWithPassphraseService { let privateKeyData = passphrase.data(using: .utf8)!.sha256() let privateKey = PrivateKey(data: privateKeyData, network: self.network, isPublicKeyCompressed: true) - let eWallet = DogeWallet(privateKey: privateKey) + let eWallet = try DogeWallet(privateKey: privateKey, addressConverter: addressConverter) self.dogeWallet = eWallet if !self.enabled { @@ -307,6 +310,8 @@ extension DogeWalletService: SwinjectDependentService { apiService = container.resolve(ApiService.self) dialogService = container.resolve(DialogService.self) router = container.resolve(Router.self) + addressConverter = container.resolve(AddressConverterFactory.self)? + .make(network: network) } } @@ -529,8 +534,8 @@ extension DogeWalletService { } let value = NSDecimalNumber(decimal: (amount.decimalValue * DogeWalletService.multiplier)).uint64Value - - let lockScript = Script.buildPublicKeyHashOut(pubKeyHash: wallet.publicKey.toCashaddr().data) + + let lockScript = wallet.addressEntity.lockingScript let txHash = Data(hex: txid).map { Data($0.reversed()) } ?? Data() let txIndex = vout.uint32Value diff --git a/BitcoinKit/Sources/BitcoinKit/Core/BitcoinError.swift b/BitcoinKit/Sources/BitcoinKit/Core/BitcoinError.swift new file mode 100644 index 000000000..c9d843a22 --- /dev/null +++ b/BitcoinKit/Sources/BitcoinKit/Core/BitcoinError.swift @@ -0,0 +1,33 @@ +// +// BitcoinError.swift +// +// +// Created by Andrey Golubenko on 06.06.2023. +// + +import Foundation + +enum BitcoinError: LocalizedError { + case unknownAddressType + case invalidAddressLength + case invalidChecksum + case wrongAddressPrefix + case list(errors: [Error]) + + var errorDescription: String? { + switch self { + case .unknownAddressType: + return "Unknown address type" + case .invalidAddressLength: + return "Invalid address length" + case .invalidChecksum: + return "Invalid checksum" + case .wrongAddressPrefix: + return "Wrong address prefix" + case let .list(errors): + return errors + .map { $0.localizedDescription } + .joined(separator: ". ") + } + } +} diff --git a/BitcoinKit/Sources/BitcoinKit/Core/Keys/Address.swift b/BitcoinKit/Sources/BitcoinKit/Core/Keys/Address.swift deleted file mode 100644 index 7ec966c19..000000000 --- a/BitcoinKit/Sources/BitcoinKit/Core/Keys/Address.swift +++ /dev/null @@ -1,268 +0,0 @@ -// -// Address.swift -// -// Copyright © 2018 Kishikawa Katsumi -// Copyright © 2018 BitcoinKit developers -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -// - -import Foundation - -public protocol AddressProtocol { - var network: Network { get } - var type: AddressType { get } - var data: Data { get } - var publicKey: Data? { get } - - var base58: String { get } - var cashaddr: String { get } -} - -#if os(iOS) || os(tvOS) || os(watchOS) -public typealias Address = AddressProtocol & QRCodeConvertible -#else -public typealias Address = AddressProtocol -#endif - -public enum AddressError: Error { - case invalid - case invalidScheme - case invalidVersionByte -} - -public struct LegacyAddress: Address { - public let network: Network - public let type: AddressType - public let data: Data - public let base58: Base58Check - public let cashaddr: String - public let publicKey: Data? - - public typealias Base58Check = String - - public init(data: Data, type: AddressType, network: Network, base58: String, bech32: String, publicKey: Data?) { - self.data = data - self.type = type - self.network = network - self.base58 = base58 - self.cashaddr = bech32 - self.publicKey = publicKey - } - - public init(_ base58: Base58Check) throws { - guard let raw = Base58.decode(base58) else { - throw AddressError.invalid - } - let checksum = raw.suffix(4) - let pubKeyHash = raw.dropLast(4) - let checksumConfirm = Crypto.sha256sha256(pubKeyHash).prefix(4) - guard checksum == checksumConfirm else { - throw AddressError.invalid - } - - let network: Network - let type: AddressType - let addressPrefix = pubKeyHash[0] - switch addressPrefix { - case Network.mainnet.pubkeyhash: - network = .mainnet - type = .pubkeyHash - case Network.testnet.pubkeyhash: - network = .testnet - type = .pubkeyHash - case Network.mainnet.scripthash: - network = .mainnet - type = .scriptHash - case Network.testnet.scripthash: - network = .testnet - type = .scriptHash - default: - throw AddressError.invalidVersionByte - } - - self.network = network - self.type = type - self.publicKey = nil - self.data = pubKeyHash.dropFirst() - self.base58 = base58 - - // cashaddr - switch type { - case .pubkeyHash, .scriptHash: - let payload = Data([type.versionByte160]) + self.data - self.cashaddr = Bech32.encode(payload, prefix: network.scheme) - default: - self.cashaddr = "" - } - } - - public init(_ base58: Base58Check, for network: Network) throws { - guard let raw = Base58.decode(base58) else { - throw AddressError.invalid - } - let checksum = raw.suffix(4) - let pubKeyHash = raw.dropLast(4) - let checksumConfirm = Crypto.sha256sha256(pubKeyHash).prefix(4) - guard checksum == checksumConfirm else { - throw AddressError.invalid - } - - let type: AddressType - let addressPrefix = pubKeyHash[0] - switch addressPrefix { - case network.pubkeyhash: - type = .pubkeyHash - case network.scripthash: - type = .scriptHash - default: - throw AddressError.invalidVersionByte - } - - self.network = network - self.type = type - self.publicKey = nil - self.data = pubKeyHash.dropFirst() - self.base58 = base58 - - // cashaddr - switch type { - case .pubkeyHash, .scriptHash: - let payload = Data([type.versionByte160]) + self.data - self.cashaddr = Bech32.encode(payload, prefix: network.scheme) - default: - self.cashaddr = "" - } - } - - public init(data: Data, type: AddressType, network: Network) { - let addressData: Data = [type.versionByte] + data - self.data = data - self.type = type - self.network = network - self.publicKey = nil - self.base58 = publicKeyHashToAddress(addressData) - self.cashaddr = Bech32.encode(addressData, prefix: network.scheme) - } -} - -extension LegacyAddress: Equatable { - public static func == (lhs: LegacyAddress, rhs: LegacyAddress) -> Bool { - return lhs.network == rhs.network && lhs.data == rhs.data && lhs.type == rhs.type - } -} - -extension LegacyAddress: CustomStringConvertible { - public var description: String { - return base58 - } -} - -public struct Cashaddr: Address { - public let network: Network - public let type: AddressType - public let data: Data - public let base58: String - public let cashaddr: CashaddrWithScheme - public let publicKey: Data? - - public typealias CashaddrWithScheme = String - - public init(data: Data, type: AddressType, network: Network, base58: String, bech32: CashaddrWithScheme, publicKey: Data?) { - self.data = data - self.type = type - self.network = network - self.base58 = base58 - self.cashaddr = bech32 - self.publicKey = publicKey - } - - public init(_ cashaddr: CashaddrWithScheme) throws { - guard let decoded = Bech32.decode(cashaddr) else { - throw AddressError.invalid - } - let (prefix, raw) = (decoded.prefix, decoded.data) - self.cashaddr = cashaddr - self.publicKey = nil - - switch prefix { - case Network.mainnet.scheme: - network = .mainnet - case Network.testnet.scheme: - network = .testnet - default: - throw AddressError.invalidScheme - } - - let versionByte = raw[0] - let hash = raw.dropFirst() - - guard hash.count == VersionByte.getSize(from: versionByte) else { - throw AddressError.invalidVersionByte - } - self.data = hash - guard let typeBits = VersionByte.TypeBits(rawValue: (versionByte & 0b01111000)) else { - throw AddressError.invalidVersionByte - } - - switch typeBits { - case .pubkeyHash: - type = .pubkeyHash - base58 = publicKeyHashToAddress(Data([network.pubkeyhash]) + data) - case .scriptHash: - type = .scriptHash - base58 = publicKeyHashToAddress(Data([network.scripthash]) + data) - } - } - public init(data: Data, type: AddressType, network: Network) { - let addressData: Data = [type.versionByte] + data - self.data = data - self.type = type - self.network = network - self.publicKey = nil - self.base58 = publicKeyHashToAddress(addressData) - self.cashaddr = Bech32.encode(addressData, prefix: network.scheme) - } -} - -extension Cashaddr: Equatable { - public static func == (lhs: Cashaddr, rhs: Cashaddr) -> Bool { - return lhs.network == rhs.network && lhs.data == rhs.data && lhs.type == rhs.type - } -} - -extension Cashaddr: CustomStringConvertible { - public var description: String { - return cashaddr - } -} - -extension AddressProtocol { - public var lockingScript: Data { - switch type { - case .pubkeyHash: - return Script.buildPublicKeyHashOut(pubKeyHash: data) - case .scriptHash: - return Script.buildScriptHashOut(scriptHash: data) - default: - assertionFailure("Unknown type") - return .init() - } - } -} diff --git a/BitcoinKit/Sources/BitcoinKit/Core/Keys/AddressFactory.swift b/BitcoinKit/Sources/BitcoinKit/Core/Keys/AddressFactory.swift deleted file mode 100644 index 80c8741f9..000000000 --- a/BitcoinKit/Sources/BitcoinKit/Core/Keys/AddressFactory.swift +++ /dev/null @@ -1,81 +0,0 @@ -// -// AddressFactory.swift -// -// Copyright © 2018 BitcoinKit developers -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -// - -import Foundation - -public struct AddressFactory { - public static func create(_ plainAddress: String) throws -> Address { - do { - return try Cashaddr(plainAddress) - } catch AddressError.invalidVersionByte { - throw AddressError.invalidVersionByte - } catch AddressError.invalidScheme { - throw AddressError.invalidScheme - } catch AddressError.invalid { - return try LegacyAddress(plainAddress) - } - } - - private static func getBase58DecodeAsBytes(address: String, length: Int) -> [UTF8.CodeUnit]? { - let b58Chars = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" - - var output: [UTF8.CodeUnit] = Array(repeating: 0, count: length) - - for i in 0.. Bool { - guard address.count >= 26 && address.count <= 35, - address.rangeOfCharacter(from: CharacterSet.alphanumerics.inverted) == nil, - let decodedAddress = getBase58DecodeAsBytes(address: address, length: 25), - decodedAddress.count >= 4 - else { return false } - - let decodedAddressNoCheckSum = Array(decodedAddress.prefix(decodedAddress.count - 4)) - - let hashedSum = Crypto.sha256sha256(Data(decodedAddressNoCheckSum)) - - let checkSum = Array(decodedAddress.suffix(from: decodedAddress.count - 4)) - let hashedSumHeader = Array(hashedSum.prefix(4)) - - return hashedSumHeader == checkSum - } -} diff --git a/BitcoinKit/Sources/BitcoinKit/Core/Keys/AddressType.swift b/BitcoinKit/Sources/BitcoinKit/Core/Keys/AddressType.swift index 02fb3d68a..500b19e5c 100644 --- a/BitcoinKit/Sources/BitcoinKit/Core/Keys/AddressType.swift +++ b/BitcoinKit/Sources/BitcoinKit/Core/Keys/AddressType.swift @@ -24,11 +24,19 @@ import Foundation -public class AddressType { - static let pubkeyHash: AddressType = PubkeyHash() - static let scriptHash: AddressType = ScriptHash() - - var versionByte: UInt8 { return 0 } +public enum AddressType { + case pubkeyHash + case scriptHash + + var versionByte: UInt8 { + switch self { + case .pubkeyHash: + return .zero + case .scriptHash: + return 8 + } + } + var versionByte160: UInt8 { return versionByte + 0 } var versionByte192: UInt8 { return versionByte + 1 } var versionByte224: UInt8 { return versionByte + 2 } @@ -44,9 +52,3 @@ extension AddressType: Equatable { return lhs.versionByte == rhs.versionByte } } -public class PubkeyHash: AddressType { - public override var versionByte: UInt8 { return 0 } -} -public class ScriptHash: AddressType { - public override var versionByte: UInt8 { return 8 } -} diff --git a/BitcoinKit/Sources/BitcoinKit/Core/Keys/Addresses/Address.swift b/BitcoinKit/Sources/BitcoinKit/Core/Keys/Addresses/Address.swift new file mode 100644 index 000000000..e412f22d7 --- /dev/null +++ b/BitcoinKit/Sources/BitcoinKit/Core/Keys/Addresses/Address.swift @@ -0,0 +1,21 @@ +// +// Address.swift +// +// +// Created by Andrey Golubenko on 05.06.2023. +// + +import Foundation + +public protocol AddressProtocol { + var scriptType: ScriptType { get } + var lockingScriptPayload: Data { get } + var stringValue: String { get } + var lockingScript: Data { get } +} + +#if os(iOS) || os(tvOS) || os(watchOS) +public typealias Address = AddressProtocol & QRCodeConvertible +#else +public typealias Address = AddressProtocol +#endif diff --git a/BitcoinKit/Sources/BitcoinKit/Core/Keys/Addresses/LegacyAddress.swift b/BitcoinKit/Sources/BitcoinKit/Core/Keys/Addresses/LegacyAddress.swift new file mode 100644 index 000000000..cdc74ab90 --- /dev/null +++ b/BitcoinKit/Sources/BitcoinKit/Core/Keys/Addresses/LegacyAddress.swift @@ -0,0 +1,63 @@ +// +// LegacyAddress.swift +// +// Copyright © 2018 Kishikawa Katsumi +// Copyright © 2018 BitcoinKit developers +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +public final class LegacyAddress: Address, Equatable { + public let type: AddressType + public let lockingScriptPayload: Data + public let stringValue: String + + public var scriptType: ScriptType { + switch type { + case .pubkeyHash: return .p2pkh + case .scriptHash: return .p2sh + } + } + + public var qrcodeString: String { + stringValue + } + + public var lockingScript: Data { + switch type { + case .pubkeyHash: return OpCode.p2pkhStart + OpCode.push(lockingScriptPayload) + OpCode.p2pkhFinish + case .scriptHash: return OpCode.p2shStart + OpCode.push(lockingScriptPayload) + OpCode.p2shFinish + } + } + + public init(type: AddressType, payload: Data, base58: String) { + self.type = type + self.lockingScriptPayload = payload + self.stringValue = base58 + } + + public static func ==(lhs: LegacyAddress, rhs: T) -> Bool { + guard let rhs = rhs as? LegacyAddress else { + return false + } + return lhs.type == rhs.type && lhs.lockingScriptPayload == rhs.lockingScriptPayload + } +} diff --git a/BitcoinKit/Sources/BitcoinKit/Core/Keys/Addresses/SegWitV0Address.swift b/BitcoinKit/Sources/BitcoinKit/Core/Keys/Addresses/SegWitV0Address.swift new file mode 100644 index 000000000..f8ebd58dc --- /dev/null +++ b/BitcoinKit/Sources/BitcoinKit/Core/Keys/Addresses/SegWitV0Address.swift @@ -0,0 +1,35 @@ +import Foundation + +public final class SegWitV0Address: Address, Equatable { + public let type: AddressType + public let lockingScriptPayload: Data + public let stringValue: String + + public var qrcodeString: String { + stringValue + } + + public var scriptType: ScriptType { + switch type { + case .pubkeyHash: return .p2wpkh + case .scriptHash: return .p2wsh + } + } + + public var lockingScript: Data { + OpCode.segWitOutputScript(lockingScriptPayload, versionByte: 0) + } + + public init(type: AddressType, payload: Data, bech32: String) { + self.type = type + self.lockingScriptPayload = payload + self.stringValue = bech32 + } + + static public func ==(lhs: SegWitV0Address, rhs: T) -> Bool { + guard let rhs = rhs as? SegWitV0Address else { + return false + } + return lhs.type == rhs.type && lhs.lockingScriptPayload == rhs.lockingScriptPayload + } +} diff --git a/BitcoinKit/Sources/BitcoinKit/Core/Keys/Addresses/TaprootAddress.swift b/BitcoinKit/Sources/BitcoinKit/Core/Keys/Addresses/TaprootAddress.swift new file mode 100644 index 000000000..a89047ff3 --- /dev/null +++ b/BitcoinKit/Sources/BitcoinKit/Core/Keys/Addresses/TaprootAddress.swift @@ -0,0 +1,29 @@ +import Foundation + +public final class TaprootAddress: Address, Equatable { + public let lockingScriptPayload: Data + public let stringValue: String + public let version: UInt8 + public var scriptType = ScriptType.p2tr + + public var lockingScript: Data { + OpCode.segWitOutputScript(lockingScriptPayload, versionByte: Int(version)) + } + + public var qrcodeString: String { + stringValue + } + + public init(payload: Data, bech32m: String, version: UInt8) { + self.lockingScriptPayload = payload + self.stringValue = bech32m + self.version = version + } + + static public func ==(lhs: TaprootAddress, rhs: T) -> Bool { + guard let rhs = rhs as? TaprootAddress else { + return false + } + return lhs.lockingScriptPayload == rhs.lockingScriptPayload && lhs.version == rhs.version + } +} diff --git a/BitcoinKit/Sources/BitcoinKit/Core/Keys/Bech32.swift b/BitcoinKit/Sources/BitcoinKit/Core/Keys/Bech32.swift new file mode 100644 index 000000000..4d560825f --- /dev/null +++ b/BitcoinKit/Sources/BitcoinKit/Core/Keys/Bech32.swift @@ -0,0 +1,208 @@ +// +// Bech32.swift +// +// Created by Evolution Group Ltd on 12.02.2018. +// Copyright © 2018 Evolution Group Ltd. All rights reserved. +// + +// Base32 address format for native v0-16 witness outputs implementation +// https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki +// Inspired by Pieter Wuille C++ implementation + +import Foundation + +/// Bech32 checksum implementation +public final class Bech32 { + static let shared = Bech32() + + private let gen: [UInt32] = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3] + /// Bech32 checksum delimiter + private let checksumMarker: String = "1" + /// Bech32 character set for encoding + private let encCharset: Data = "qpzry9x8gf2tvdw0s3jn54khce6mua7l".data(using: .utf8)! + /// Bech32 character set for decoding + private let decCharset: [Int8] = [ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + 15, -1, 10, 17, 21, 20, 26, 30, 7, 5, -1, -1, -1, -1, -1, -1, + -1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, + 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1, + -1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, + 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1 + ] + + public init() {} + + /// Find the polynomial with value coefficients mod the generator as 30-bit. + private func polymod(_ values: Data) -> UInt32 { + var chk: UInt32 = 1 + for v in values { + let top = (chk >> 25) + chk = (chk & 0x1ffffff) << 5 ^ UInt32(v) + for i: UInt8 in 0..<5 { + chk ^= ((top >> i) & 1) == 0 ? 0 : gen[Int(i)] + } + } + return chk + } + + /// Expand a HRP for use in checksum computation. + private func expandHrp(_ hrp: String) -> Data { + guard let hrpBytes = hrp.data(using: .utf8) else { return Data() } + var result = Data(repeating: 0x00, count: hrpBytes.count*2+1) + for (i, c) in hrpBytes.enumerated() { + result[i] = c >> 5 + result[i + hrpBytes.count + 1] = c & 0x1f + } + result[hrp.count] = 0 + return result + } + + private func extractChecksumWithEncoding(hrp: String, checksum: Data) -> (check: UInt32, encoding: Encoding)? { + var data = expandHrp(hrp) + data.append(checksum) + let check = polymod(data) + return Encoding.fromCheck(check).flatMap { (check, $0) } + } + + /// Create checksum + private func createChecksum(hrp: String, values: Data, encoding: Encoding) -> Data { + var enc = expandHrp(hrp) + enc.append(values) + enc.append(Data(repeating: 0x00, count: 6)) + let mod: UInt32 = polymod(enc) ^ encoding.checksumXorConstant + var ret: Data = Data(repeating: 0x00, count: 6) + for i in 0..<6 { + ret[i] = UInt8((mod >> (5 * (5 - i))) & 31) + } + return ret + } + + /// Encode Bech32 string + public func encode(_ hrp: String, values: Data, encoding: Encoding) -> String { + let checksum = createChecksum(hrp: hrp, values: values, encoding: encoding) + var combined = values + combined.append(checksum) + guard let hrpBytes = hrp.data(using: .utf8) else { return "" } + var ret = hrpBytes + ret.append("1".data(using: .utf8)!) + for i in combined { + ret.append(encCharset[Int(i)]) + } + return String(data: ret, encoding: .utf8) ?? "" + } + + /// Decode Bech32 string + public func decode(_ str: String) throws -> (hrp: String, checksum: Data, encoding: Encoding) { + guard let strBytes = str.data(using: .utf8) else { + throw DecodingError.nonUTF8String + } + guard strBytes.count <= 90 else { + throw DecodingError.stringLengthExceeded + } + var lower: Bool = false + var upper: Bool = false + for c in strBytes { + // printable range + if c < 33 || c > 126 { + throw DecodingError.nonPrintableCharacter + } + // 'a' to 'z' + if c >= 97 && c <= 122 { + lower = true + } + // 'A' to 'Z' + if c >= 65 && c <= 90 { + upper = true + } + } + if lower && upper { + throw DecodingError.invalidCase + } + guard let pos = str.range(of: checksumMarker, options: .backwards)?.lowerBound else { + throw DecodingError.noChecksumMarker + } + let intPos: Int = str.distance(from: str.startIndex, to: pos) + guard intPos >= 1 else { + throw DecodingError.incorrectHrpSize + } + guard intPos + 7 <= str.count else { + throw DecodingError.incorrectChecksumSize + } + let vSize: Int = str.count - 1 - intPos + var values: Data = Data(repeating: 0x00, count: vSize) + for i in 0.. Encoding? { + switch check { + case Encoding.bech32.checksumXorConstant: return .bech32 + case Encoding.bech32m.checksumXorConstant: return .bech32m + default: return nil + } + } + } +} diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/AddressConverter.swift b/BitcoinKit/Sources/BitcoinKit/Core/Keys/Converters/AddressConverter.swift similarity index 81% rename from BitcoinKit/Sources/BitcoinKit/Scripts/AddressConverter.swift rename to BitcoinKit/Sources/BitcoinKit/Core/Keys/Converters/AddressConverter.swift index 1bb84f171..69b260db3 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/AddressConverter.swift +++ b/BitcoinKit/Sources/BitcoinKit/Core/Keys/Converters/AddressConverter.swift @@ -1,21 +1,20 @@ // // AddressConverter.swift -// BitcoinKit +// // -// Created by Anton Boyarkin on 12/02/2019. +// Created by Andrey Golubenko on 06.06.2023. // import Foundation -class AddressConverter { - enum ConversionError: Error { - case invalidChecksum - case invalidAddressLength - case unknownAddressType - case wrongAddressPrefix - } - - public static func extract(from signatureScript: Data, with network: Network) -> Address? { +public protocol AddressConverter { + func convert(address: String) throws -> Address + func convert(lockingScriptPayload: Data, type: ScriptType) throws -> Address + func convert(publicKey: PublicKey, type: ScriptType) throws -> Address +} + +extension AddressConverter { + public func extract(from signatureScript: Data, with network: Network) -> Address? { var payload: Data? var validScriptType: ScriptType = ScriptType.unknown let sigScriptCount = signatureScript.count @@ -60,7 +59,8 @@ class AddressConverter { } if let payload = payload { let keyHash = Crypto.sha256ripemd160(payload) - if let address = try? network.convert(keyHash: keyHash, type: validScriptType) { + let address = try? convert(lockingScriptPayload: payload, type: validScriptType) + if let address = address { outputAddress = address } } diff --git a/BitcoinKit/Sources/BitcoinKit/Core/Keys/Converters/AddressConverterChain.swift b/BitcoinKit/Sources/BitcoinKit/Core/Keys/Converters/AddressConverterChain.swift new file mode 100644 index 000000000..ca1b56394 --- /dev/null +++ b/BitcoinKit/Sources/BitcoinKit/Core/Keys/Converters/AddressConverterChain.swift @@ -0,0 +1,54 @@ +import Foundation + +final class AddressConverterChain: AddressConverter { + private let concreteConverters: [AddressConverter] + + init(concreteConverters: [AddressConverter]) { + self.concreteConverters = concreteConverters + } + + func convert(address: String) throws -> Address { + var errors = [Error]() + + for converter in concreteConverters { + do { + let converted = try converter.convert(address: address) + return converted + } catch { + errors.append(error) + } + } + + throw BitcoinError.list(errors: errors) + } + + func convert(lockingScriptPayload: Data, type: ScriptType) throws -> Address { + var errors = [Error]() + + for converter in concreteConverters { + do { + let converted = try converter.convert(lockingScriptPayload: lockingScriptPayload, type: type) + return converted + } catch { + errors.append(error) + } + } + + throw BitcoinError.list(errors: errors) + } + + public func convert(publicKey: PublicKey, type: ScriptType) throws -> Address { + var errors = [Error]() + + for converter in concreteConverters { + do { + let converted = try converter.convert(publicKey: publicKey, type: type) + return converted + } catch { + errors.append(error) + } + } + + throw BitcoinError.list(errors: errors) + } +} diff --git a/BitcoinKit/Sources/BitcoinKit/Core/Keys/Converters/AddressConverterFactory.swift b/BitcoinKit/Sources/BitcoinKit/Core/Keys/Converters/AddressConverterFactory.swift new file mode 100644 index 000000000..e0deec1b0 --- /dev/null +++ b/BitcoinKit/Sources/BitcoinKit/Core/Keys/Converters/AddressConverterFactory.swift @@ -0,0 +1,24 @@ +// +// AddressConverterFactory.swift +// +// +// Created by Andrey Golubenko on 06.06.2023. +// + +public struct AddressConverterFactory { + public func make(network: Network) -> AddressConverter { + let segWitAddressConverter = SegWitBech32AddressConverter(prefix: "bc") + + let base58AddressConverter = Base58AddressConverter( + addressVersion: network.pubkeyhash, + addressScriptVersion: network.scripthash + ) + + return AddressConverterChain(concreteConverters: [ + segWitAddressConverter, + base58AddressConverter + ]) + } + + public init() {} +} diff --git a/BitcoinKit/Sources/BitcoinKit/Core/Keys/Converters/Base58AddressConverter.swift b/BitcoinKit/Sources/BitcoinKit/Core/Keys/Converters/Base58AddressConverter.swift new file mode 100644 index 000000000..c514e64c0 --- /dev/null +++ b/BitcoinKit/Sources/BitcoinKit/Core/Keys/Converters/Base58AddressConverter.swift @@ -0,0 +1,71 @@ +import Foundation + +final class Base58AddressConverter: AddressConverter { + private static let checkSumLength = 4 + private let addressVersion: UInt8 + private let addressScriptVersion: UInt8 + + init(addressVersion: UInt8, addressScriptVersion: UInt8) { + self.addressVersion = addressVersion + self.addressScriptVersion = addressScriptVersion + } + + func convert(address: String) throws -> Address { + // check length of address to avoid wrong converting + guard address.count >= 26 && address.count <= 35 else { + throw BitcoinError.invalidAddressLength + } + + guard let hex = Base58.decode(address) else { + throw BitcoinError.unknownAddressType + } + + // check decoded length. Must be 1(version) + 20(KeyHash) + 4(CheckSum) + if hex.count != Base58AddressConverter.checkSumLength + 20 + 1 { + throw BitcoinError.invalidAddressLength + } + let givenChecksum = hex.suffix(Base58AddressConverter.checkSumLength) + let doubleSHA256 = Crypto.sha256sha256(hex.prefix(hex.count - Base58AddressConverter.checkSumLength)) + let actualChecksum = doubleSHA256.prefix(Base58AddressConverter.checkSumLength) + guard givenChecksum == actualChecksum else { + throw BitcoinError.invalidChecksum + } + + let type: AddressType + switch hex[0] { + case addressVersion: type = AddressType.pubkeyHash + case addressScriptVersion: type = AddressType.scriptHash + default: throw BitcoinError.wrongAddressPrefix + } + + let keyHash = hex.dropFirst().dropLast(4) + return LegacyAddress(type: type, payload: keyHash, base58: address) + } + + func convert(lockingScriptPayload: Data, type: ScriptType) throws -> Address { + let version: UInt8 + let addressType: AddressType + + switch type { + case .p2pkh, .p2pk: + version = addressVersion + addressType = AddressType.pubkeyHash + case .p2sh, .p2wpkhSh: + version = addressScriptVersion + addressType = AddressType.scriptHash + default: throw BitcoinError.unknownAddressType + } + + var withVersion = (Data([version])) + lockingScriptPayload + let doubleSHA256 = Crypto.sha256sha256(withVersion) + let checksum = doubleSHA256.prefix(4) + withVersion += checksum + let base58 = Base58.encode(withVersion) + return LegacyAddress(type: addressType, payload: lockingScriptPayload, base58: base58) + } + + func convert(publicKey: PublicKey, type: ScriptType) throws -> Address { + let keyHash = type == .p2wpkhSh ? publicKey.hashP2wpkhWrappedInP2sh : publicKey.hashP2pkh + return try convert(lockingScriptPayload: keyHash, type: type) + } +} diff --git a/BitcoinKit/Sources/BitcoinKit/Core/Keys/Converters/SegWitBech32AddressConverter.swift b/BitcoinKit/Sources/BitcoinKit/Core/Keys/Converters/SegWitBech32AddressConverter.swift new file mode 100644 index 000000000..8342fa329 --- /dev/null +++ b/BitcoinKit/Sources/BitcoinKit/Core/Keys/Converters/SegWitBech32AddressConverter.swift @@ -0,0 +1,67 @@ +// +// SegWitBech32AddressConverter.swift +// +// +// Created by Andrey Golubenko on 05.06.2023. +// + +import Foundation + +final class SegWitBech32AddressConverter: AddressConverter { + private let prefix: String + private let hasAdvanced: Bool + + init(prefix: String, hasAdvanced: Bool = true) { + self.prefix = prefix + self.hasAdvanced = hasAdvanced + } + + func convert(address: String) throws -> Address { + if let segWitData = try? SegWitBech32.decode(hrp: prefix, addr: address, hasAdvanced: hasAdvanced) { + switch segWitData.version { + case 0: + var type: AddressType = .pubkeyHash + switch segWitData.program.count { + case 20: type = .pubkeyHash + case 32: type = .scriptHash + default: break + } + return SegWitV0Address(type: type, payload: segWitData.program, bech32: address) + case 1: + guard segWitData.program.count == 32 else { + break + } + return TaprootAddress(payload: segWitData.program, bech32m: address, version: segWitData.version) + default: + break + } + } + throw BitcoinError.unknownAddressType + } + + func convert(lockingScriptPayload: Data, type: ScriptType) throws -> Address { + switch type { + case .p2wpkh: + let bech32 = try SegWitBech32.encode(hrp: prefix, version: 0, program: lockingScriptPayload, encoding: .bech32) + return SegWitV0Address(type: AddressType.pubkeyHash, payload: lockingScriptPayload, bech32: bech32) + case .p2wsh: + let bech32 = try SegWitBech32.encode(hrp: prefix, version: 0, program: lockingScriptPayload, encoding: .bech32) + return SegWitV0Address(type: AddressType.scriptHash, payload: lockingScriptPayload, bech32: bech32) + case .p2tr: + let bech32 = try SegWitBech32.encode(hrp: prefix, version: 1, program: lockingScriptPayload, encoding: .bech32m) + return TaprootAddress(payload: lockingScriptPayload, bech32m: bech32, version: 1) + default: throw BitcoinError.unknownAddressType + } + } + + func convert(publicKey: PublicKey, type: ScriptType) throws -> Address { + switch type { + case .p2wpkh, .p2wsh: + return try convert(lockingScriptPayload: publicKey.hashP2pkh, type: type) +// case .p2tr: +// return try convert(lockingScriptPayload: publicKey.convertedForP2tr, type: type) + default: + throw BitcoinError.unknownAddressType + } + } +} diff --git a/BitcoinKit/Sources/BitcoinKit/Core/Keys/Encoding.swift b/BitcoinKit/Sources/BitcoinKit/Core/Keys/Encoding.swift index 1fe920735..ee064f4f0 100644 --- a/BitcoinKit/Sources/BitcoinKit/Core/Keys/Encoding.swift +++ b/BitcoinKit/Sources/BitcoinKit/Core/Keys/Encoding.swift @@ -161,148 +161,3 @@ extension Encoding { return Data(repeating: 0, count: zerosCount) + Data(decodedBytes) } } - -public struct Bech32 { - private static let base32Alphabets = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" - - public static func encode(_ bytes: Data, prefix: String, seperator: String = ":") -> String { - let payload = convertTo5bit(data: bytes, pad: true) - let checksum: Data = createChecksum(prefix: prefix, payload: payload) // Data of [UInt5] - let combined: Data = payload + checksum // Data of [UInt5] - var base32 = "" - for b in combined { - let strIndex = String.Index(utf16Offset: Int(b), in: base32Alphabets) - base32 += String(base32Alphabets[strIndex]) - } - - return prefix + seperator + base32 - } - - // string : "bitcoincash:qql8zpwglr3q5le9jnjxkmypefaku39dkygsx29fzk" - public static func decode(_ string: String, seperator: String = ":") -> (prefix: String, data: Data)? { - // We can't have empty string. - // Bech32 should be uppercase only / lowercase only. - guard !string.isEmpty && [string.lowercased(), string.uppercased()].contains(string) else { - return nil - } - - let components = string.components(separatedBy: seperator) - // We can only handle string contains both scheme and base32 - guard components.count == 2 else { - return nil - } - let (prefix, base32) = (components[0], components[1]) - - var decodedIn5bit: [UInt8] = [UInt8]() - for c in base32.lowercased() { - // We can't have characters other than base32 alphabets. - guard let baseIndex = base32Alphabets.firstIndex(of: c)?.utf16Offset(in: base32Alphabets) else { - return nil - } - decodedIn5bit.append(UInt8(baseIndex)) - } - - // We can't have invalid checksum - let payload = Data(decodedIn5bit) - guard verifyChecksum(prefix: prefix, payload: payload) else { - return nil - } - - // Drop checksum - guard let bytes = try? convertFrom5bit(data: payload.dropLast(8)) else { - return nil - } - return (prefix, Data(bytes)) - } - - private static func verifyChecksum(prefix: String, payload: Data) -> Bool { - return PolyMod(expand(prefix) + payload) == 0 - } - - private static func expand(_ prefix: String) -> Data { - var ret: Data = Data() - let buf: [UInt8] = Array(prefix.utf8) - for b in buf { - ret += b & 0x1f - } - ret += Data(repeating: 0, count: 1) - return ret - } - - private static func createChecksum(prefix: String, payload: Data) -> Data { - let enc: Data = expand(prefix) + payload + Data(repeating: 0, count: 8) - let mod: UInt64 = PolyMod(enc) - var ret: Data = Data() - for i in 0..<8 { - ret += UInt8((mod >> (5 * (7 - i))) & 0x1f) - } - return ret - } - - private static func PolyMod(_ data: Data) -> UInt64 { - var c: UInt64 = 1 - for d in data { - let c0: UInt8 = UInt8(c >> 35) - c = ((c & 0x07ffffffff) << 5) ^ UInt64(d) - if c0 & 0x01 != 0 { c ^= 0x98f2bc8e61 } - if c0 & 0x02 != 0 { c ^= 0x79b76d99e2 } - if c0 & 0x04 != 0 { c ^= 0xf33e5fb3c4 } - if c0 & 0x08 != 0 { c ^= 0xae2eabe2a8 } - if c0 & 0x10 != 0 { c ^= 0x1e4f43e470 } - } - return c ^ 1 - } - - private static func convertTo5bit(data: Data, pad: Bool) -> Data { - var acc = Int() - var bits = UInt8() - let maxv: Int = 31 // 31 = 0x1f = 00011111 - var converted: [UInt8] = [] - for d in data { - acc = (acc << 8) | Int(d) - bits += 8 - - while bits >= 5 { - bits -= 5 - converted.append(UInt8(acc >> Int(bits) & maxv)) - } - } - - let lastBits: UInt8 = UInt8(acc << (5 - bits) & maxv) - if pad && bits > 0 { - converted.append(lastBits) - } - return Data(converted) - } - - internal static func convertFrom5bit(data: Data) throws -> Data { - var acc = Int() - var bits = UInt8() - let maxv: Int = 255 // 255 = 0xff = 11111111 - var converted: [UInt8] = [] - for d in data { - guard (d >> 5) == 0 else { - throw DecodeError.invalidCharacter - } - acc = (acc << 5) | Int(d) - bits += 5 - - while bits >= 8 { - bits -= 8 - converted.append(UInt8(acc >> Int(bits) & maxv)) - } - } - - let lastBits: UInt8 = UInt8(acc << (8 - bits) & maxv) - guard bits < 5 && lastBits == 0 else { - throw DecodeError.invalidBits - } - - return Data(converted) - } - - private enum DecodeError: Error { - case invalidCharacter - case invalidBits - } -} diff --git a/BitcoinKit/Sources/BitcoinKit/Core/Keys/PublicKey.swift b/BitcoinKit/Sources/BitcoinKit/Core/Keys/PublicKey.swift index fff10b408..8d7deebb6 100644 --- a/BitcoinKit/Sources/BitcoinKit/Core/Keys/PublicKey.swift +++ b/BitcoinKit/Sources/BitcoinKit/Core/Keys/PublicKey.swift @@ -34,39 +34,22 @@ public struct PublicKey { public let data: Data @available(*, deprecated, renamed: "data") public var raw: Data { return data } - public var pubkeyHash: Data { - return Crypto.sha256ripemd160(data) - } public let network: Network public let isCompressed: Bool + public let hashP2pkh: Data + public let hashP2wpkhWrappedInP2sh: Data public init(bytes data: Data, network: Network) { self.data = data self.network = network let header = data[0] self.isCompressed = (header == 0x02 || header == 0x03) - } - - /// Version = 1 byte of 0 (zero); on the test network, this is 1 byte of 111 - /// Key hash = Version concatenated with RIPEMD-160(SHA-256(public key)) - /// Checksum = 1st 4 bytes of SHA-256(SHA-256(Key hash)) - /// Bitcoin Address = Base58Encode(Key hash concatenated with Checksum) - private func base58() -> String { - let versionByte: Data = Data([network.pubkeyhash]) - return publicKeyHashToAddress(versionByte + pubkeyHash) - } - - private func bech32() -> String { - let versionByte: Data = Data([VersionByte.pubkeyHash160]) - return Bech32.encode(versionByte + pubkeyHash, prefix: network.scheme) - } - - public func toLegacy() -> LegacyAddress { - return LegacyAddress(data: pubkeyHash, type: .pubkeyHash, network: network, base58: base58(), bech32: bech32(), publicKey: data) - } - - public func toCashaddr() -> Cashaddr { - return Cashaddr(data: pubkeyHash, type: .pubkeyHash, network: network, base58: base58(), bech32: bech32(), publicKey: data) + hashP2pkh = Crypto.sha256ripemd160(data) + + hashP2wpkhWrappedInP2sh = Crypto.sha256ripemd160(OpCode.segWitOutputScript( + hashP2pkh, + versionByte: .zero + )) } } diff --git a/BitcoinKit/Sources/BitcoinKit/Core/Network.swift b/BitcoinKit/Sources/BitcoinKit/Core/Network.swift index 287123a46..e12ac73e4 100644 --- a/BitcoinKit/Sources/BitcoinKit/Core/Network.swift +++ b/BitcoinKit/Sources/BitcoinKit/Core/Network.swift @@ -275,29 +275,3 @@ public class Testnet: Network { return Data(Data(hex: "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943")!.reversed()) } } - -extension Network { - func convert(keyHash: Data, type: ScriptType) throws -> Address { - let version: UInt8 - let addressType: AddressType - switch type { - case .p2pkh, .p2pk: - version = self.pubkeyhash - addressType = .pubkeyHash - case .p2sh, .p2wpkhSh: - version = self.scripthash - addressType = .scriptHash - default: throw AddressConverter.ConversionError.unknownAddressType - } - return try convertToLegacy(keyHash: keyHash, version: version, addressType: addressType) - } - - func convertToLegacy(keyHash: Data, version: UInt8, addressType: AddressType) throws -> LegacyAddress { - var withVersion = (Data([version])) + keyHash - let doubleSHA256 = Crypto.sha256sha256(withVersion) - let checksum = doubleSHA256.prefix(4) - withVersion += checksum - let base58 = Base58.encode(withVersion) - return try LegacyAddress(base58) - } -} diff --git a/BitcoinKit/Sources/BitcoinKit/Core/PaymentURI.swift b/BitcoinKit/Sources/BitcoinKit/Core/PaymentURI.swift index 90237df81..43ea160ea 100644 --- a/BitcoinKit/Sources/BitcoinKit/Core/PaymentURI.swift +++ b/BitcoinKit/Sources/BitcoinKit/Core/PaymentURI.swift @@ -40,11 +40,11 @@ public struct PaymentURI { case amount } - public init(_ string: String) throws { + public init(_ string: String, addressConverter: AddressConverter) throws { guard let components = URLComponents(string: string), let scheme = components.scheme, scheme.lowercased() == "bitcoin" else { throw PaymentURIError.invalid } - guard let address = try? AddressFactory.create(components.path) else { + guard let address = try? addressConverter.convert(address: components.path) else { throw PaymentURIError.malformed(.address) } self.address = address diff --git a/BitcoinKit/Sources/BitcoinKit/Core/SegWitBech32.swift b/BitcoinKit/Sources/BitcoinKit/Core/SegWitBech32.swift new file mode 100644 index 000000000..99c0b94ba --- /dev/null +++ b/BitcoinKit/Sources/BitcoinKit/Core/SegWitBech32.swift @@ -0,0 +1,116 @@ +// +// SegWitBech32.swift +// +// Created by Evolution Group Ltd on 12.02.2018. +// Copyright © 2018 Evolution Group Ltd. All rights reserved. +// + +// Base32 address format for native v0-16 witness outputs implementation +// https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki +// Inspired by Pieter Wuille C++ implementation + +import Foundation + +/// Segregated Witness Address encoder/decoder +public class SegWitBech32 { + /// Convert from one power-of-2 number base to another + private static func convertBits(from: Int, to: Int, pad: Bool, idata: Data) throws -> Data { + var acc: Int = 0 + var bits: Int = 0 + let maxv: Int = (1 << to) - 1 + let maxAcc: Int = (1 << (from + to - 1)) - 1 + var odata = Data() + for ibyte in idata { + acc = ((acc << from) | Int(ibyte)) & maxAcc + bits += from + while bits >= to { + bits -= to + odata.append(UInt8((acc >> bits) & maxv)) + } + } + if pad { + if bits != 0 { + odata.append(UInt8((acc << (to - bits)) & maxv)) + } + } else if (bits >= from || ((acc << (to - bits)) & maxv) != 0) { + throw CoderError.bitsConversionFailed + } + return odata + } + + /// Decode segwit address + public static func decode(hrp: String, addr: String, hasAdvanced: Bool = true) throws -> (version: UInt8, program: Data) { + let dec = try Bech32.shared.decode(addr) + guard dec.hrp == hrp else { + throw CoderError.hrpMismatch(dec.hrp, hrp) + } + guard dec.checksum.count >= 1 else { + throw CoderError.checksumSizeTooLow + } + let idata = (hasAdvanced ? dec.checksum.advanced(by: 1) : dec.checksum) + let conv = try convertBits(from: 5, to: 8, pad: false, idata: idata) + guard conv.count >= 2 && conv.count <= 40 else { + throw CoderError.dataSizeMismatch(conv.count) + } + guard dec.checksum[0] <= 16 else { + throw CoderError.segwitVersionNotSupported(dec.checksum[0]) + } + if dec.checksum[0] == 0 && conv.count != 20 && conv.count != 32 { + throw CoderError.segwitV0ProgramSizeMismatch(conv.count) + } + if (dec.checksum[0] == 0 && dec.encoding != .bech32) || (dec.checksum[0] != 0 && dec.encoding != .bech32m) { + throw CoderError.segwitVersionAndEncodingMismatch + } + return (dec.checksum[0], conv) + } + + /// Encode segwit address + public static func encode(hrp: String, version: UInt8, program: Data, encoding: Bech32.Encoding) throws -> String { + var enc = Data([version]) + enc.append(try convertBits(from: 8, to: 5, pad: true, idata: program)) + let result = Bech32.shared.encode(hrp, values: enc, encoding: encoding) + guard let _ = try? decode(hrp: hrp, addr: result) else { + throw CoderError.encodingCheckFailed + } + return result + } + + public init() {} + +} + +extension SegWitBech32 { + public enum CoderError: LocalizedError { + case bitsConversionFailed + case hrpMismatch(String, String) + case checksumSizeTooLow + + case dataSizeMismatch(Int) + case segwitVersionNotSupported(UInt8) + case segwitV0ProgramSizeMismatch(Int) + case segwitVersionAndEncodingMismatch + + case encodingCheckFailed + + public var errorDescription: String? { + switch self { + case .bitsConversionFailed: + return "Failed to perform bits conversion" + case .checksumSizeTooLow: + return "Checksum size is too low" + case .dataSizeMismatch(let size): + return "Program size \(size) does not meet required range 2...40" + case .encodingCheckFailed: + return "Failed to check result after encoding" + case .hrpMismatch(let got, let expected): + return "Human-readable-part \"\(got)\" does not match requested \"\(expected)\"" + case .segwitV0ProgramSizeMismatch(let size): + return "Segwit program size \(size) does not meet version 0 requirements" + case .segwitVersionNotSupported(let version): + return "Segwit version \(version) is not supported by this decoder" + case .segwitVersionAndEncodingMismatch: + return "Wrong encoding is used for the Segwit version being used" + } + } + } +} diff --git a/BitcoinKit/Sources/BitcoinKit/Messages/Transaction.swift b/BitcoinKit/Sources/BitcoinKit/Messages/Transaction.swift index 2fc720c09..0658e43d7 100644 --- a/BitcoinKit/Sources/BitcoinKit/Messages/Transaction.swift +++ b/BitcoinKit/Sources/BitcoinKit/Messages/Transaction.swift @@ -74,21 +74,15 @@ public struct Transaction { return data } - public mutating func unpack(with network: Network) { + public mutating func unpack(with network: Network, addressConverter: AddressConverter) { for index in 0.. Bool { - let hash = address.base58 - return inputs.filter { input -> Bool in input.address == hash }.count > 0 - || outputs.filter { output -> Bool in output.address == hash }.count > 0 - } public func isCoinbase() -> Bool { return inputs.count == 1 && inputs[0].isCoinbase() @@ -169,7 +163,7 @@ public struct Transaction { for (i, utxo) in unsignedTx.utxos.enumerated() { let pubkeyHash: Data = Script.getPublicKeyHash(from: utxo.output.lockingScript) - let keysOfUtxo: [PrivateKey] = keys.filter { $0.publicKey().pubkeyHash == pubkeyHash } + let keysOfUtxo: [PrivateKey] = keys.filter { $0.publicKey().hashP2pkh == pubkeyHash } guard let key = keysOfUtxo.first else { print("No keys to this txout : \(utxo.output.value)") continue diff --git a/BitcoinKit/Sources/BitcoinKit/Messages/TransactionInput.swift b/BitcoinKit/Sources/BitcoinKit/Messages/TransactionInput.swift index bdb695c8a..c9cfa6e92 100644 --- a/BitcoinKit/Sources/BitcoinKit/Messages/TransactionInput.swift +++ b/BitcoinKit/Sources/BitcoinKit/Messages/TransactionInput.swift @@ -59,8 +59,8 @@ public struct TransactionInput { return data } - public mutating func unpack(with network: Network) { - address = AddressConverter.extract(from: self.signatureScript, with: network)?.base58 + public mutating func unpack(with network: Network, addressConverter: AddressConverter) { + address = addressConverter.extract(from: signatureScript, with: network)?.stringValue } static func deserialize(_ byteStream: ByteStream) -> TransactionInput { diff --git a/BitcoinKit/Sources/BitcoinKit/Mock/MockHelper.swift b/BitcoinKit/Sources/BitcoinKit/Mock/MockHelper.swift index 541be2bd1..4180aadfe 100644 --- a/BitcoinKit/Sources/BitcoinKit/Mock/MockHelper.swift +++ b/BitcoinKit/Sources/BitcoinKit/Mock/MockHelper.swift @@ -1,126 +1,126 @@ +//// +//// MockHelper.swift +//// +//// Copyright © 2018 BitcoinKit developers +//// +//// Permission is hereby granted, free of charge, to any person obtaining a copy +//// of this software and associated documentation files (the "Software"), to deal +//// in the Software without restriction, including without limitation the rights +//// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +//// copies of the Software, and to permit persons to whom the Software is +//// furnished to do so, subject to the following conditions: +//// +//// The above copyright notice and this permission notice shall be included in +//// all copies or substantial portions of the Software. +//// +//// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +//// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +//// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +//// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +//// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +//// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +//// THE SOFTWARE. +//// // -// MockHelper.swift -// -// Copyright © 2018 BitcoinKit developers -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -// - -import Foundation - -public struct MockHelper { - - public static func createUtxo(lockScript: Script) -> UnspentTransaction { - let outputMock = TransactionOutput(value: 100_000_000, lockingScript: lockScript.data) - let outpointMock = TransactionOutPoint(hash: Data(), index: 0) - return UnspentTransaction(output: outputMock, outpoint: outpointMock) - } - - public static func createTransaction(utxo: UnspentTransaction) -> Transaction { - let toAddress: Address = try! AddressFactory.create("1Bp9U1ogV3A14FMvKbRJms7ctyso4Z4Tcx") - let changeAddress: Address = try! AddressFactory.create("1FQc5LdgGHMHEN9nwkjmz6tWkxhPpxBvBU") - // 1. inputs - let unsignedInputs = [TransactionInput(previousOutput: utxo.outpoint, - signatureScript: Data(), - sequence: UInt32.max)] - - // 2. outputs - // 2-1. amount, change, fee - let amount: UInt64 = 10_000 - let fee: UInt64 = 1000 - let change: UInt64 = utxo.output.value - amount - fee - - // 2-2. Script - let lockingScriptTo = Script(address: toAddress)! - let lockingScriptChange = Script(address: changeAddress)! - - // 2-3. TransactionOutput - let toOutput = TransactionOutput(value: amount, lockingScript: lockingScriptTo.data) - let changeOutput = TransactionOutput(value: change, lockingScript: lockingScriptChange.data) - - // 3. Tx - let tx = Transaction(version: 1, inputs: unsignedInputs, outputs: [toOutput, changeOutput], lockTime: 0) - return tx - } - - public static func updateTransaction(_ tx: Transaction, unlockScriptData: Data) -> Transaction { - let i = 0 - var inputs = tx.inputs - - // Sequence may need to be updated - let txin = inputs[i] - inputs[i] = TransactionInput(previousOutput: txin.previousOutput, - signatureScript: unlockScriptData, - sequence: txin.sequence) - - return Transaction(version: tx.version, - inputs: inputs, - outputs: tx.outputs, - lockTime: tx.lockTime) - } - - public static func verifySingleKey(lockScript: Script, unlockScriptBuilder: MockUnlockScriptBuilder, key: MockKey, verbose: Bool = true) throws -> Bool { - // mocks - let utxoMock: UnspentTransaction = MockHelper.createUtxo(lockScript: lockScript) - let txMock: Transaction = MockHelper.createTransaction(utxo: utxoMock) - - // signature, unlockScript(scriptSig) - let hashType = SighashType.BCH.ALL - let signature: Data = key.privkey.sign(txMock, utxoToSign: utxoMock, hashType: hashType) - let sigWithHashType: Data = signature + UInt8(hashType) - let pair: SigKeyPair = SigKeyPair(sigWithHashType, key.pubkey) - let unlockScript: Script = unlockScriptBuilder.build(pairs: [pair]) - // signed tx - let signedTxMock = MockHelper.updateTransaction(txMock, unlockScriptData: unlockScript.data) - - // context - let context = ScriptExecutionContext(transaction: signedTxMock, utxoToVerify: utxoMock.output, inputIndex: 0)! - context.verbose = verbose - - // script test - return try ScriptMachine.verify(lockScript: lockScript, unlockScript: unlockScript, context: context) - } - - public static func verifyMultiKey(lockScript: Script, unlockScriptBuilder: MockUnlockScriptBuilder, keys: [MockKey], verbose: Bool = true) throws -> Bool { - // mocks - let utxoMock: UnspentTransaction = MockHelper.createUtxo(lockScript: lockScript) - let txMock: Transaction = MockHelper.createTransaction(utxo: utxoMock) - - // signature, unlockScript(scriptSig) - let hashType = SighashType.BCH.ALL - var sigKeyPairs: [SigKeyPair] = [] - for key in keys { - let signature: Data = key.privkey.sign(txMock, utxoToSign: utxoMock, hashType: hashType) - let sigWithHashType: Data = signature + UInt8(hashType) - sigKeyPairs.append(SigKeyPair(sigWithHashType, key.pubkey)) - } - - let unlockScript: Script = unlockScriptBuilder.build(pairs: sigKeyPairs) - // signed tx - let signedTxMock = MockHelper.updateTransaction(txMock, unlockScriptData: unlockScript.data) - - // context - let context = ScriptExecutionContext(transaction: signedTxMock, utxoToVerify: utxoMock.output, inputIndex: 0)! - context.verbose = verbose - - // script test - return try ScriptMachine.verify(lockScript: lockScript, unlockScript: unlockScript, context: context) - } - -} +//import Foundation +// +//public struct MockHelper { +// +// public static func createUtxo(lockScript: Script) -> UnspentTransaction { +// let outputMock = TransactionOutput(value: 100_000_000, lockingScript: lockScript.data) +// let outpointMock = TransactionOutPoint(hash: Data(), index: 0) +// return UnspentTransaction(output: outputMock, outpoint: outpointMock) +// } +// +// public static func createTransaction(utxo: UnspentTransaction) -> Transaction { +// let toAddress: Address = try! AddressFactory.create("1Bp9U1ogV3A14FMvKbRJms7ctyso4Z4Tcx") +// let changeAddress: Address = try! AddressFactory.create("1FQc5LdgGHMHEN9nwkjmz6tWkxhPpxBvBU") +// // 1. inputs +// let unsignedInputs = [TransactionInput(previousOutput: utxo.outpoint, +// signatureScript: Data(), +// sequence: UInt32.max)] +// +// // 2. outputs +// // 2-1. amount, change, fee +// let amount: UInt64 = 10_000 +// let fee: UInt64 = 1000 +// let change: UInt64 = utxo.output.value - amount - fee +// +// // 2-2. Script +// let lockingScriptTo = Script(address: toAddress)! +// let lockingScriptChange = Script(address: changeAddress)! +// +// // 2-3. TransactionOutput +// let toOutput = TransactionOutput(value: amount, lockingScript: lockingScriptTo.data) +// let changeOutput = TransactionOutput(value: change, lockingScript: lockingScriptChange.data) +// +// // 3. Tx +// let tx = Transaction(version: 1, inputs: unsignedInputs, outputs: [toOutput, changeOutput], lockTime: 0) +// return tx +// } +// +// public static func updateTransaction(_ tx: Transaction, unlockScriptData: Data) -> Transaction { +// let i = 0 +// var inputs = tx.inputs +// +// // Sequence may need to be updated +// let txin = inputs[i] +// inputs[i] = TransactionInput(previousOutput: txin.previousOutput, +// signatureScript: unlockScriptData, +// sequence: txin.sequence) +// +// return Transaction(version: tx.version, +// inputs: inputs, +// outputs: tx.outputs, +// lockTime: tx.lockTime) +// } +// +// public static func verifySingleKey(lockScript: Script, unlockScriptBuilder: MockUnlockScriptBuilder, key: MockKey, verbose: Bool = true) throws -> Bool { +// // mocks +// let utxoMock: UnspentTransaction = MockHelper.createUtxo(lockScript: lockScript) +// let txMock: Transaction = MockHelper.createTransaction(utxo: utxoMock) +// +// // signature, unlockScript(scriptSig) +// let hashType = SighashType.BCH.ALL +// let signature: Data = key.privkey.sign(txMock, utxoToSign: utxoMock, hashType: hashType) +// let sigWithHashType: Data = signature + UInt8(hashType) +// let pair: SigKeyPair = SigKeyPair(sigWithHashType, key.pubkey) +// let unlockScript: Script = unlockScriptBuilder.build(pairs: [pair]) +// // signed tx +// let signedTxMock = MockHelper.updateTransaction(txMock, unlockScriptData: unlockScript.data) +// +// // context +// let context = ScriptExecutionContext(transaction: signedTxMock, utxoToVerify: utxoMock.output, inputIndex: 0)! +// context.verbose = verbose +// +// // script test +// return try ScriptMachine.verify(lockScript: lockScript, unlockScript: unlockScript, context: context) +// } +// +// public static func verifyMultiKey(lockScript: Script, unlockScriptBuilder: MockUnlockScriptBuilder, keys: [MockKey], verbose: Bool = true) throws -> Bool { +// // mocks +// let utxoMock: UnspentTransaction = MockHelper.createUtxo(lockScript: lockScript) +// let txMock: Transaction = MockHelper.createTransaction(utxo: utxoMock) +// +// // signature, unlockScript(scriptSig) +// let hashType = SighashType.BCH.ALL +// var sigKeyPairs: [SigKeyPair] = [] +// for key in keys { +// let signature: Data = key.privkey.sign(txMock, utxoToSign: utxoMock, hashType: hashType) +// let sigWithHashType: Data = signature + UInt8(hashType) +// sigKeyPairs.append(SigKeyPair(sigWithHashType, key.pubkey)) +// } +// +// let unlockScript: Script = unlockScriptBuilder.build(pairs: sigKeyPairs) +// // signed tx +// let signedTxMock = MockHelper.updateTransaction(txMock, unlockScriptData: unlockScript.data) +// +// // context +// let context = ScriptExecutionContext(transaction: signedTxMock, utxoToVerify: utxoMock.output, inputIndex: 0)! +// context.verbose = verbose +// +// // script test +// return try ScriptMachine.verify(lockScript: lockScript, unlockScript: unlockScript, context: context) +// } +// +//} diff --git a/BitcoinKit/Sources/BitcoinKit/Mock/MockKey.swift b/BitcoinKit/Sources/BitcoinKit/Mock/MockKey.swift index 585abc736..4f72f15cc 100644 --- a/BitcoinKit/Sources/BitcoinKit/Mock/MockKey.swift +++ b/BitcoinKit/Sources/BitcoinKit/Mock/MockKey.swift @@ -38,7 +38,7 @@ public class MockKey { return privkey.publicKey() } public var pubkeyHash: Data { - return pubkey.pubkeyHash + return pubkey.hashP2pkh } private init(wif: String) { diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/Opcode.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/Opcode.swift index 3026a47aa..336ab2d0d 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/Opcode.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/Opcode.swift @@ -28,6 +28,14 @@ public enum OpCode: OpCodeProtocol { // swiftlint:disable:next line_length case OP_0, OP_FALSE, OP_PUSHDATA1, OP_PUSHDATA2, OP_PUSHDATA4, OP_1NEGATE, OP_RESERVED, OP_1, OP_TRUE, OP_2, OP_3, OP_4, OP_5, OP_6, OP_7, OP_8, OP_9, OP_10, OP_11, OP_12, OP_13, OP_14, OP_15, OP_16, OP_NOP, OP_VER, OP_IF, OP_NOTIF, OP_VERIF, OP_VERNOTIF, OP_ELSE, OP_ENDIF, OP_VERIFY, OP_RETURN, OP_TOALTSTACK, OP_FROMALTSTACK, OP_2DROP, OP_2DUP, OP_3DUP, OP_2OVER, OP_2ROT, OP_2SWAP, OP_IFDUP, OP_DEPTH, OP_DROP, OP_DUP, OP_NIP, OP_OVER, OP_PICK, OP_ROLL, OP_ROT, OP_SWAP, OP_TUCK, OP_CAT, OP_SIZE, OP_SPLIT, OP_NUM2BIN, OP_BIN2NUM, OP_INVERT, OP_AND, OP_OR, OP_XOR, OP_EQUAL, OP_EQUALVERIFY, OP_RESERVED1, OP_RESERVED2, OP_1ADD, OP_1SUB, OP_2MUL, OP_2DIV, OP_NEGATE, OP_ABS, OP_NOT, OP_0NOTEQUAL, OP_ADD, OP_SUB, OP_MUL, OP_DIV, OP_MOD, OP_LSHIFT, OP_RSHIFT, OP_BOOLAND, OP_BOOLOR, OP_NUMEQUAL, OP_NUMEQUALVERIFY, OP_NUMNOTEQUAL, OP_LESSTHAN, OP_GREATERTHAN, OP_LESSTHANOREQUAL, OP_GREATERTHANOREQUAL, OP_MIN, OP_MAX, OP_WITHIN, OP_RIPEMD160, OP_SHA1, OP_SHA256, OP_HASH160, OP_HASH256, OP_CODESEPARATOR, OP_CHECKSIG, OP_CHECKSIGVERIFY, OP_CHECKMULTISIG, OP_CHECKMULTISIGVERIFY, OP_CHECKLOCKTIMEVERIFY, OP_CHECKSEQUENCEVERIFY, OP_PUBKEYHASH, OP_PUBKEY, OP_INVALIDOPCODE, OP_NOP1, OP_NOP4, OP_NOP5, OP_NOP6, OP_NOP7, OP_NOP8, OP_NOP9, OP_NOP10 + static let p2pkhStart = Data(from: [OpCode.OP_DUP, OpCode.OP_HASH160]) + static let p2pkhFinish = Data([OpCode.OP_EQUALVERIFY.value, OpCode.checkSig]) + static let p2pkFinish = Data([OpCode.checkSig]) + static let p2shStart = Data(from: [OpCode.OP_HASH160]) + static let p2shFinish = Data(from: [OpCode.OP_EQUAL]) + static let pushData1: UInt8 = 0x4c + static let pushData2: UInt8 = 0x4d + static let pushData4: UInt8 = 0x4e static let pFromShCodes = [checkSig, checkSigVerify, checkMultiSig, checkMultiSigVerify] static let checkSig: UInt8 = 0xAC static let checkSigVerify: UInt8 = 0xAD @@ -192,6 +200,35 @@ public enum OpCode: OpCodeProtocol { public func mainProcess(_ context: ScriptExecutionContext) throws { try opcode.mainProcess(context) } + + public static func push(_ value: Int) -> Data { + guard value != 0 else { + return Data([0]) + } + guard value <= 16 else { + return Data() + } + return Data([UInt8(value + 0x50)]) + } + + public static func push(_ data: Data) -> Data { + let length = data.count + var bytes = Data() + + switch length { + case 0x00...0x4b: bytes = Data([UInt8(length)]) + case 0x4c...0xff: bytes = Data([OpCode.pushData1]) + UInt8(length).littleEndian + case 0x0100...0xffff: bytes = Data([OpCode.pushData2]) + UInt16(length).littleEndian + case 0x10000...0xffffffff: bytes = Data([OpCode.pushData4]) + UInt32(length).littleEndian + default: return data + } + + return bytes + data + } + + public static func segWitOutputScript(_ data: Data, versionByte: Int = 0) -> Data { + return OpCode.push(versionByte) + OpCode.push(data) + } } //public struct OpCode { diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/Script.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/Script.swift index 9c885aa9f..25e0ca407 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/Script.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/Script.swift @@ -54,18 +54,6 @@ public class Script { return data.hex } - public func toP2SH() -> Script { - return try! Script() - .append(.OP_HASH160) - .appendData(Crypto.sha256ripemd160(data)) - .append(.OP_EQUAL) - } - - public func standardP2SHAddress(network: Network) -> Address { - let scriptHash: Data = Crypto.sha256ripemd160(data) - return Cashaddr(data: scriptHash, type: .scriptHash, network: network) - } - // Multisignature script attribute. // If multisig script is not detected, this is nil public typealias MultisigVariables = (nSigRequired: UInt, publickeys: [PublicKey]) @@ -98,34 +86,6 @@ public class Script { self.init(data: scriptData) } - public convenience init?(address: Address) { - self.init() - switch address.type { - case .pubkeyHash: - // OP_DUP OP_HASH160 OP_EQUALVERIFY OP_CHECKSIG - do { - try self.append(.OP_DUP) - .append(.OP_HASH160) - .appendData(address.data) - .append(.OP_EQUALVERIFY) - .append(.OP_CHECKSIG) - } catch { - return nil - } - case .scriptHash: - // OP_HASH160 OP_EQUAL - do { - try self.append(.OP_HASH160) - .appendData(address.data) - .append(.OP_EQUAL) - } catch { - return nil - } - default: - return nil - } - } - // OP_ ... OP_ OP_CHECKMULTISIG public convenience init?(publicKeys: [PublicKey], signaturesRequired: UInt) { // First make sure the arguments make sense. @@ -306,21 +266,6 @@ public class Script { return chunks } - public func standardAddress(network: Network) -> Address? { - if isPayToPublicKeyHashScript { - guard let dataChunk = chunk(at: 2) as? DataChunk else { - return nil - } - return Cashaddr(data: dataChunk.pushedData, type: .pubkeyHash, network: network) - } else if isPayToScriptHashScript { - guard let dataChunk = chunk(at: 1) as? DataChunk else { - return nil - } - return Cashaddr(data: dataChunk.pushedData, type: .scriptHash, network: network) - } - return nil - } - // MARK: - Modification public func invalidateSerialization() { dataCache = nil diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/ScriptFactory.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/ScriptFactory.swift index f765fc3f2..cf9eab7ac 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/ScriptFactory.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/ScriptFactory.swift @@ -44,14 +44,6 @@ public extension ScriptFactory.Standard { .append(.OP_CHECKSIG) } - static func buildP2PKH(address: Address) -> Script? { - return Script(address: address) - } - - static func buildP2SH(script: Script) -> Script { - return script.toP2SH() - } - static func buildMultiSig(publicKeys: [PublicKey]) -> Script? { return Script(publicKeys: publicKeys, signaturesRequired: UInt(publicKeys.count)) } @@ -75,22 +67,6 @@ public extension ScriptFactory.LockTime { let lockDate = Date(timeIntervalSinceNow: lockIntervalSinceNow) return build(script: script, lockDate: lockDate) } - - // P2PKH + LockTime - static func build(address: Address, lockIntervalSinceNow: TimeInterval) -> Script? { - guard let p2pkh = Script(address: address) else { - return nil - } - let lockDate = Date(timeIntervalSinceNow: lockIntervalSinceNow) - return build(script: p2pkh, lockDate: lockDate) - } - - static func build(address: Address, lockDate: Date) -> Script? { - guard let p2pkh = Script(address: address) else { - return nil - } - return build(script: p2pkh, lockDate: lockDate) - } } // MARK: - OpReturn @@ -172,14 +148,14 @@ public extension ScriptFactory.HashedTimeLockedContract { .append(.OP_EQUALVERIFY) .append(.OP_DUP) .append(.OP_HASH160) - .appendData(recipient.data) + .appendData(recipient.lockingScriptPayload) .append(.OP_ELSE) .appendData(lockDate.bigNumData) .append(.OP_CHECKLOCKTIMEVERIFY) .append(.OP_DROP) .append(.OP_DUP) .append(.OP_HASH160) - .appendData(sender.data) + .appendData(sender.lockingScriptPayload) .append(.OP_ENDIF) .append(.OP_EQUALVERIFY) .append(.OP_CHECKSIG) diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/ScriptType.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/ScriptType.swift index 8483d1111..f93e3a850 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/ScriptType.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/ScriptType.swift @@ -8,7 +8,7 @@ import Foundation public enum ScriptType: Int { - case unknown, p2pkh, p2pk, p2multi, p2sh, p2wsh, p2wpkh, p2wpkhSh + case unknown, p2pkh, p2pk, p2multi, p2sh, p2wsh, p2wpkh, p2wpkhSh, p2tr var size: Int { switch self { @@ -18,31 +18,12 @@ public enum ScriptType: Int { case .p2wsh: return 34 case .p2wpkh: return 22 case .p2wpkhSh: return 23 - default: return 0 - } - } - - var keyLength: UInt8 { - switch self { - case .p2pk: return 0x21 - case .p2pkh: return 0x14 - case .p2sh: return 0x14 - case .p2wsh: return 0x20 - case .p2wpkh: return 0x14 - case .p2wpkhSh: return 0x14 - default: return 0 - } - } - - var addressType: AddressType { - switch self { - case .p2sh, .p2wsh: return .scriptHash - default: return .pubkeyHash + case .p2tr: return 34 + case .unknown, .p2multi: return .zero } } var witness: Bool { - return self == .p2wpkh || self == .p2wpkhSh || self == .p2wsh + self == .p2wpkh || self == .p2wpkhSh || self == .p2wsh || self == .p2tr } - } diff --git a/BitcoinKit/Sources/BitcoinKit/Wallet/HDWallet.swift b/BitcoinKit/Sources/BitcoinKit/Wallet/HDWallet.swift index 1492a12a4..aa1acf837 100644 --- a/BitcoinKit/Sources/BitcoinKit/Wallet/HDWallet.swift +++ b/BitcoinKit/Sources/BitcoinKit/Wallet/HDWallet.swift @@ -42,6 +42,7 @@ public final class HDWallet { private let seed: Data private let keychain: HDKeychain + private let addressConverter: AddressConverter private let purpose: UInt32 private let coinType: UInt32 @@ -85,6 +86,7 @@ public final class HDWallet { // Public derivation is used at this level. externalIndex = 0 internalIndex = 0 + addressConverter = AddressConverterFactory().make(network: network) } // MARK: - External Addresses & Keys (Receive Addresses & Keys) @@ -94,7 +96,7 @@ public final class HDWallet { public func receiveAddress(index: UInt32) throws -> Address { let key = try publicKey(index: index) - return key.toCashaddr() + return try addressConverter.convert(publicKey: key, type: .p2pkh) } public func publicKey(index: UInt32) throws -> PublicKey { @@ -121,7 +123,7 @@ public final class HDWallet { public func changeAddress(index: UInt32) throws -> Address { let key = try changePublicKey(index: index) - return key.toCashaddr() + return try addressConverter.convert(publicKey: key, type: .p2pkh) } public func changePublicKey(index: UInt32) throws -> PublicKey {