Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLibWally/libwally-core
Submodule libwally-core updated 141 files
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)
}
}

}
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down