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 Jan 24, 2022
1 parent 40f150c commit 29e4b3e
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 1 deletion.
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_script.h"
header "libwally-core/include/wally_transaction.h"
Expand Down
10 changes: 9 additions & 1 deletion LibWally.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

/* Begin PBXBuildFile section */
A20C942522C6BC3900B0D206 /* libwallycore.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A20C942422C6BC3900B0D206 /* libwallycore.a */; };
A21016BD279EE9D00002330E /* Descriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A21016BC279EE9D00002330E /* Descriptor.swift */; };
A21016BF279EEFBD0002330E /* DescriptorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A21016BE279EEFBD0002330E /* DescriptorTests.swift */; };
A21016C1279F03E20002330E /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = A21016C0279F03E20002330E /* README.md */; };
A232260122B94A6B00C3B79C /* Transaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = A232260022B94A6B00C3B79C /* Transaction.swift */; };
A232260322B94A8400C3B79C /* TransactionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A232260222B94A8400C3B79C /* TransactionTests.swift */; };
Expand Down Expand Up @@ -41,6 +43,8 @@
A20557A522C6CDBE007221AA /* LibWally.modulemap */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = "sourcecode.module-map"; path = LibWally.modulemap; sourceTree = "<group>"; };
A20C942422C6BC3900B0D206 /* libwallycore.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libwallycore.a; path = "CLibWally/libwally-core/src/.libs/libwallycore.a"; sourceTree = "<group>"; };
A20C942622C6BDB000B0D206 /* CLibWally */ = {isa = PBXFileReference; lastKnownFileType = folder; path = CLibWally; 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>"; };
A21016C0279F03E20002330E /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = SOURCE_ROOT; };
A232260022B94A6B00C3B79C /* Transaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Transaction.swift; sourceTree = "<group>"; };
A232260222B94A8400C3B79C /* TransactionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -116,6 +120,7 @@
FE1A3C0522B395B300EDCB58 /* Address.swift */,
FEC79CE3229E7F3800D86E2E /* BIP32.swift */,
FE120F54229C3B6900E7720C /* BIP39.swift */,
A21016BC279EE9D00002330E /* Descriptor.swift */,
A2BCE19323A7D6B200737BEB /* PSBT.swift */,
FE8B80A322B3E5630041CC94 /* Script.swift */,
A232260022B94A6B00C3B79C /* Transaction.swift */,
Expand All @@ -131,6 +136,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 @@ -239,14 +245,14 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
A21016C1279F03E20002330E /* README.md in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
FE9CD3B8229C397900345DFA /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
A21016C1279F03E20002330E /* README.md in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -259,6 +265,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 @@ -273,6 +280,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
85 changes: 85 additions & 0 deletions LibWally/Descriptor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
//
// 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
import CLibWally

public enum DescriptorError: Error {
case invalid
case noAddress // There is no address representation, e.g. pk()
case missingChecksum
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 raw_descriptor: String
public var network: Network
public var canonical: String

// The descriptor is not fully validated.
public init(_ descriptor: String, _ network: Network) throws {
self.raw_descriptor = descriptor
self.network = network

// Insist on a checksum (we assume any inappropriate use of # is caught in wally_descriptor_canonicalize)
if descriptor.firstIndex(of: "#") == nil {
throw DescriptorError.missingChecksum
}

// Canonicalize the descriptor, which also partially validates the input.
var output: UnsafeMutablePointer<Int8>?
defer {
wally_free_string(output)
}
if (wally_descriptor_canonicalize(descriptor, nil, 0, &output) != WALLY_OK) {
throw DescriptorError.invalid
} else {
precondition(output != nil)
self.canonical = String(cString: output!)
}
}

// 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.raw_descriptor.firstIndex(of: "*") == nil {
throw DescriptorError.notRanged
}

var output: UnsafeMutablePointer<Int8>?
defer {
wally_free_string(output)
}

let result = wally_descriptor_to_address(self.raw_descriptor, nil, index, UInt32(network == .mainnet ? WALLY_NETWORK_BITCOIN_MAINNET : WALLY_NETWORK_BITCOIN_TESTNET), 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.raw_descriptor.firstIndex(of: "*") != nil {
throw DescriptorError.ranged
}
return try getAddress(0)
}

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

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 {
// Using .network and xpub/tpub inconsistently is not caught during canonicalization.
// So we use getAddress() instead.
let desc = try! Descriptor("pkh([3442193e/0']xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw/1)#62cpuxwx", .testnet)

XCTAssertThrowsError(try desc.getAddress()) { 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.4. 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 29e4b3e

Please sign in to comment.