-
-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Output descriptor: parsing and address derivation
- Loading branch information
Showing
5 changed files
with
179 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters