Skip to content

Commit

Permalink
Output descriptor: parsing and address derivation
Browse files Browse the repository at this point in the history
  • Loading branch information
Sjors committed Feb 10, 2023
1 parent 8af3ba7 commit 087edf0
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 0 deletions.
1 change: 1 addition & 0 deletions CLibWally/module.modulemap
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
9 changes: 9 additions & 0 deletions LibWally.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -71,6 +73,8 @@
FE9CD3C1229C397900345DFA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
FEC79CE3229E7F3800D86E2E /* BIP32.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BIP32.swift; sourceTree = "<group>"; };
FEC79CE5229E807500D86E2E /* BIP32Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BIP32Tests.swift; sourceTree = "<group>"; };
A21016BC279EE9D00002330E /* Descriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Descriptor.swift; sourceTree = "<group>"; };
A21016BE279EEFBD0002330E /* DescriptorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DescriptorTests.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -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 */,
Expand All @@ -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 */,
Expand Down Expand Up @@ -281,6 +287,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
A21016C1279F03E20002330E /* README.md in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -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 */,
Expand All @@ -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 */,
Expand Down
96 changes: 96 additions & 0 deletions LibWally/Descriptor.swift
Original file line number Diff line number Diff line change
@@ -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<UInt32>.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<Int8>?
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<Int8>?
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)
}

}
76 changes: 76 additions & 0 deletions LibWallyTests/DescriptorTests.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}

}
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ Supports a minimal set of features based on v0.8.8. 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
Expand Down

0 comments on commit 087edf0

Please sign in to comment.