diff --git a/CLibWally/libwally-core b/CLibWally/libwally-core index 38b85bf0..23e6b626 160000 --- a/CLibWally/libwally-core +++ b/CLibWally/libwally-core @@ -1 +1 @@ -Subproject commit 38b85bf06194ebe0d1d841e11cd8e1b58aa2a9ca +Subproject commit 23e6b626c8906bce2e3179409b938c9ef9bca463 diff --git a/CLibWally/module.modulemap b/CLibWally/module.modulemap index 2d79b8e2..5c3c1fe6 100644 --- a/CLibWally/module.modulemap +++ b/CLibWally/module.modulemap @@ -5,6 +5,7 @@ module CLibWally { header "libwally-core/include/wally_bip32.h" header "libwally-core/include/wally_bip38.h" header "libwally-core/include/wally_bip39.h" + header "libwally-core/include/wally_descriptor.h" header "libwally-core/include/wally_psbt.h" header "libwally-core/include/wally_psbt_members.h" header "libwally-core/include/wally_script.h" diff --git a/LibWally.xcodeproj/project.pbxproj b/LibWally.xcodeproj/project.pbxproj index b17541e9..4e09ae01 100644 --- a/LibWally.xcodeproj/project.pbxproj +++ b/LibWally.xcodeproj/project.pbxproj @@ -25,6 +25,8 @@ FE9CD3C0229C397900345DFA /* BIP39Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE9CD3BF229C397900345DFA /* BIP39Tests.swift */; }; FEC79CE4229E7F3800D86E2E /* BIP32.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC79CE3229E7F3800D86E2E /* BIP32.swift */; }; FEC79CE6229E807500D86E2E /* BIP32Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC79CE5229E807500D86E2E /* BIP32Tests.swift */; }; + A21016BD279EE9D00002330E /* Descriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A21016BC279EE9D00002330E /* Descriptor.swift */; }; + A21016BF279EEFBD0002330E /* DescriptorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A21016BE279EEFBD0002330E /* DescriptorTests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -71,6 +73,8 @@ FE9CD3C1229C397900345DFA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; FEC79CE3229E7F3800D86E2E /* BIP32.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BIP32.swift; sourceTree = ""; }; FEC79CE5229E807500D86E2E /* BIP32Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BIP32Tests.swift; sourceTree = ""; }; + A21016BC279EE9D00002330E /* Descriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Descriptor.swift; sourceTree = ""; }; + A21016BE279EEFBD0002330E /* DescriptorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DescriptorTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -133,6 +137,7 @@ FE1A3C0522B395B300EDCB58 /* Address.swift */, FEC79CE3229E7F3800D86E2E /* BIP32.swift */, FE120F54229C3B6900E7720C /* BIP39.swift */, + A21016BC279EE9D00002330E /* Descriptor.swift */, A2BCE19323A7D6B200737BEB /* PSBT.swift */, FE8B80A322B3E5630041CC94 /* Script.swift */, A232260022B94A6B00C3B79C /* Transaction.swift */, @@ -148,6 +153,7 @@ FEC79CE5229E807500D86E2E /* BIP32Tests.swift */, FE9CD3BF229C397900345DFA /* BIP39Tests.swift */, FE8B80A122B397090041CC94 /* AddressTests.swift */, + A21016BE279EEFBD0002330E /* DescriptorTests.swift */, A2BCE19123A7D28500737BEB /* PSBTTests.swift */, FE8B80A522B3E5760041CC94 /* ScriptTests.swift */, A232260222B94A8400C3B79C /* TransactionTests.swift */, @@ -281,6 +287,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + A21016C1279F03E20002330E /* README.md in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -300,6 +307,7 @@ files = ( FE120F55229C3B6900E7720C /* BIP39.swift in Sources */, A2BCE19423A7D6B200737BEB /* PSBT.swift in Sources */, + A21016BD279EE9D00002330E /* Descriptor.swift in Sources */, FE39CDFA229DAAF400DD135E /* DataExtension.swift in Sources */, FE1A3C0622B395B300EDCB58 /* Address.swift in Sources */, FE8B80A422B3E5630041CC94 /* Script.swift in Sources */, @@ -314,6 +322,7 @@ files = ( FE8B80A222B397090041CC94 /* AddressTests.swift in Sources */, A23509D72398F33E0045D3A5 /* DataExtensionTests.swift in Sources */, + A21016BF279EEFBD0002330E /* DescriptorTests.swift in Sources */, FE8B80A622B3E5760041CC94 /* ScriptTests.swift in Sources */, FEC79CE6229E807500D86E2E /* BIP32Tests.swift in Sources */, A2BCE19223A7D28500737BEB /* PSBTTests.swift in Sources */, diff --git a/LibWally/Descriptor.swift b/LibWally/Descriptor.swift new file mode 100644 index 00000000..b6957d0c --- /dev/null +++ b/LibWally/Descriptor.swift @@ -0,0 +1,96 @@ +// +// Descriptor.swift +// Descriptor +// +// Created by Sjors Provoost on 24/01/2022. +// Copyright © 2022 Sjors Provoost. Distributed under the MIT software +// license, see the accompanying file LICENSE.md + +import Foundation +@_implementationOnly import CLibWally + +public enum DescriptorError: Error { + case invalid + case noAddress // There is no address representation, e.g. pk() + case notRanged // No index should be used for getAddress() when called on a non-ranged descriptor + case ranged // Index must be used for getAddress() when called on a ranged descriptor +} + + +public struct Descriptor { + // The descriptor string we were initialized with. Not normalized and not fully validated. + var wally_descriptor: OpaquePointer? + public var network: Network + public var canonical: String + public var isRanged: Bool + + // The descriptor is not fully validated. + public init(_ descriptor: String, _ network: Network) throws { + self.network = network + + // Parse descriptor + if (wally_descriptor_parse(descriptor, nil, UInt32(network == .mainnet ? WALLY_NETWORK_BITCOIN_MAINNET : WALLY_NETWORK_BITCOIN_TESTNET), UInt32( WALLY_MINISCRIPT_REQUIRE_CHECKSUM), &wally_descriptor) != WALLY_OK) { + throw DescriptorError.invalid + } + + // Store properties + let feature_flags = UnsafeMutablePointer.allocate(capacity: 1) + precondition(wally_descriptor_get_features(wally_descriptor, feature_flags) == WALLY_OK) + self.isRanged = (feature_flags.pointee & UInt32(WALLY_MS_IS_RANGED)) != 0 + + // Canonicalize the descriptor + var output: UnsafeMutablePointer? + defer { + wally_free_string(output) + } + if (wally_descriptor_canonicalize(wally_descriptor, 0, &output) != WALLY_OK) { + throw DescriptorError.invalid + } else { + precondition(output != nil) + self.canonical = String(cString: output!) + } + } + + /* Deinitializers may only be declared within a class or actor. + / I'm unsure if not freeing up the memory is safe. */ + + // deinit { + // wally_descriptor_free(self.wally_descriptor) + // } + + // May throw if something is wrong with the descriptor. + // Will throw if descriptor can't be expressed as an address, e.g. pk(). + public func getAddress(_ index: UInt32) throws -> Address { + if index != 0 && !self.isRanged { + throw DescriptorError.notRanged + } + + var output: UnsafeMutablePointer? + defer { + wally_free_string(output) + } + + let result = wally_descriptor_to_address(self.wally_descriptor, 0, 0, index, UInt32(0), &output) + + if result != WALLY_OK { + throw DescriptorError.invalid + } + + precondition(output != nil) + if let address = Address(String(cString: output!)) { + return address + } else { + // This code is not reached for pk() descriptors, because wally_descriptor_to_address will fail + // TODO: catch descriptors that can't be expressed as an address earlier and explictly + throw DescriptorError.noAddress + } + } + + public func getAddress() throws -> Address { + if self.isRanged { + throw DescriptorError.ranged + } + return try getAddress(0) + } + +} diff --git a/LibWallyTests/DescriptorTests.swift b/LibWallyTests/DescriptorTests.swift new file mode 100644 index 00000000..d18fba51 --- /dev/null +++ b/LibWallyTests/DescriptorTests.swift @@ -0,0 +1,76 @@ +// +// DescriptorTests.swift +// DescriptorTests +// +// Created by Sjors Provoost on 24/01/2022. +// Copyright © 2022 Sjors Provoost. Distributed under the MIT software +// license, see the accompanying file LICENSE.md + +import XCTest +@testable import LibWally + +class DescriptorTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testChecksum() throws { + XCTAssertThrowsError(try Descriptor("pkh([3442193e/0']xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw/1)", .mainnet)) { error in + XCTAssertEqual(error as! DescriptorError, DescriptorError.invalid) + } + + XCTAssertNoThrow(try Descriptor("pkh([3442193e/0']xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw/1)#62cpuxwx", .mainnet)) + + XCTAssertThrowsError(try Descriptor("pkh([3442193e/0']xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw/1)#00000000", .mainnet)) { error in + XCTAssertEqual(error as! DescriptorError, DescriptorError.invalid) + } + } + + func testAddress() throws { + let desc = try! Descriptor("pkh([3442193e/0']xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw/1)#62cpuxwx", .mainnet) + XCTAssertEqual(try! desc.getAddress().description, "1JQheacLPdM5ySCkrZkV66G2ApAXe1mqLj") + } + + func testAddressFromRange() throws { + let desc = try! Descriptor("pkh([3442193e/0']xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw/*)#p44786lk", .mainnet) + XCTAssertEqual(try! desc.getAddress(1).description, "1JQheacLPdM5ySCkrZkV66G2ApAXe1mqLj") + } + + func testMatchingNetwork() throws { + XCTAssertThrowsError(try Descriptor("pkh([3442193e/0']xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw/1)#62cpuxwx", .testnet)) { error in + XCTAssertEqual(error as! DescriptorError, DescriptorError.invalid) + } + } + + func testNonAddressDescriptor() throws { + let desc = try! Descriptor("pk([3442193e/0']xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw/1)#397dme97", .mainnet) + + XCTAssertThrowsError(try desc.getAddress()) { error in + // TODO: have it throw noAddress + XCTAssertEqual(error as! DescriptorError, DescriptorError.invalid) + } + + } + + func testIndexForNonRangedDescriptor() throws { + let desc = try! Descriptor("pkh([3442193e/0']xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw/1)#62cpuxwx", .mainnet) + + XCTAssertThrowsError(try desc.getAddress(2)) { error in + XCTAssertEqual(error as! DescriptorError, DescriptorError.notRanged) + } + } + + func testMissingIndexForRangedDescriptor() throws { + let desc = try! Descriptor("pkh([3442193e/0']xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw/*)#p44786lk", .mainnet) + + XCTAssertThrowsError(try desc.getAddress()) { error in + XCTAssertEqual(error as! DescriptorError, DescriptorError.ranged) + } + } + +} diff --git a/README.md b/README.md index dfc96650..d5be99ac 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Opinionated Swift wrapper around [LibWally](https://github.com/ElementsProject/libwally-core), a collection of useful primitives for cryptocurrency wallets. -Supports a minimal set of features based on v0.8.7. See also [original docs](https://wally.readthedocs.io/en/release_0.8.7). +Supports a minimal set of features based on v0.8.8. See also [original docs](https://wally.readthedocs.io/en/release_0.8.8). - [ ] Core Functions - [x] base58 encode / decode @@ -20,6 +20,10 @@ Supports a minimal set of features based on v0.8.7. See also [original docs](htt - [ ] Derive scriptPubKey #6 (wishlist) - [ ] BIP38 Functions - [x] BIP39 Functions +- [ ] Descriptor functions + - [x] Parse and canonicalize + - [x] Convert to address + - [ ] Convert to scriptPubKey - [ ] Script Functions - [x] Serialize scriptPubKey - [x] Determine scriptPubkey type