Skip to content

Commit 19b34b1

Browse files
committed
Output descriptor: parsing and address derivation
1 parent 40f150c commit 19b34b1

File tree

5 files changed

+179
-1
lines changed

5 files changed

+179
-1
lines changed

CLibWally/module.modulemap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ module CLibWally {
55
header "libwally-core/include/wally_bip32.h"
66
header "libwally-core/include/wally_bip38.h"
77
header "libwally-core/include/wally_bip39.h"
8+
header "libwally-core/include/wally_descriptor.h"
89
header "libwally-core/include/wally_psbt.h"
910
header "libwally-core/include/wally_script.h"
1011
header "libwally-core/include/wally_transaction.h"

LibWally.xcodeproj/project.pbxproj

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
/* Begin PBXBuildFile section */
1010
A20C942522C6BC3900B0D206 /* libwallycore.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A20C942422C6BC3900B0D206 /* libwallycore.a */; };
11+
A21016BD279EE9D00002330E /* Descriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A21016BC279EE9D00002330E /* Descriptor.swift */; };
12+
A21016BF279EEFBD0002330E /* DescriptorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A21016BE279EEFBD0002330E /* DescriptorTests.swift */; };
1113
A21016C1279F03E20002330E /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = A21016C0279F03E20002330E /* README.md */; };
1214
A232260122B94A6B00C3B79C /* Transaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = A232260022B94A6B00C3B79C /* Transaction.swift */; };
1315
A232260322B94A8400C3B79C /* TransactionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A232260222B94A8400C3B79C /* TransactionTests.swift */; };
@@ -41,6 +43,8 @@
4143
A20557A522C6CDBE007221AA /* LibWally.modulemap */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = "sourcecode.module-map"; path = LibWally.modulemap; sourceTree = "<group>"; };
4244
A20C942422C6BC3900B0D206 /* libwallycore.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libwallycore.a; path = "CLibWally/libwally-core/src/.libs/libwallycore.a"; sourceTree = "<group>"; };
4345
A20C942622C6BDB000B0D206 /* CLibWally */ = {isa = PBXFileReference; lastKnownFileType = folder; path = CLibWally; sourceTree = "<group>"; };
46+
A21016BC279EE9D00002330E /* Descriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Descriptor.swift; sourceTree = "<group>"; };
47+
A21016BE279EEFBD0002330E /* DescriptorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DescriptorTests.swift; sourceTree = "<group>"; };
4448
A21016C0279F03E20002330E /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = SOURCE_ROOT; };
4549
A232260022B94A6B00C3B79C /* Transaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Transaction.swift; sourceTree = "<group>"; };
4650
A232260222B94A8400C3B79C /* TransactionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionTests.swift; sourceTree = "<group>"; };
@@ -116,6 +120,7 @@
116120
FE1A3C0522B395B300EDCB58 /* Address.swift */,
117121
FEC79CE3229E7F3800D86E2E /* BIP32.swift */,
118122
FE120F54229C3B6900E7720C /* BIP39.swift */,
123+
A21016BC279EE9D00002330E /* Descriptor.swift */,
119124
A2BCE19323A7D6B200737BEB /* PSBT.swift */,
120125
FE8B80A322B3E5630041CC94 /* Script.swift */,
121126
A232260022B94A6B00C3B79C /* Transaction.swift */,
@@ -131,6 +136,7 @@
131136
FEC79CE5229E807500D86E2E /* BIP32Tests.swift */,
132137
FE9CD3BF229C397900345DFA /* BIP39Tests.swift */,
133138
FE8B80A122B397090041CC94 /* AddressTests.swift */,
139+
A21016BE279EEFBD0002330E /* DescriptorTests.swift */,
134140
A2BCE19123A7D28500737BEB /* PSBTTests.swift */,
135141
FE8B80A522B3E5760041CC94 /* ScriptTests.swift */,
136142
A232260222B94A8400C3B79C /* TransactionTests.swift */,
@@ -239,14 +245,14 @@
239245
isa = PBXResourcesBuildPhase;
240246
buildActionMask = 2147483647;
241247
files = (
248+
A21016C1279F03E20002330E /* README.md in Resources */,
242249
);
243250
runOnlyForDeploymentPostprocessing = 0;
244251
};
245252
FE9CD3B8229C397900345DFA /* Resources */ = {
246253
isa = PBXResourcesBuildPhase;
247254
buildActionMask = 2147483647;
248255
files = (
249-
A21016C1279F03E20002330E /* README.md in Resources */,
250256
);
251257
runOnlyForDeploymentPostprocessing = 0;
252258
};
@@ -259,6 +265,7 @@
259265
files = (
260266
FE120F55229C3B6900E7720C /* BIP39.swift in Sources */,
261267
A2BCE19423A7D6B200737BEB /* PSBT.swift in Sources */,
268+
A21016BD279EE9D00002330E /* Descriptor.swift in Sources */,
262269
FE39CDFA229DAAF400DD135E /* DataExtension.swift in Sources */,
263270
FE1A3C0622B395B300EDCB58 /* Address.swift in Sources */,
264271
FE8B80A422B3E5630041CC94 /* Script.swift in Sources */,
@@ -273,6 +280,7 @@
273280
files = (
274281
FE8B80A222B397090041CC94 /* AddressTests.swift in Sources */,
275282
A23509D72398F33E0045D3A5 /* DataExtensionTests.swift in Sources */,
283+
A21016BF279EEFBD0002330E /* DescriptorTests.swift in Sources */,
276284
FE8B80A622B3E5760041CC94 /* ScriptTests.swift in Sources */,
277285
FEC79CE6229E807500D86E2E /* BIP32Tests.swift in Sources */,
278286
A2BCE19223A7D28500737BEB /* PSBTTests.swift in Sources */,

LibWally/Descriptor.swift

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
//
2+
// Descriptor.swift
3+
// Descriptor
4+
//
5+
// Created by Sjors Provoost on 24/01/2022.
6+
// Copyright © 2022 Sjors Provoost. Distributed under the MIT software
7+
// license, see the accompanying file LICENSE.md
8+
9+
import Foundation
10+
import CLibWally
11+
12+
public enum DescriptorError: Error {
13+
case invalid
14+
case noAddress // There is no address representation, e.g. pk()
15+
case missingChecksum
16+
case notRanged // No index should be used for getAddress() when called on a non-ranged descriptor
17+
case ranged // Index must be used for getAddress() when called on a ranged descriptor
18+
}
19+
20+
21+
public struct Descriptor {
22+
// The descriptor string we were initialized with. Not normalized and not fully validated.
23+
var raw_descriptor: String
24+
public var network: Network
25+
public var canonical: String
26+
27+
// The descriptor is not fully validated.
28+
public init(_ descriptor: String, _ network: Network) throws {
29+
self.raw_descriptor = descriptor
30+
self.network = network
31+
32+
// Insist on a checksum (we assume any inappropriate use of # is caught in wally_descriptor_canonicalize)
33+
if descriptor.firstIndex(of: "#") == nil {
34+
throw DescriptorError.missingChecksum
35+
}
36+
37+
// Canonicalize the descriptor, which also partially validates the input.
38+
var output: UnsafeMutablePointer<Int8>?
39+
defer {
40+
wally_free_string(output)
41+
}
42+
if (wally_descriptor_canonicalize(descriptor, nil, 0, &output) != WALLY_OK) {
43+
throw DescriptorError.invalid
44+
} else {
45+
precondition(output != nil)
46+
self.canonical = String(cString: output!)
47+
}
48+
}
49+
50+
// May throw if something is wrong with the descriptor.
51+
// Will throw if descriptor can't be expressed as an address, e.g. pk().
52+
func getAddress(_ index: UInt32) throws -> Address {
53+
if index != 0 && self.raw_descriptor.firstIndex(of: "*") == nil {
54+
throw DescriptorError.notRanged
55+
}
56+
57+
var output: UnsafeMutablePointer<Int8>?
58+
defer {
59+
wally_free_string(output)
60+
}
61+
62+
let result = wally_descriptor_to_address(self.raw_descriptor, nil, index, UInt32(network == .mainnet ? WALLY_NETWORK_BITCOIN_MAINNET : WALLY_NETWORK_BITCOIN_TESTNET), 0, &output)
63+
64+
if result != WALLY_OK {
65+
throw DescriptorError.invalid
66+
}
67+
68+
precondition(output != nil)
69+
if let address = Address(String(cString: output!)) {
70+
return address
71+
} else {
72+
// This code is not reached for pk() descriptors, because wally_descriptor_to_address will fail
73+
// TODO: catch descriptors that can't be expressed as an address earlier and explictly
74+
throw DescriptorError.noAddress
75+
}
76+
}
77+
78+
public func getAddress() throws -> Address {
79+
if self.raw_descriptor.firstIndex(of: "*") != nil {
80+
throw DescriptorError.ranged
81+
}
82+
return try getAddress(0)
83+
}
84+
85+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
//
2+
// DescriptorTests.swift
3+
// DescriptorTests
4+
//
5+
// Created by Sjors Provoost on 24/01/2022.
6+
// Copyright © 2022 Sjors Provoost. Distributed under the MIT software
7+
// license, see the accompanying file LICENSE.md
8+
9+
import XCTest
10+
@testable import LibWally
11+
12+
class DescriptorTests: XCTestCase {
13+
14+
override func setUpWithError() throws {
15+
// Put setup code here. This method is called before the invocation of each test method in the class.
16+
}
17+
18+
override func tearDownWithError() throws {
19+
// Put teardown code here. This method is called after the invocation of each test method in the class.
20+
}
21+
22+
func testChecksum() throws {
23+
XCTAssertThrowsError(try Descriptor("pkh([3442193e/0']xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw/1)", .mainnet)) { error in
24+
XCTAssertEqual(error as! DescriptorError, DescriptorError.missingChecksum)
25+
}
26+
27+
XCTAssertNoThrow(try Descriptor("pkh([3442193e/0']xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw/1)#62cpuxwx", .mainnet))
28+
29+
XCTAssertThrowsError(try Descriptor("pkh([3442193e/0']xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw/1)#00000000", .mainnet)) { error in
30+
XCTAssertEqual(error as! DescriptorError, DescriptorError.invalid)
31+
}
32+
}
33+
34+
func testAddress() throws {
35+
let desc = try! Descriptor("pkh([3442193e/0']xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw/1)#62cpuxwx", .mainnet)
36+
XCTAssertEqual(try! desc.getAddress().description, "1JQheacLPdM5ySCkrZkV66G2ApAXe1mqLj")
37+
}
38+
39+
func testAddressFromRange() throws {
40+
let desc = try! Descriptor("pkh([3442193e/0']xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw/*)#p44786lk", .mainnet)
41+
XCTAssertEqual(try! desc.getAddress(1).description, "1JQheacLPdM5ySCkrZkV66G2ApAXe1mqLj")
42+
}
43+
44+
func testMatchingNetwork() throws {
45+
// Using .network and xpub/tpub inconsistently is not caught during canonicalization.
46+
// So we use getAddress() instead.
47+
let desc = try! Descriptor("pkh([3442193e/0']xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw/1)#62cpuxwx", .testnet)
48+
49+
XCTAssertThrowsError(try desc.getAddress()) { error in
50+
XCTAssertEqual(error as! DescriptorError, DescriptorError.invalid)
51+
}
52+
}
53+
54+
func testNonAddressDescriptor() throws {
55+
let desc = try! Descriptor("pk([3442193e/0']xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw/1)#397dme97", .mainnet)
56+
57+
XCTAssertThrowsError(try desc.getAddress()) { error in
58+
// TODO: have it throw noAddress
59+
XCTAssertEqual(error as! DescriptorError, DescriptorError.invalid)
60+
}
61+
62+
}
63+
64+
func testIndexForNonRangedDescriptor() throws {
65+
let desc = try! Descriptor("pkh([3442193e/0']xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw/1)#62cpuxwx", .mainnet)
66+
67+
XCTAssertThrowsError(try desc.getAddress(2)) { error in
68+
XCTAssertEqual(error as! DescriptorError, DescriptorError.notRanged)
69+
}
70+
}
71+
72+
func testMissingIndexForRangedDescriptor() throws {
73+
let desc = try! Descriptor("pkh([3442193e/0']xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw/*)#p44786lk", .mainnet)
74+
75+
XCTAssertThrowsError(try desc.getAddress()) { error in
76+
XCTAssertEqual(error as! DescriptorError, DescriptorError.ranged)
77+
}
78+
}
79+
80+
}

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ Supports a minimal set of features based on v0.8.4. See also [original docs](htt
2020
- [ ] Derive scriptPubKey #6 (wishlist)
2121
- [ ] BIP38 functions
2222
- [x] BIP39 functions
23+
- [ ] Descriptor functions
24+
- [x] Parse and canonicalize
25+
- [x] Convert to address
26+
- [ ] Convert to scriptPubKey
2327
- [ ] Script functions
2428
- [x] Serialize scriptPubKey
2529
- [x] Determine scriptPubkey type

0 commit comments

Comments
 (0)