Compare commits
1 Commits
main
...
fix-apploc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a014a2dc2f |
@ -342,7 +342,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = CC0000010000000000000004 /* Birch.xcconfig */;
|
||||
buildSettings = {
|
||||
CURRENT_PROJECT_VERSION = 26;
|
||||
CURRENT_PROJECT_VERSION = 25;
|
||||
MARKETING_VERSION = 0.2.0;
|
||||
};
|
||||
name = Debug;
|
||||
@ -351,7 +351,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = CC0000010000000000000004 /* Birch.xcconfig */;
|
||||
buildSettings = {
|
||||
CURRENT_PROJECT_VERSION = 26;
|
||||
CURRENT_PROJECT_VERSION = 25;
|
||||
MARKETING_VERSION = 0.2.0;
|
||||
};
|
||||
name = Release;
|
||||
|
||||
@ -0,0 +1,87 @@
|
||||
{
|
||||
"originHash" : "11f3c5d73e6615e055e5b9f3671e6180f277a34f298c3f7c6935dcc8dd281089",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "bbqr-swift",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/bitcoinppl/bbqr-swift",
|
||||
"state" : {
|
||||
"revision" : "83b828077ecc4f5d2cf8889da5543a61b4a60a3c",
|
||||
"version" : "0.3.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "bcswiftdcbor",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/BlockchainCommons/BCSwiftDCBOR",
|
||||
"state" : {
|
||||
"revision" : "21efa67ada2f22a6c277e1961f1059bb376e9b1a",
|
||||
"version" : "2.0.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "bcswiftfloat16",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/blockchaincommons/BCSwiftFloat16",
|
||||
"state" : {
|
||||
"revision" : "a27f3935a7b1db715713eda67369b02feade2ded",
|
||||
"version" : "2.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "bcswifttags",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/BlockchainCommons/BCSwiftTags",
|
||||
"state" : {
|
||||
"revision" : "ced8d92c7cc53375cdf9806c59251fe0161f02ec",
|
||||
"version" : "0.2.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "bdk-swift",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/newtonick/bdk-swift",
|
||||
"state" : {
|
||||
"revision" : "4660bc83ea6088906edb090652d261e8ed4c09e3",
|
||||
"version" : "2.3.1-ssl-patch"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-numberkit",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/objecthub/swift-numberkit.git",
|
||||
"state" : {
|
||||
"revision" : "33af3f9011e45dcd8ee696492d30dbcd5a8a67f3",
|
||||
"version" : "2.6.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swiftsortedcollections",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/wolfmcnally/SwiftSortedCollections",
|
||||
"state" : {
|
||||
"revision" : "dd6c8e0eaef987e55a35c056d185144a7c71fc19",
|
||||
"version" : "0.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "urkit",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/BlockchainCommons/URKit",
|
||||
"state" : {
|
||||
"revision" : "c0a447560768e2552cf85a586dea8cfc26162891",
|
||||
"version" : "15.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "urui",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/BlockchainCommons/URUI",
|
||||
"state" : {
|
||||
"revision" : "c1b0ac2d0ba77741f00f439d311e7c85ee26a70a",
|
||||
"version" : "12.0.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 3
|
||||
}
|
||||
@ -1333,18 +1333,12 @@ final class BitcoinService {
|
||||
) -> String {
|
||||
let chain = isChange ? "1" : "0"
|
||||
let coinType = network.coinType
|
||||
let isTestnet = network != .mainnet
|
||||
|
||||
let normalized = cosigners.map { cosigner -> (xpub: String, fingerprint: String, derivationPath: String) in
|
||||
let raw = cosigner.xpub.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
|
||||
let xpub = URService.normalizeXpub(raw, isTestnet: isTestnet) ?? raw
|
||||
return (xpub: xpub, fingerprint: cosigner.fingerprint, derivationPath: cosigner.derivationPath)
|
||||
}
|
||||
|
||||
let sorted = normalized.sorted { $0.xpub < $1.xpub }
|
||||
let sorted = cosigners.sorted { $0.xpub < $1.xpub }
|
||||
|
||||
let keys = sorted.map { cosigner in
|
||||
"[\(cosigner.fingerprint)/48'/\(coinType)'/0'/2']\(cosigner.xpub)/\(chain)/*"
|
||||
let xpub = cosigner.xpub.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
|
||||
return "[\(cosigner.fingerprint)/48'/\(coinType)'/0'/2']\(xpub)/\(chain)/*"
|
||||
}.joined(separator: ",")
|
||||
|
||||
return "wsh(sortedmulti(\(requiredSignatures),\(keys)))"
|
||||
@ -1357,20 +1351,11 @@ final class BitcoinService {
|
||||
network: BitcoinNetwork
|
||||
) -> String {
|
||||
let coinType = network.coinType
|
||||
let isTestnet = network != .mainnet
|
||||
|
||||
// Normalize each cosigner xpub to standard xpub/tpub format (BDK descriptor
|
||||
// parser does not accept SLIP132-tagged Vpub/Zpub/Ypub/Upub keys).
|
||||
let normalized = cosigners.map { cosigner -> (xpub: String, fingerprint: String, derivationPath: String) in
|
||||
let raw = cosigner.xpub.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
|
||||
let xpub = URService.normalizeXpub(raw, isTestnet: isTestnet) ?? raw
|
||||
return (xpub: xpub, fingerprint: cosigner.fingerprint, derivationPath: cosigner.derivationPath)
|
||||
}
|
||||
|
||||
let sorted = normalized.sorted { $0.xpub < $1.xpub }
|
||||
let sorted = cosigners.sorted { $0.xpub < $1.xpub }
|
||||
|
||||
let keys = sorted.map { cosigner in
|
||||
"[\(cosigner.fingerprint)/48'/\(coinType)'/0'/2']\(cosigner.xpub)/<0;1>/*"
|
||||
let xpub = cosigner.xpub.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
|
||||
return "[\(cosigner.fingerprint)/48'/\(coinType)'/0'/2']\(xpub)/<0;1>/*"
|
||||
}.joined(separator: ",")
|
||||
|
||||
let raw = "wsh(sortedmulti(\(requiredSignatures),\(keys)))"
|
||||
|
||||
@ -193,17 +193,13 @@ final class SetupWizardViewModel {
|
||||
func buildDescriptors() {
|
||||
guard allCosignersComplete else { return }
|
||||
|
||||
// Build key origin strings — normalize to standard xpub/tpub format before
|
||||
// sorting so BIP67 ordering matches what's emitted in the descriptor.
|
||||
let isTestnet = network != .mainnet
|
||||
// Build key origin strings and sort by xpub (BIP67 lexicographic sort)
|
||||
var keyEntries: [(origin: String, xpub: String, fingerprint: String, path: String, label: String, index: Int)] = []
|
||||
|
||||
for i in 0 ..< totalCosigners {
|
||||
let raw = cosignerXpubs[i].trimmingCharacters(in: CharacterSet(charactersIn: "/"))
|
||||
let normalized = URService.normalizeXpub(raw, isTestnet: isTestnet) ?? raw
|
||||
keyEntries.append((
|
||||
origin: "[\(cosignerFingerprints[i])/48'/\(network.coinType)'/0'/2']",
|
||||
xpub: normalized,
|
||||
xpub: cosignerXpubs[i],
|
||||
fingerprint: cosignerFingerprints[i],
|
||||
path: cosignerDerivationPaths[i],
|
||||
label: cosignerLabels[i],
|
||||
@ -215,10 +211,12 @@ final class SetupWizardViewModel {
|
||||
keyEntries.sort { $0.xpub < $1.xpub }
|
||||
|
||||
let externalKeys = keyEntries.map {
|
||||
"\($0.origin)\($0.xpub)/0/*"
|
||||
let xpub = $0.xpub.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
|
||||
return "\($0.origin)\(xpub)/0/*"
|
||||
}.joined(separator: ",")
|
||||
let internalKeys = keyEntries.map {
|
||||
"\($0.origin)\($0.xpub)/1/*"
|
||||
let xpub = $0.xpub.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
|
||||
return "\($0.origin)\(xpub)/1/*"
|
||||
}.joined(separator: ",")
|
||||
|
||||
externalDescriptor = "wsh(sortedmulti(\(requiredSignatures),\(externalKeys)))"
|
||||
|
||||
@ -211,105 +211,6 @@ struct DescriptorTests {
|
||||
#expect(desc.contains("[73c5da0a/48'/1'/0'/2']"), "Should contain cosigner fingerprint/path")
|
||||
}
|
||||
|
||||
// MARK: - SLIP132 Vpub/Zpub normalization
|
||||
|
||||
/// Cosigners as they might be entered by the user — first one is in SLIP132
|
||||
/// `Vpub` format (BIP-84 wsh testnet), the other two are standard `tpub`.
|
||||
/// BDK's descriptor parser only accepts `xpub`/`tpub`, so the descriptor
|
||||
/// builder must normalize the `Vpub` to `tpub` before assembly.
|
||||
private static let mixedFormatCosigners: [(xpub: String, fingerprint: String, derivationPath: String)] = [
|
||||
(xpub: "Vpub5kv6Y3xqGFyhZQyCz8LzaSwVzAJLJTvHcUewWAhrLRRRjZeYs53qrfspVEBKZw6rvwGy8Z1ef7e7Vzsu3BLF6MkjFXWnLpmftKQT1Eub5Cf",
|
||||
fingerprint: "d03ce438", derivationPath: "m/48'/1'/0'/2'"),
|
||||
(xpub: "tpubDE2JvCZ3g8tEX3yegvXFn9cpzUyA2EEg6EwS7sAHcPER9yA6nFKdGPyLzsswYWa3SvEbKFmUiyFe9QQrpVpKwxojCud4ThNEv8R3j411Lcs",
|
||||
fingerprint: "f9755e5b", derivationPath: "m/48'/1'/0'/2'"),
|
||||
(xpub: "tpubDFEegnzQJr8LdYmGh1dGy3vqVgWtZ5w6q2cw4fbXhp15A29hvpf4NtAeFNvmmDRFTzeu1CveXs6dK2iPVADn2fSXWAQhHZhtLRGeHLmiBi5",
|
||||
fingerprint: "acc95047", derivationPath: "m/48'/1'/0'/2'"),
|
||||
]
|
||||
|
||||
/// The expected `tpub` form of the `Vpub` from `mixedFormatCosigners[0]`.
|
||||
private static let convertedTpub =
|
||||
"tpubDE4AYPPuhwTk7ENvANSMNU84wRecxjikg4e1WFHE4a6fxsNogCqnA7zzxyDoXp93JeyWNViXEKnkqaysaCrZRnTZDLYXnmbt7zrGxWYc3Mx"
|
||||
|
||||
@Test func combinedDescriptorNormalizesVpubToTpub() {
|
||||
let desc = BitcoinService.buildCombinedDescriptor(
|
||||
requiredSignatures: 2,
|
||||
cosigners: Self.mixedFormatCosigners,
|
||||
network: .testnet4
|
||||
)
|
||||
|
||||
// No SLIP132-tagged keys should remain in the assembled descriptor.
|
||||
#expect(!desc.contains("Vpub"), "Descriptor should not contain SLIP132 Vpub keys after normalization")
|
||||
#expect(!desc.contains("Zpub"), "Descriptor should not contain SLIP132 Zpub keys after normalization")
|
||||
|
||||
// The converted tpub from the original Vpub must be present, paired with
|
||||
// the cosigner's original fingerprint.
|
||||
#expect(desc.contains(Self.convertedTpub), "Vpub should normalize to expected tpub: \(Self.convertedTpub)")
|
||||
#expect(desc.contains("[d03ce438/48'/1'/0'/2']\(Self.convertedTpub)"), "Converted tpub should retain the original fingerprint/origin")
|
||||
}
|
||||
|
||||
@Test func singleChainDescriptorNormalizesVpubToTpub() {
|
||||
let external = BitcoinService.buildDescriptor(
|
||||
requiredSignatures: 2,
|
||||
cosigners: Self.mixedFormatCosigners,
|
||||
network: .testnet4,
|
||||
isChange: false
|
||||
)
|
||||
|
||||
#expect(!external.contains("Vpub"), "External descriptor should not contain Vpub")
|
||||
#expect(external.contains(Self.convertedTpub), "External descriptor should contain the converted tpub")
|
||||
}
|
||||
|
||||
@Test func descriptorBuiltFromMixedFormatsMatchesAllTpubVersion() {
|
||||
// Building the descriptor from the Vpub-mixed list should produce the same
|
||||
// result as building it from the equivalent all-tpub list — proving the
|
||||
// SLIP132 input is fully normalized away.
|
||||
let allTpubCosigners: [(xpub: String, fingerprint: String, derivationPath: String)] = [
|
||||
(xpub: Self.convertedTpub, fingerprint: "d03ce438", derivationPath: "m/48'/1'/0'/2'"),
|
||||
Self.mixedFormatCosigners[1],
|
||||
Self.mixedFormatCosigners[2],
|
||||
]
|
||||
|
||||
let fromMixed = BitcoinService.buildCombinedDescriptor(
|
||||
requiredSignatures: 2,
|
||||
cosigners: Self.mixedFormatCosigners,
|
||||
network: .testnet4
|
||||
)
|
||||
let fromAllTpub = BitcoinService.buildCombinedDescriptor(
|
||||
requiredSignatures: 2,
|
||||
cosigners: allTpubCosigners,
|
||||
network: .testnet4
|
||||
)
|
||||
|
||||
#expect(fromMixed == fromAllTpub, "Descriptor built from Vpub+tpub mix should equal all-tpub descriptor")
|
||||
}
|
||||
|
||||
@Test func descriptorSortsByNormalizedXpubForBIP67() {
|
||||
// The user-supplied example: cosigners entered in [Vpub, tpub, tpub] order
|
||||
// with fingerprints [d03ce438, f9755e5b, acc95047]. After normalization,
|
||||
// BIP67 lexicographic sort by tpub puts them in this fingerprint order:
|
||||
// 1. f9755e5b (tpubDE2JvCZ3g8tEX...)
|
||||
// 2. d03ce438 (tpubDE4AYPPuhwTk7... — converted from Vpub)
|
||||
// 3. acc95047 (tpubDFEegnzQJr8L...)
|
||||
let desc = BitcoinService.buildCombinedDescriptor(
|
||||
requiredSignatures: 2,
|
||||
cosigners: Self.mixedFormatCosigners,
|
||||
network: .testnet4
|
||||
)
|
||||
|
||||
let fp1 = desc.range(of: "[f9755e5b/48'/1'/0'/2']")
|
||||
let fp2 = desc.range(of: "[d03ce438/48'/1'/0'/2']")
|
||||
let fp3 = desc.range(of: "[acc95047/48'/1'/0'/2']")
|
||||
|
||||
#expect(fp1 != nil, "Descriptor should contain f9755e5b key origin")
|
||||
#expect(fp2 != nil, "Descriptor should contain d03ce438 key origin")
|
||||
#expect(fp3 != nil, "Descriptor should contain acc95047 key origin")
|
||||
|
||||
if let fp1, let fp2, let fp3 {
|
||||
#expect(fp1.lowerBound < fp2.lowerBound, "f9755e5b (tpubDE2J...) should sort before d03ce438 (tpubDE4A...)")
|
||||
#expect(fp2.lowerBound < fp3.lowerBound, "d03ce438 (tpubDE4A...) should sort before acc95047 (tpubDFEe...)")
|
||||
}
|
||||
}
|
||||
|
||||
/// Real descriptor decoded from a known-good crypto-output UR (from URServiceTests)
|
||||
private func realURDescriptor() -> String? {
|
||||
let urString = "UR:CRYPTO-OUTPUT/TAADMETAADMSOEADADAOLFTAADDLOSAOWKAXHDCLAOPDFNLNESAXHSJOFTVWFWHPTDUYPYHSROVLSWVDSRVWKBNNECZTHYMOURGSFDVDVAAAHDCXGMDKHPWMZTLRSOBSMWIOBWFWRPTODKNSEYAMTAHKRKQDISJTGWNSTSSFQDKPZSVTAHTAADEHOEADAEAOADAMTAADDYOTADLOCSDYYKADYKAEYKAOYKAOCYDYOTJEGMAXAAAYCYOYJNLKZMASJZGUIHIHIEGUINIOJTIHJPCXEYTAADDLOSAOWKAXHDCLAXIYMYFYWEMKASIOVSFYFDFDVASWONMTSKURSSTDMHVWSKLEAMKOVSGSDSCNSGNDOEAAHDCXBAMHFTFLGSDTBGBGFGGUREENGLFYTSHSCEJNKPHGGLFDFMTEWLENBDBBOXDYEMWTAHTAADEHOEADAEAOADAMTAADDYOTADLOCSDYYKADYKAEYKAOYKAOCYKNBWOSPAAXAAAYCYGRFPNSJOASJZGUIHIHIEGUINIOJTIHJPCXEHDLSWWZMD"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user