├── github_logo.png ├── .codecov.yml ├── SLPWalletHostTests ├── Assets.xcassets │ ├── Contents.json │ └── AppIcon.appiconset │ │ └── Contents.json ├── ViewController.swift ├── Info.plist ├── Base.lproj │ ├── Main.storyboard │ └── LaunchScreen.storyboard └── AppDelegate.swift ├── .gitmodules ├── SLPWallet.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcshareddata │ └── xcschemes │ │ ├── SLPWalletHostTests.xcscheme │ │ └── SLPWallet.xcscheme └── project.pbxproj ├── SLPWallet.xcworkspace ├── xcshareddata │ ├── WorkspaceSettings.xcsettings │ └── IDEWorkspaceChecks.plist └── contents.xcworkspacedata ├── SLPWallet ├── Wallet │ ├── SLPWalletAccount.swift │ └── SLPWalletUTXO.swift ├── Extensions │ ├── UserDefaults+Extensions.swift │ ├── String+Extensions.swift │ ├── Double+Extensions.swift │ ├── Data+Extensions.swift │ └── Array+Extensions.swift ├── StorageProvider.swift ├── StorageProvider │ ├── InternalStorageProvider.swift │ └── SecureStorageProvider.swift ├── SLPWalletConfig.swift ├── Token │ ├── SLPTokenUTXO.swift │ └── SLPToken.swift ├── Info.plist ├── Utils │ ├── TokenQtyConverter.swift │ ├── SLPTransactionParser.swift │ └── SLPTransactionBuilder.swift ├── Networks │ └── RestNetwork.swift ├── Services │ └── RestService.swift └── SLPWallet.swift ├── SLPWalletTests ├── SLPWalletTests.entitlements ├── UserDefault+ExtensionsTest.swift ├── String+ExtensionsTest.swift ├── Double+ExtensionsTest.swift ├── Info.plist ├── SLPWalletConfigTest.swift ├── SecureStorageProviderTest.swift ├── InternalStorageProviderTest.swift ├── TokenQtyConverterTest.swift ├── SLPWalletUTXOTest.swift ├── Assets │ ├── tx_details_genesis_tst.json │ ├── tx_details_mint_lvl001.json │ └── tx_details_send_tst.json ├── SLPTokenTest.swift ├── RestServiceTest.swift ├── SLPWalletTest.swift ├── SLPTransactionParserTest.swift └── SLPTransactionBuilderTest.swift ├── .travis.yml ├── Podfile ├── LICENSE ├── SLPWallet.podspec ├── .gitignore └── README.md /github_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bitcoin-com/slp-wallet-sdk-ios/HEAD/github_logo.png -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | branch: master 3 | ignore: 4 | - SLPWalletHostTests/* 5 | - Sample/* 6 | - Pods/* -------------------------------------------------------------------------------- /SLPWalletHostTests/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Sample/SLPWalletDemo"] 2 | path = Sample/SLPWalletDemo 3 | url = https://github.com/Bitcoin-com/slp-wallet-sdk-ios-demo.git 4 | -------------------------------------------------------------------------------- /SLPWallet.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SLPWallet.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /SLPWallet.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /SLPWallet.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SLPWallet.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SLPWallet/Wallet/SLPWalletAccount.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SLPAccount.swift 3 | // SLPWallet 4 | // 5 | // Created by Jean-Baptiste Dominguez on 2019/03/20. 6 | // Copyright © 2019 Bitcoin.com. All rights reserved. 7 | // 8 | 9 | import BitcoinKit 10 | 11 | struct SLPWalletAccount { 12 | let privKey: PrivateKey 13 | let cashAddress: String 14 | } 15 | -------------------------------------------------------------------------------- /SLPWalletTests/SLPWalletTests.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | keychain-access-groups 6 | 7 | $(AppIdentifierPrefix)com.bitcoin.SLPWalletTests 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /SLPWallet/Extensions/UserDefaults+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaults+Extensions.swift 3 | // SLPSDK 4 | // 5 | // Created by Jean-Baptiste Dominguez on 2019/02/26. 6 | // Copyright © 2019 Bitcoin.com. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension UserDefaults { 12 | public static var SLPWallet: UserDefaults { 13 | return UserDefaults(suiteName: "SLPWallet")! 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /SLPWallet/StorageProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StorageProvider.swift 3 | // SLPWallet 4 | // 5 | // Created by Jean-Baptiste Dominguez on 2019/03/27. 6 | // Copyright © 2019 Bitcoin.com. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol StorageProvider { 12 | func remove(_ key: String) throws 13 | func setString(_ value: String, key: String) throws 14 | func getString(_ key: String) throws -> String? 15 | } 16 | -------------------------------------------------------------------------------- /SLPWalletHostTests/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // SLPWalletHostTests 4 | // 5 | // Created by Jean-Baptiste Dominguez on 2019/03/10. 6 | // Copyright © 2019 Bitcoin.com. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | // Do any additional setup after loading the view, typically from a nib. 16 | } 17 | 18 | 19 | } 20 | 21 | -------------------------------------------------------------------------------- /SLPWallet/Extensions/String+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Extensions.swift 3 | // SLPWallet 4 | // 5 | // Created by Jean-Baptiste Dominguez on 2019/02/28. 6 | // Copyright © 2019 Bitcoin.com. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension String { 12 | func toSatoshis() -> Int64 { 13 | return self.toDouble().toSatoshis() 14 | } 15 | 16 | func toDouble() -> Double { 17 | return NSDecimalNumber(string: self).doubleValue 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode10.1 3 | before_install: 4 | - travis_retry pod repo update 5 | install: 6 | - gem install cocoapods --pre 7 | - pod deintegrate 8 | - pod cache clean --all 9 | - pod setup 10 | - travis_wait 30 pod install 11 | script: 12 | - xcodebuild -workspace SLPWallet.xcworkspace -scheme SLPWallet -destination platform\=iOS\ Simulator,OS\=12.1,name\=iPhone\ XR -enableCodeCoverage YES test > /dev/null 13 | after_success: 14 | - bash <(curl -s https://codecov.io/bash) -J 'SLPWallet' -t 256b3c8f-ae5a-4958-85b5-d5bbc86ebb83 > /dev/null -------------------------------------------------------------------------------- /SLPWalletTests/UserDefault+ExtensionsTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefault+ExtensionsTest.swift 3 | // SLPWalletTests 4 | // 5 | // Created by Jean-Baptiste Dominguez on 2019/03/12. 6 | // Copyright © 2019 Bitcoin.com. All rights reserved. 7 | // 8 | 9 | import Nimble 10 | import Quick 11 | @testable import SLPWallet 12 | 13 | class UserDefaultTest: QuickSpec { 14 | override func spec() { 15 | describe("UserDefault") { 16 | context("Get SLPWallet") { 17 | it("should success") { 18 | expect(UserDefaults.SLPWallet).toNot(beNil()) 19 | } 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /SLPWallet/Extensions/Double+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Double+Extensions.swift 3 | // SLPWallet 4 | // 5 | // Created by Jean-Baptiste Dominguez on 2019/02/28. 6 | // Copyright © 2019 Bitcoin.com. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Double { 12 | func toSatoshis() -> Int64 { 13 | let double = NSDecimalNumber(value: self).multiplying(by: 100000000).doubleValue 14 | return Int64(double.rounded()) 15 | } 16 | 17 | func toString() -> String { 18 | return String(self) 19 | } 20 | 21 | func toInt() -> Int { 22 | return NSDecimalNumber(value: self).intValue 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /SLPWalletTests/String+ExtensionsTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+ExtensionsTest.swift 3 | // SLPWalletTests 4 | // 5 | // Created by Jean-Baptiste Dominguez on 2019/03/12. 6 | // Copyright © 2019 Bitcoin.com. All rights reserved. 7 | // 8 | 9 | import Nimble 10 | import Quick 11 | @testable import SLPWallet 12 | 13 | class StringTest: QuickSpec { 14 | override func spec() { 15 | describe("String") { 16 | context("Converts") { 17 | it("should success") { 18 | let bch: String = "1.2" 19 | expect(bch.toSatoshis()).to(equal(120000000)) 20 | } 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /SLPWallet/StorageProvider/InternalStorageProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaultStorageProvider.swift 3 | // SLPWallet 4 | // 5 | // Created by Jean-Baptiste Dominguez on 2019/03/27. 6 | // Copyright © 2019 Bitcoin.com. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class InternalStorageProvider: StorageProvider { 12 | 13 | func remove(_ key: String) throws { 14 | UserDefaults.SLPWallet.removeObject(forKey: key) 15 | } 16 | 17 | func setString(_ value: String, key: String) throws { 18 | UserDefaults.SLPWallet.set(value, forKey: key) 19 | } 20 | 21 | func getString(_ key: String) throws -> String? { 22 | return UserDefaults.SLPWallet.getString(forKey: key) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | source 'https://github.com/Bitcoin-com/CocoaPods.git' 2 | source 'https://github.com/CocoaPods/Specs.git' 3 | 4 | platform :ios, '10.0' 5 | 6 | abstract_target 'All' do 7 | use_frameworks! 8 | 9 | # Pods for all targets 10 | pod 'RxSwift', '~> 4.0' 11 | pod 'RxCocoa', '~> 4.0' 12 | pod 'Moya/RxSwift', '~> 11.0' 13 | pod 'KeychainAccess', '~> 3.1.2' 14 | pod 'BitcoinKit', '~> 1.1.1' 15 | 16 | target 'SLPWallet' do 17 | end 18 | 19 | target 'SLPWalletTests' do 20 | inherit! :search_paths 21 | 22 | # Pods for SLPWalletTests 23 | pod 'RxBlocking' 24 | pod 'RxTest' 25 | pod 'Quick' 26 | pod 'Nimble' 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /SLPWalletTests/Double+ExtensionsTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Double+ExtensionsTest.swift 3 | // SLPWalletTests 4 | // 5 | // Created by Jean-Baptiste Dominguez on 2019/03/12. 6 | // Copyright © 2019 Bitcoin.com. All rights reserved. 7 | // 8 | 9 | import Nimble 10 | import Quick 11 | @testable import SLPWallet 12 | 13 | class DoubleTest: QuickSpec { 14 | override func spec() { 15 | describe("Double") { 16 | context("Converts") { 17 | it("should success") { 18 | let bch: Double = 1.2 19 | expect(bch.toInt()).to(equal(1)) 20 | expect(bch.toString()).to(equal("1.2")) 21 | expect(bch.toSatoshis()).to(equal(120000000)) 22 | } 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /SLPWallet/SLPWalletConfig.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SLPWalletConfig.swift 3 | // SLPWallet 4 | // 5 | // Created by Jean-Baptiste Dominguez on 2019/03/27. 6 | // Copyright © 2019 Bitcoin.com. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class SLPWalletConfig { 12 | 13 | // Singleton 14 | static var shared = SLPWalletConfig() 15 | 16 | var restAPIKey: String? 17 | var restURL: String = "https://rest.bitcoin.com/v2" 18 | 19 | public static func setRestAPIKey(_ apiKey: String) { 20 | // Any throws for UserDefaults, force wrap is safe 21 | SLPWalletConfig.shared.restAPIKey = apiKey 22 | } 23 | 24 | public static func setRestURL(_ restURL: String) { 25 | // Any throws for UserDefaults, force wrap is safe 26 | SLPWalletConfig.shared.restURL = restURL 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /SLPWallet/Token/SLPTokenUTXO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SLPTokenUTXO.swift 3 | // SLPWallet 4 | // 5 | // Created by Jean-Baptiste Dominguez on 2019/03/02. 6 | // Copyright © 2019 Bitcoin.com. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class SLPTokenUTXO: SLPWalletUTXO { 12 | var _rawTokenQty: Int 13 | var _tokenQty: Double? 14 | var _isValid: Bool 15 | 16 | public var rawTokenQty: Int { get { return _rawTokenQty } } 17 | public var tokenQty: Double? { get { return _tokenQty } } 18 | public var isValid: Bool { get { return _isValid } } 19 | 20 | public init(_ txid: String, satoshis: Int64, cashAddress: String, scriptPubKey: String, index: Int, rawTokenQty: Int) { 21 | self._rawTokenQty = rawTokenQty 22 | self._isValid = false 23 | super.init(txid, satoshis: satoshis, cashAddress: cashAddress, scriptPubKey: scriptPubKey, index: index) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /SLPWalletTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | NSAppTransportSecurity 22 | 23 | NSAllowsArbitraryLoads 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /SLPWallet/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 0.1 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSAppTransportSecurity 22 | 23 | NSAllowsArbitraryLoads 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /SLPWallet/StorageProvider/SecureStorageProvider.swift: -------------------------------------------------------------------------------- 1 | 2 | // 3 | // SecureStorageProvider.swift 4 | // SLPWallet 5 | // 6 | // Created by Jean-Baptiste Dominguez on 2019/03/12. 7 | // Copyright © 2019 Bitcoin.com. All rights reserved. 8 | // 9 | 10 | import KeychainAccess 11 | 12 | class SecureStorageProvider: StorageProvider { 13 | fileprivate let keychain: Keychain = { 14 | guard let bundleId = Bundle.main.bundleIdentifier else { 15 | fatalError("Should initialize properly to start using this SDK") 16 | } 17 | return Keychain(service: bundleId) 18 | }() 19 | 20 | func remove(_ key: String) throws { 21 | try keychain.remove(key) 22 | } 23 | 24 | func setString(_ value: String, key: String) throws { 25 | try keychain.set(value, key: key) 26 | } 27 | 28 | func getString(_ key: String) throws -> String? { 29 | return try keychain.get(key) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Bitcoin com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /SLPWallet/Extensions/Data+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Data+Extensions.swift 3 | // SLPWallet 4 | // 5 | // Created by Jean-Baptiste Dominguez on 2019/02/27. 6 | // Copyright © 2019 Bitcoin.com. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public extension Data { 12 | 13 | func removeLeft() -> Data { 14 | var newData = self 15 | newData.removeFirst() 16 | return newData 17 | } 18 | 19 | func removeRight() -> Data { 20 | var newData = self 21 | newData.removeLast() 22 | return newData 23 | } 24 | 25 | var uint8: UInt8 { 26 | get { 27 | var number: UInt8 = 0 28 | self.copyBytes(to:&number, count: MemoryLayout.size) 29 | return number 30 | } 31 | } 32 | 33 | var stringASCII: String? { 34 | get { 35 | return NSString(data: self, encoding: String.Encoding.ascii.rawValue) as String? 36 | } 37 | } 38 | 39 | var stringUTF8: String? { 40 | get { 41 | return NSString(data: self, encoding: String.Encoding.utf8.rawValue) as String? 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /SLPWallet/Extensions/Array+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Array+Extensions.swift 3 | // SLPWallet 4 | // 5 | // Created by Jean-Baptiste Dominguez on 2019/03/02. 6 | // Copyright © 2019 Bitcoin.com. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public extension Array where Element: Equatable { 12 | func removeDuplicates() -> Array { 13 | var result = [Element]() 14 | for value in self { 15 | if !result.contains(value) { 16 | result.append(value) 17 | } 18 | } 19 | return result 20 | } 21 | 22 | func chunk(_ chunkSize: Int) -> [[Element]] { 23 | return stride(from: 0, to: self.count, by: chunkSize).map { (startIndex) -> [Element] in 24 | let endIndex = (startIndex.advanced(by: chunkSize) > self.count) ? self.count-startIndex : chunkSize 25 | return Array(self[startIndex..(newElements: C) where C.Iterator.Element == Element{ 30 | let filteredList = newElements.filter { !self.contains($0) } 31 | self.append(contentsOf: filteredList) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /SLPWalletTests/SLPWalletConfigTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SLPWalletConfigTest.swift 3 | // SLPWalletTests 4 | // 5 | // Created by Jean-Baptiste Dominguez on 2019/03/27. 6 | // Copyright © 2019 Bitcoin.com. All rights reserved. 7 | // 8 | 9 | import Nimble 10 | import Quick 11 | @testable import SLPWallet 12 | 13 | class SLPWalletConfigTest: QuickSpec { 14 | override func spec() { 15 | describe("SLPWalletConfig") { 16 | context("SetAPIKey") { 17 | it("should success") { 18 | expect(SLPWalletConfig.shared.restAPIKey).to(beNil()) 19 | 20 | SLPWalletConfig.setRestAPIKey("test") 21 | expect(SLPWalletConfig.shared.restAPIKey).to(equal("test")) 22 | } 23 | } 24 | 25 | context("SetURL") { 26 | it("should success") { 27 | expect(SLPWalletConfig.shared.restURL).to(equal("https://rest.bitcoin.com/v2")) 28 | 29 | SLPWalletConfig.setRestURL("test") 30 | expect(SLPWalletConfig.shared.restURL).to(equal("test")) 31 | } 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /SLPWallet/Utils/TokenQtyConverter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TokenQtyConverter.swift 3 | // SLPWallet 4 | // 5 | // Created by Jean-Baptiste Dominguez on 2019/03/04. 6 | // Copyright © 2019 Bitcoin.com. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class TokenQtyConverter { 12 | 13 | static func convertToQty(_ rawAmount: Int, decimal: Int) -> Double { 14 | let amount = decimal > 0 ? Double(rawAmount) / pow(Double(10), Double(decimal)) : Double(rawAmount) 15 | return amount 16 | } 17 | 18 | static func convertToRawQty(_ amount: Double, decimal: Int) -> Int { 19 | let rawAmount = decimal > 0 ? Int(amount * pow(Double(10), Double(decimal))) : Int(amount) 20 | return rawAmount 21 | } 22 | 23 | static func convertToData(_ rawAmount: Int) -> Data? { 24 | 25 | // Convert the amount in hexa 26 | let amountInHex = String(rawAmount, radix: 16) 27 | 28 | // Create the empty hex 29 | var amountInHex16 = [Character](repeating: "0", count: 16) 30 | for (i, value) in amountInHex.enumerated() { 31 | amountInHex16[amountInHex16.count - amountInHex.count + i] = value 32 | } 33 | 34 | return Data(hex: String(amountInHex16)) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /SLPWalletTests/SecureStorageProviderTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SecureStorageProviderTest.swift 3 | // SLPWalletTests 4 | // 5 | // Created by Jean-Baptiste Dominguez on 2019/03/27. 6 | // Copyright © 2019 Bitcoin.com. All rights reserved. 7 | // 8 | 9 | import Nimble 10 | import Quick 11 | @testable import SLPWallet 12 | 13 | class SecureStorageProviderTest: QuickSpec { 14 | 15 | override func spec() { 16 | describe("SecureStorageProvider") { 17 | context("Get/Set/Remove String") { 18 | it("should success") { 19 | let storageProvider = SecureStorageProvider() 20 | 21 | do { 22 | try storageProvider.remove("test") 23 | 24 | var storedValue = try storageProvider.getString("test") 25 | expect(storedValue).to(beNil()) 26 | 27 | try storageProvider.setString("value", key: "test") 28 | 29 | storedValue = try storageProvider.getString("test") 30 | expect(storedValue).to(equal("value")) 31 | } catch { 32 | fail() 33 | } 34 | 35 | } 36 | } 37 | } 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /SLPWalletTests/InternalStorageProviderTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InternalStorageProviderTest.swift 3 | // SLPWalletTests 4 | // 5 | // Created by Jean-Baptiste Dominguez on 2019/03/27. 6 | // Copyright © 2019 Bitcoin.com. All rights reserved. 7 | // 8 | 9 | import Nimble 10 | import Quick 11 | @testable import SLPWallet 12 | 13 | class InternalStorageProviderTest: QuickSpec { 14 | 15 | override func spec() { 16 | describe("InternalStorageProvider") { 17 | context("Get/Set/Remove String") { 18 | it("should success") { 19 | let storageProvider = InternalStorageProvider() 20 | 21 | do { 22 | try storageProvider.remove("test") 23 | 24 | var storedValue = try storageProvider.getString("test") 25 | expect(storedValue).to(beNil()) 26 | 27 | try storageProvider.setString("value", key: "test") 28 | 29 | storedValue = try storageProvider.getString("test") 30 | expect(storedValue).to(equal("value")) 31 | } catch { 32 | fail() 33 | } 34 | 35 | } 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /SLPWalletTests/TokenQtyConverterTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TokenQtyConverterTest.swift 3 | // SLPWalletTests 4 | // 5 | // Created by Jean-Baptiste Dominguez on 2019/03/12. 6 | // Copyright © 2019 Bitcoin.com. All rights reserved. 7 | // 8 | 9 | import Nimble 10 | import Quick 11 | @testable import SLPWallet 12 | 13 | class TokenQtyConverterTest: QuickSpec { 14 | override func spec() { 15 | describe("TokenQtyConverter") { 16 | context("Convert to quantity") { 17 | it("should success") { 18 | expect(TokenQtyConverter.convertToQty(123456, decimal: 3)).to(equal(123.456)) 19 | expect(TokenQtyConverter.convertToQty(123456000, decimal: 3)).to(equal(123456)) 20 | } 21 | } 22 | 23 | context("Convert to raw quantity") { 24 | it("should success") { 25 | expect(TokenQtyConverter.convertToRawQty(123.456, decimal: 3)).to(equal(123456)) 26 | expect(TokenQtyConverter.convertToRawQty(123456, decimal: 3)).to(equal(123456000)) 27 | } 28 | } 29 | 30 | context("Convert to data") { 31 | it("should success") { 32 | expect(TokenQtyConverter.convertToData(1152921504606846976)).toNot(beNil()) 33 | } 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /SLPWallet.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod spec lint SLPWallet.podspec' to ensure this is a 3 | # valid spec and to remove all comments including this before submitting the spec. 4 | # 5 | # To learn more about Podspec attributes see http://docs.cocoapods.org/specification.html 6 | # To see working Podspecs in the CocoaPods repo see https://github.com/CocoaPods/Specs/ 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | 11 | s.name = "SLPWallet" 12 | s.version = "0.1.0" 13 | s.summary = "SLP Wallet SDK for iOS" 14 | s.description = "SLP Wallet SDK for iOS" 15 | 16 | s.homepage = "https://github.com/Bitcoin-com/slp-wallet-sdk-ios" 17 | 18 | s.license = { :type => "MIT", :file => "LICENSE" } 19 | 20 | s.author = { "jbdtky" => "jb@bitcoin.com" } 21 | 22 | s.swift_version = "4.0" 23 | s.platform = :ios, "10.0" 24 | 25 | # When using multiple platforms 26 | # s.ios.deployment_target = "5.0" 27 | # s.osx.deployment_target = "10.7" 28 | # s.watchos.deployment_target = "2.0" 29 | # s.tvos.deployment_target = "9.0" 30 | 31 | s.source = { :git => "https://github.com/Bitcoin-com/slp-wallet-sdk-ios.git", :branch => "v#{s.version}" } 32 | s.source_files = "SLPWallet/**/*.swift" 33 | 34 | # s.resource = "icon.png" 35 | # s.resources = "Resources/*.png" 36 | 37 | # s.preserve_paths = "FilesToSave", "MoreFilesToSave" 38 | 39 | s.dependency "RxSwift", "~> 4.0" 40 | s.dependency "RxCocoa", "~> 4.0" 41 | s.dependency "Moya/RxSwift", "~> 11.0" 42 | s.dependency "KeychainAccess", "~> 3.1.2" 43 | s.dependency "BitcoinKit", "~> 1.1.1" 44 | 45 | end 46 | -------------------------------------------------------------------------------- /SLPWalletHostTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /SLPWalletHostTests/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /SLPWalletHostTests/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | # Package.resolved 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | Pods/ 50 | Podfile.lock 51 | 52 | # Carthage 53 | # 54 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 55 | # Carthage/Checkouts 56 | 57 | Carthage/Build 58 | 59 | # fastlane 60 | # 61 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 62 | # screenshots whenever they are needed. 63 | # For more information about the recommended setup visit: 64 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 65 | 66 | fastlane/report.xml 67 | fastlane/Preview.html 68 | fastlane/screenshots/**/*.png 69 | fastlane/test_output 70 | *.coverage.txt 71 | -------------------------------------------------------------------------------- /SLPWalletHostTests/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /SLPWallet/Wallet/SLPWalletUTXO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SLPUTXO.swift 3 | // SLPWallet 4 | // 5 | // Created by Jean-Baptiste Dominguez on 2019/03/02. 6 | // Copyright © 2019 Bitcoin.com. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import BitcoinKit 11 | 12 | public class SLPWalletUTXO { 13 | var _txid: String // TODO: Expose until I fix the TransactionBuilder to provide the right tx directly 14 | fileprivate var _satoshis: Int64 15 | fileprivate var _cashAddress: String 16 | fileprivate var _scriptPubKey: String 17 | fileprivate var _index: Int 18 | 19 | public var txid: String { get { return _txid } } 20 | public var satoshis: Int64 { get { return _satoshis } } 21 | public var cashAddress: String { get { return _cashAddress } } 22 | public var scriptPubKey: String { get { return _scriptPubKey } } 23 | public var index: Int { get { return _index } } 24 | 25 | public init(_ txid: String, satoshis: Int64, cashAddress: String, scriptPubKey: String, index: Int) { 26 | self._txid = txid 27 | self._satoshis = satoshis 28 | self._cashAddress = cashAddress 29 | self._scriptPubKey = scriptPubKey 30 | self._index = index 31 | } 32 | } 33 | 34 | extension SLPWalletUTXO { 35 | func asUnspentTransaction() -> UnspentTransaction { 36 | let transactionOutput = TransactionOutput(value: UInt64(_satoshis), lockingScript: Data(hex: _scriptPubKey)!) 37 | let txid: Data = Data(hex: String(_txid))! 38 | let txHash: Data = Data(txid.reversed()) 39 | let transactionOutpoint = TransactionOutPoint(hash: txHash, index: UInt32(_index)) 40 | return UnspentTransaction(output: transactionOutput, outpoint: transactionOutpoint) 41 | } 42 | } 43 | 44 | extension SLPWalletUTXO: Equatable { 45 | public static func == (lhs: SLPWalletUTXO, rhs: SLPWalletUTXO) -> Bool { 46 | return lhs.index == rhs.index && 47 | lhs.txid == rhs.txid 48 | } 49 | } 50 | 51 | extension SLPWalletUTXO: Hashable { 52 | public var hashValue: Int { 53 | return txid.hashValue << 8 | index 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /SLPWalletTests/SLPWalletUTXOTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SLPWalletUTXOTest.swift 3 | // SLPWalletTests 4 | // 5 | // Created by Jean-Baptiste Dominguez on 2019/03/12. 6 | // Copyright © 2019 Bitcoin.com. All rights reserved. 7 | // 8 | 9 | import Nimble 10 | import Quick 11 | @testable import SLPWallet 12 | 13 | class SLPWalletUTXOTest: QuickSpec { 14 | override func spec() { 15 | describe("SLPWalletUTXO") { 16 | context("Create SLPWalletUTXO") { 17 | it("should success") { 18 | let utxo = SLPWalletUTXO("txid", satoshis: 100, cashAddress: "cashAddress", scriptPubKey: "scriptPubKey", index: 1) 19 | expect(utxo.txid).to(equal("txid")) 20 | expect(utxo.satoshis).to(equal(100)) 21 | expect(utxo.cashAddress).to(equal("cashAddress")) 22 | expect(utxo.scriptPubKey).to(equal("scriptPubKey")) 23 | expect(utxo.index).to(equal(1)) 24 | } 25 | } 26 | context("Equal SLPWalletUTXO") { 27 | it("should be equal") { 28 | let utxo1 = SLPWalletUTXO("txid1", satoshis: 100, cashAddress: "cashAddress", scriptPubKey: "scriptPubKey", index: 1) 29 | let utxo2 = SLPWalletUTXO("txid1", satoshis: 100, cashAddress: "cashAddress", scriptPubKey: "scriptPubKey", index: 1) 30 | expect(utxo1 == utxo2).to(equal(true)) 31 | } 32 | 33 | it("should be not equal") { 34 | let utxo1 = SLPWalletUTXO("txid1", satoshis: 100, cashAddress: "cashAddress", scriptPubKey: "scriptPubKey", index: 1) 35 | let utxo2 = SLPWalletUTXO("txid2", satoshis: 100, cashAddress: "cashAddress", scriptPubKey: "scriptPubKey", index: 1) 36 | expect(utxo1 == utxo2).to(equal(false)) 37 | } 38 | 39 | it("should be not equal") { 40 | let utxo1 = SLPWalletUTXO("txid1", satoshis: 100, cashAddress: "cashAddress", scriptPubKey: "scriptPubKey", index: 1) 41 | let utxo2 = SLPWalletUTXO("txid1", satoshis: 100, cashAddress: "cashAddress", scriptPubKey: "scriptPubKey", index: 2) 42 | expect(utxo1 == utxo2).to(equal(false)) 43 | } 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /SLPWalletHostTests/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // SLPWalletHostTests 4 | // 5 | // Created by Jean-Baptiste Dominguez on 2019/03/10. 6 | // Copyright © 2019 Bitcoin.com. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /SLPWallet/Networks/RestNetwork.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Network.swift 3 | // SLPWallet 4 | // 5 | // Created by Jean-Baptiste Dominguez on 2019/02/26. 6 | // Copyright © 2019 Bitcoin.com. All rights reserved. 7 | // 8 | 9 | import Moya 10 | 11 | enum RestNetwork { 12 | case fetchUTXOs([String]) 13 | case fetchTxDetails([String]) 14 | case fetchTxValidations([String]) 15 | case broadcast(String) 16 | } 17 | 18 | extension RestNetwork: TargetType { 19 | 20 | public var baseURL: URL { 21 | guard let url = URL(string: SLPWalletConfig.shared.restURL) else { 22 | fatalError("should be able to parse this URL") 23 | } 24 | 25 | return url 26 | } 27 | 28 | public var path: String { 29 | switch self { 30 | case .fetchUTXOs: 31 | return "/address/utxo" 32 | case .fetchTxDetails: 33 | return "/transaction/details" 34 | case .fetchTxValidations: 35 | return "/slp/validateTxid" 36 | case .broadcast(let rawTx): 37 | return "/rawtransactions/sendRawTransaction/\(rawTx)" 38 | } 39 | } 40 | 41 | public var method: Moya.Method { 42 | switch self { 43 | case .fetchUTXOs: return .post 44 | case .fetchTxDetails: return .post 45 | case .fetchTxValidations: return .post 46 | case .broadcast: return .get 47 | } 48 | } 49 | 50 | public var sampleData: Data { 51 | return Data() 52 | } 53 | 54 | public var task: Task { 55 | switch self { 56 | case .fetchUTXOs(let addresses): 57 | return .requestParameters(parameters: ["addresses": addresses], encoding: JSONEncoding.default) 58 | case .fetchTxDetails(let txids): 59 | return .requestParameters(parameters: ["txids": txids], encoding: JSONEncoding.default) 60 | case .fetchTxValidations(let txids): 61 | return .requestParameters(parameters: ["txids": txids], encoding: JSONEncoding.default) 62 | default: 63 | return .requestPlain 64 | } 65 | } 66 | 67 | public var headers: [String : String]? { 68 | var headers = ["Content-Type": "application/json"] 69 | 70 | guard let apiKey = SLPWalletConfig.shared.restAPIKey else { 71 | return headers 72 | } 73 | 74 | // Add the API Key 75 | headers["Authorization"] = "Basic \(apiKey)" 76 | 77 | return headers 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /SLPWalletTests/Assets/tx_details_genesis_tst.json: -------------------------------------------------------------------------------- 1 | {"txid":"9cc1cf24e502554d2d3d09918c27decda2c260762961acd469c5473fbcfe192e","version":1,"locktime":573581,"vin":[{"txid":"30d884ff293570caf7618fe70c11837bd08c6ee685d66f6e1fcbd85758412782","vout":3,"sequence":4294967294,"n":0,"scriptSig":{"hex":"483045022100b625b427753e2383414f191b621fbdd1b9ce9594f91d8a74b45659979071ffc202207be933af6a412f86f0c9435a03f66fbf47cbfd7de1b3b36393e30b4879ceac4741210329d5ffda1250d97614cfd3a5cb1c89d0a255c59584c091915b21b3e64137fe7a","asm":"3045022100b625b427753e2383414f191b621fbdd1b9ce9594f91d8a74b45659979071ffc202207be933af6a412f86f0c9435a03f66fbf47cbfd7de1b3b36393e30b4879ceac47[ALL|FORKID] 0329d5ffda1250d97614cfd3a5cb1c89d0a255c59584c091915b21b3e64137fe7a"},"value":641440,"legacyAddress":"1GiDQv4mH5mRQ339nkPbM9ppoD2L5Sub8E","cashAddress":"bitcoincash:qzk92nt0xdxc9qy3yj53h9rjw8dk0s9cqqucfqpcd6"}],"vout":[{"value":"0.00000000","n":0,"scriptPubKey":{"hex":"6a04534c500001010747454e4553495303545354105472792120537769667420546f6b656e0e6a6240626974636f696e2e636f6d4c000102010208000000174876e800","asm":"OP_RETURN 5262419 1 47454e45534953 5526356 5472792120537769667420546f6b656e 6a6240626974636f696e2e636f6d 0 2 2 000000174876e800"},"spentTxId":null,"spentIndex":null,"spentHeight":null},{"value":"0.00000546","n":1,"scriptPubKey":{"hex":"76a9146a234e78ca9c43f7161614572e41519c5cea082388ac","asm":"OP_DUP OP_HASH160 6a234e78ca9c43f7161614572e41519c5cea0823 OP_EQUALVERIFY OP_CHECKSIG","addresses":["1AgCvUYA7jp9muzaz6BSjFgTaeH969wAEc"],"type":"pubkeyhash"},"spentTxId":"8260679c31a773efaed90d0a74a7f0ee80153bc015788ae2d8aabb8d011ef286","spentIndex":0,"spentHeight":573597},{"value":"0.00000546","n":2,"scriptPubKey":{"hex":"76a9146a234e78ca9c43f7161614572e41519c5cea082388ac","asm":"OP_DUP OP_HASH160 6a234e78ca9c43f7161614572e41519c5cea0823 OP_EQUALVERIFY OP_CHECKSIG","addresses":["1AgCvUYA7jp9muzaz6BSjFgTaeH969wAEc"],"type":"pubkeyhash"},"spentTxId":null,"spentIndex":null,"spentHeight":null},{"value":"0.00640012","n":3,"scriptPubKey":{"hex":"76a914ac554d6f334d82809124a91b947271db67c0b80088ac","asm":"OP_DUP OP_HASH160 ac554d6f334d82809124a91b947271db67c0b800 OP_EQUALVERIFY OP_CHECKSIG","addresses":["1GiDQv4mH5mRQ339nkPbM9ppoD2L5Sub8E"],"type":"pubkeyhash"},"spentTxId":"337f747d6b8d5af84990b2a19e817765ca8796ac6616535bb95f3c285bbbccf8","spentIndex":2,"spentHeight":573593}],"blockhash":"0000000000000000056bbad37777fd51a6bf0ad204e43010e044b019a6a93e7f","blockheight":573582,"confirmations":1706,"time":1552474594,"blocktime":1552474594,"valueOut":0.00641104,"size":336,"valueIn":0.0064144,"fees":0.00000336} 2 | -------------------------------------------------------------------------------- /SLPWalletTests/Assets/tx_details_mint_lvl001.json: -------------------------------------------------------------------------------- 1 | {"txid":"c3b72361cee1a7ed5d0911714da7439313eaf22fde842f871656e4d438eba7d1","version":2,"locktime":0,"vin":[{"txid":"218e7c3acc75b9053dd59890b759bb6a80255cc5a776f396903d7467cb0516a4","vout":0,"sequence":4294967295,"n":0,"scriptSig":{"hex":"47304402204e189d7b3150e248a1d12d43f2270d1082fffcd16ad15f22f827af0d7f2c6c95022027b8dd61540019f428fa9d4f909d63a42fa9600dcbabeef244d6fa3674629370412103d0fa8422e080b6a45db0eab75ae3b3e89b390190657d4852a2e6622ee7435eba","asm":"304402204e189d7b3150e248a1d12d43f2270d1082fffcd16ad15f22f827af0d7f2c6c95022027b8dd61540019f428fa9d4f909d63a42fa9600dcbabeef244d6fa3674629370[ALL|FORKID] 03d0fa8422e080b6a45db0eab75ae3b3e89b390190657d4852a2e6622ee7435eba"},"value":1000,"legacyAddress":"19rr6Noynu23KdVU5nodRJHaHyeREm3Xvd","cashAddress":"bitcoincash:qpsjuhn3qdhwtk27q38zrc296ynh89sx05n0y0ygas"},{"txid":"4712f1953f364e11f41e6139bee7899e7b3ef53eaed08a19403b793b80129456","vout":2,"sequence":4294967295,"n":1,"scriptSig":{"hex":"47304402200c3f3b49ab99bff38a99c0f0aeb17c269ac5e57a5a352a6db83561d183bbcb2402207e3def849dc967c8ee8f6a17196b2b08e60b93c56b7f3044caa964753ca9f4b741210238ec2a07b7df4b362528768138b0a24500aa626e0805bca27d53574a1cd36dca","asm":"304402200c3f3b49ab99bff38a99c0f0aeb17c269ac5e57a5a352a6db83561d183bbcb2402207e3def849dc967c8ee8f6a17196b2b08e60b93c56b7f3044caa964753ca9f4b7[ALL|FORKID] 0238ec2a07b7df4b362528768138b0a24500aa626e0805bca27d53574a1cd36dca"},"value":546,"legacyAddress":"1L3TpUNL4HtxkVrw87toGdJthb9VvLE5pc","cashAddress":"bitcoincash:qrgwrx7hvd27jqz8q5fmgr4kg5cy76yp05a3prllmr"}],"vout":[{"value":"0.00000000","n":0,"scriptPubKey":{"hex":"6a04534c50000101044d494e5420d5efb237f43a822ede2086bbefca44f1157b7adf2ddeed87c4b294bd136d1d360102080000000000000001","asm":"OP_RETURN 5262419 1 1414416717 d5efb237f43a822ede2086bbefca44f1157b7adf2ddeed87c4b294bd136d1d36 2 0000000000000001"},"spentTxId":null,"spentIndex":null,"spentHeight":null},{"value":"0.00000546","n":1,"scriptPubKey":{"hex":"76a914a133833c086fa0a4f624d46f17a94a7c1220496088ac","asm":"OP_DUP OP_HASH160 a133833c086fa0a4f624d46f17a94a7c12204960 OP_EQUALVERIFY OP_CHECKSIG","addresses":["1FhMW3dRKz14gKWVZ26ax3sTXqrhpQJR1p"],"type":"pubkeyhash"},"spentTxId":"846e5f8791436b3d502e6e87eae5fb6bbde99533a7259c42dab76f0e37c51b27","spentIndex":2,"spentHeight":575019},{"value":"0.00000546","n":2,"scriptPubKey":{"hex":"76a914d0e19bd76355e900470513b40eb645304f68817d88ac","asm":"OP_DUP OP_HASH160 d0e19bd76355e900470513b40eb645304f68817d OP_EQUALVERIFY OP_CHECKSIG","addresses":["1L3TpUNL4HtxkVrw87toGdJthb9VvLE5pc"],"type":"pubkeyhash"},"spentTxId":"396b971bf45ffab5e0545756de4ecf4b7109dca3df6b4207a155e8cff77cce22","spentIndex":1,"spentHeight":575089}],"blockhash":"000000000000000002ef5af4772deb604aed77cb978e403c2465ec71ba5881de","blockheight":575019,"confirmations":283,"time":1553331316,"blocktime":1553331316,"valueOut":0.00001092,"size":438,"valueIn":0.00001546,"fees":0.00000454} -------------------------------------------------------------------------------- /SLPWalletTests/Assets/tx_details_send_tst.json: -------------------------------------------------------------------------------- 1 | {"txid":"a9f639148662ca6376c3650f3d7e6dffbe9a477cf947499bfcb2c85412331c2e","version":1,"locktime":0,"vin":[{"txid":"3c3366de75c244ba2ccaab3578880784e904d1b65151ca651816a98fe94079a3","vout":2,"sequence":4294967295,"n":0,"scriptSig":{"hex":"4730440220600f4fc21f63214142b3263c0658289d8a52d83753654c4caa1f0803b88af88502201becdda4624d13b29338f46ef830ad405feb042769ab96f9902c8f117ad0bcb341210258fa5858bd9d8f2eca41f6b828cccedde7a316284948773e29a9415d55c750ca","asm":"30440220600f4fc21f63214142b3263c0658289d8a52d83753654c4caa1f0803b88af88502201becdda4624d13b29338f46ef830ad405feb042769ab96f9902c8f117ad0bcb3[ALL|FORKID] 0258fa5858bd9d8f2eca41f6b828cccedde7a316284948773e29a9415d55c750ca"},"value":546,"legacyAddress":"1F7yFGSFeK5Y87aasQEREyHb1A4XtvDZ88","cashAddress":"bitcoincash:qzdwxdtprwxhynpvcqngtupp3tn58smte5z2yfqe0v"},{"txid":"3c3366de75c244ba2ccaab3578880784e904d1b65151ca651816a98fe94079a3","vout":3,"sequence":4294967295,"n":1,"scriptSig":{"hex":"4730440220181362ca6fda6a479fbe18552d83af9621b6c56f0f0c36acc8e2f4a66f1692d1022058591e45a4dd2359a42f085e4268dffd03d41de1da12ef17c07e83566d12a3494121038f2d5d296773570cde7f5db285c9e82a2607bc18ac0eb28e7239c129ac8beed4","asm":"30440220181362ca6fda6a479fbe18552d83af9621b6c56f0f0c36acc8e2f4a66f1692d1022058591e45a4dd2359a42f085e4268dffd03d41de1da12ef17c07e83566d12a349[ALL|FORKID] 038f2d5d296773570cde7f5db285c9e82a2607bc18ac0eb28e7239c129ac8beed4"},"value":194511,"legacyAddress":"12dQET6vKJZnrP8EivYaobdjon5zKyzZc8","cashAddress":"bitcoincash:qqga5ljshfug5g27532d9xc0w55yxdjzwygw62c0nk"}],"vout":[{"value":"0.00000000","n":0,"scriptPubKey":{"hex":"6a04534c500001010453454e44209cc1cf24e502554d2d3d09918c27decda2c260762961acd469c5473fbcfe192e080000000000030cdc080000000000e61a82","asm":"OP_RETURN 5262419 1 1145980243 9cc1cf24e502554d2d3d09918c27decda2c260762961acd469c5473fbcfe192e 0000000000030cdc 0000000000e61a82"},"spentTxId":null,"spentIndex":null,"spentHeight":null},{"value":"0.00000546","n":1,"scriptPubKey":{"hex":"76a9146a234e78ca9c43f7161614572e41519c5cea082388ac","asm":"OP_DUP OP_HASH160 6a234e78ca9c43f7161614572e41519c5cea0823 OP_EQUALVERIFY OP_CHECKSIG","addresses":["1AgCvUYA7jp9muzaz6BSjFgTaeH969wAEc"],"type":"pubkeyhash"},"spentTxId":null,"spentIndex":null,"spentHeight":null},{"value":"0.00000546","n":2,"scriptPubKey":{"hex":"76a9149ae335611b8d724c2cc02685f0218ae743c36bcd88ac","asm":"OP_DUP OP_HASH160 9ae335611b8d724c2cc02685f0218ae743c36bcd OP_EQUALVERIFY OP_CHECKSIG","addresses":["1F7yFGSFeK5Y87aasQEREyHb1A4XtvDZ88"],"type":"pubkeyhash"},"spentTxId":"2a37795de56822b43b769f6546c5a65ec3dd96bcfe5738142f8f150093771137","spentIndex":0,"spentHeight":575214},{"value":"0.00192966","n":3,"scriptPubKey":{"hex":"76a91411da7e50ba788a215ea454d29b0f75284336427188ac","asm":"OP_DUP OP_HASH160 11da7e50ba788a215ea454d29b0f752843364271 OP_EQUALVERIFY OP_CHECKSIG","addresses":["12dQET6vKJZnrP8EivYaobdjon5zKyzZc8"],"type":"pubkeyhash"},"spentTxId":"2a37795de56822b43b769f6546c5a65ec3dd96bcfe5738142f8f150093771137","spentIndex":1,"spentHeight":575214}],"blockhash":"000000000000000000ab606a11f63365cc4d5260083d3a54d0dd09ef6efc2d79","blockheight":575214,"confirmations":85,"time":1553449491,"blocktime":1553449491,"valueOut":0.00194058,"size":479,"valueIn":0.00195057,"fees":0.00000999} -------------------------------------------------------------------------------- /SLPWallet/Token/SLPToken.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SLPToken.swift 3 | // SLPWallet 4 | // 5 | // Created by Jean-Baptiste Dominguez on 2019/03/02. 6 | // Copyright © 2019 Bitcoin.com. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RxSwift 11 | 12 | public class SLPToken { 13 | var _tokenId: String? 14 | var _tokenTicker: String? 15 | var _tokenName: String? 16 | var _mintUTXO: SLPWalletUTXO? 17 | var _decimal: Int? { 18 | willSet { 19 | guard let decimal = newValue else { 20 | return 21 | } 22 | 23 | // If decimal == 0, replace per the rawTokenQty 24 | _utxos.forEach { $0._tokenQty = (decimal > 0 ? (Double($0._rawTokenQty) / pow(Double(10), Double(decimal))) : Double($0.rawTokenQty)) } 25 | } 26 | } 27 | 28 | var _utxos = [SLPTokenUTXO]() { 29 | willSet { 30 | guard let decimal = self.decimal else { 31 | return 32 | } 33 | 34 | // If decimal == 0, replace per the rawTokenQty 35 | newValue.forEach { $0._tokenQty = (decimal > 0 ? (Double($0._rawTokenQty) / pow(Double(10), Double(decimal))) : Double($0._rawTokenQty)) } 36 | } 37 | } 38 | 39 | // Public interface 40 | public var tokenId: String? { get { return _tokenId } } 41 | public var tokenTicker: String? { get { return _tokenTicker } } 42 | public var tokenName: String? { get { return _tokenName } } 43 | public var mintUTXO: SLPWalletUTXO? { get { return _mintUTXO } } 44 | public var decimal: Int? { get { return _decimal } } 45 | public var utxos: [SLPTokenUTXO] { get { return _utxos.filter { $0.isValid } } } 46 | 47 | public init() { 48 | } 49 | 50 | public init(_ tokenId: String) { 51 | self._tokenId = tokenId 52 | } 53 | 54 | public func getGas() -> Int { 55 | return utxos.reduce(0, { $0 + Int($1.satoshis) }) 56 | } 57 | 58 | public func getBalance() -> Double { 59 | return utxos.reduce(0, { $0 + ($1.tokenQty ?? 0) }) 60 | } 61 | } 62 | 63 | extension SLPToken { 64 | func addUTXO(_ utxo: SLPTokenUTXO) { 65 | guard let decimal = self.decimal else { 66 | _utxos.append(utxo) 67 | return 68 | } 69 | 70 | utxo._tokenQty = decimal > 0 ? (Double(utxo.rawTokenQty) / pow(Double(10), Double(decimal))) : Double(utxo.rawTokenQty) 71 | _utxos.append(utxo) 72 | } 73 | 74 | func addUTXOs(_ utxos: [SLPTokenUTXO]) { 75 | utxos.forEach { self.addUTXO($0) } 76 | } 77 | 78 | func removeUTXO(_ utxo: SLPTokenUTXO) { 79 | guard let i = _utxos.firstIndex(where: { $0.index == utxo.index && $0.txid == utxo.txid }) else { 80 | return 81 | } 82 | _utxos.remove(at: i) 83 | } 84 | 85 | func merge(_ token: SLPToken) -> SLPToken { 86 | if let tokenId = token._tokenId { 87 | self._tokenId = tokenId 88 | } 89 | if let tokenName = token._tokenName { 90 | self._tokenName = tokenName 91 | } 92 | if let tokenTicker = token._tokenTicker { 93 | self._tokenTicker = tokenTicker 94 | } 95 | if let decimal = token._decimal { 96 | self._decimal = decimal 97 | } 98 | if let mintUTXO = token._mintUTXO { 99 | self._mintUTXO = mintUTXO 100 | } 101 | self._utxos.append(contentsOf: token._utxos) 102 | 103 | return self 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /SLPWallet.xcodeproj/xcshareddata/xcschemes/SLPWalletHostTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /SLPWalletTests/SLPTokenTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SLPTokenTest.swift 3 | // SLPWalletTests 4 | // 5 | // Created by Jean-Baptiste Dominguez on 2019/03/12. 6 | // Copyright © 2019 Bitcoin.com. All rights reserved. 7 | // 8 | 9 | import Nimble 10 | import Quick 11 | @testable import SLPWallet 12 | 13 | class SLPWalletTokenTest: QuickSpec { 14 | override func spec() { 15 | describe("SLPWalletToken") { 16 | context("Add UTXOs") { 17 | it("should success") { 18 | let token = SLPToken("test") 19 | 20 | let utxo = SLPTokenUTXO("txid", satoshis: 100, cashAddress: "cashAddress", scriptPubKey: "scriptPubKey", index: 1, rawTokenQty: 10) 21 | token.addUTXOs([utxo]) 22 | 23 | expect(token._utxos.count).to(equal(1)) 24 | } 25 | } 26 | 27 | context("Add UTXO") { 28 | it("should success") { 29 | let token = SLPToken("test") 30 | 31 | let utxo = SLPTokenUTXO("txid", satoshis: 100, cashAddress: "cashAddress", scriptPubKey: "scriptPubKey", index: 1, rawTokenQty: 10) 32 | token.addUTXO(utxo) 33 | 34 | expect(token._utxos.count).to(equal(1)) 35 | } 36 | } 37 | 38 | context("Get balance without decimal") { 39 | it("should success") { 40 | let token = SLPToken("test") 41 | 42 | let utxo = SLPTokenUTXO("txid", satoshis: 100, cashAddress: "cashAddress", scriptPubKey: "scriptPubKey", index: 1, rawTokenQty: 10) 43 | utxo._isValid = true 44 | 45 | token.addUTXO(utxo) 46 | 47 | expect(token.getBalance()).to(equal(0)) 48 | } 49 | } 50 | 51 | context("Get balance with decimal nil") { 52 | it("should success") { 53 | let token = SLPToken("test") 54 | token._decimal = nil 55 | 56 | let utxo = SLPTokenUTXO("txid", satoshis: 100, cashAddress: "cashAddress", scriptPubKey: "scriptPubKey", index: 1, rawTokenQty: 10) 57 | utxo._isValid = true 58 | 59 | token.addUTXO(utxo) 60 | 61 | expect(token.getBalance()).to(equal(0)) 62 | } 63 | } 64 | 65 | context("Get balance with decimal 2") { 66 | it("should success") { 67 | let token = SLPToken("test") 68 | token._decimal = 2 69 | 70 | let utxo = SLPTokenUTXO("txid", satoshis: 100, cashAddress: "cashAddress", scriptPubKey: "scriptPubKey", index: 1, rawTokenQty: 10) 71 | utxo._isValid = true 72 | 73 | token.addUTXO(utxo) 74 | 75 | expect(token.getBalance()).to(equal(0.1)) 76 | } 77 | } 78 | 79 | context("Get balance with decimal 0") { 80 | it("should success") { 81 | let token = SLPToken("test") 82 | token._decimal = 0 83 | 84 | let utxo = SLPTokenUTXO("txid", satoshis: 100, cashAddress: "cashAddress", scriptPubKey: "scriptPubKey", index: 1, rawTokenQty: 10) 85 | utxo._isValid = true 86 | 87 | token.addUTXO(utxo) 88 | 89 | expect(token.getBalance()).to(equal(10)) 90 | } 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /SLPWallet.xcodeproj/xcshareddata/xcschemes/SLPWallet.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 32 | 33 | 39 | 40 | 41 | 42 | 44 | 50 | 51 | 52 | 53 | 54 | 60 | 61 | 62 | 63 | 64 | 65 | 75 | 76 | 82 | 83 | 84 | 85 | 86 | 87 | 93 | 94 | 100 | 101 | 102 | 103 | 105 | 106 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /SLPWalletTests/RestServiceTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RestServiceTest.swift 3 | // SLPWalletTests 4 | // 5 | // Created by Jean-Baptiste Dominguez on 2019/02/27. 6 | // Copyright © 2019 Bitcoin.com. All rights reserved. 7 | // 8 | 9 | import Nimble 10 | import Quick 11 | import RxBlocking 12 | @testable import SLPWallet 13 | 14 | class RestServiceTest: QuickSpec { 15 | override func spec() { 16 | describe("RestService") { 17 | 18 | beforeEach { 19 | SLPWalletConfig.setRestURL("https://rest.bitcoin.com/v2") 20 | } 21 | 22 | context("Fetch UTXO") { 23 | it("should success") { 24 | let utxos = try! RestService 25 | .fetchUTXOs(["bitcoincash:qzk92nt0xdxc9qy3yj53h9rjw8dk0s9cqqucfqpcd6"]) 26 | .toBlocking() 27 | .single() 28 | expect(utxos).notTo(beNil()) 29 | } 30 | } 31 | 32 | context("Fetch TxDetails") { 33 | it("should success") { 34 | let txs = try! RestService 35 | .fetchTxDetails(["ce7f87ac5d086ad1c736c472ce5bc75f020bf22d3e2ed8603c675a6517b9c1cd"]) 36 | .toBlocking() 37 | .single() 38 | expect(txs).notTo(beNil()) 39 | } 40 | 41 | it("should fail") { 42 | do { 43 | _ = try RestService 44 | .fetchTxDetails(["test"]) 45 | .toBlocking() 46 | .single() 47 | fail() 48 | } catch RestService.RestError.REST_TX_DETAILS { 49 | // Success 50 | } catch { 51 | fail() 52 | } 53 | } 54 | } 55 | 56 | context("Broadcast RawTx") { 57 | it("should fail") { 58 | do { 59 | _ = try RestService 60 | .broadcast("0100000001060f095464b748f3d383b677f0cd5c85807d4b2324412e2759b64706a72f42e3010000006b483045022100c22fb8802b7d539e8143a8b6f71cf4c0d1b496a5846d5f480277bd4360032f8b02204508d9304f5b62d0e29b07a13234cff2f5c1adc54fb34cc2d7207556127e184e41210329d5ffda1250d97614cfd3a5cb1c89d0a255c59584c091915b21b3e64137fe7affffffff040000000000000000406a04534c500001010453454e4420e3422fa70647b659272e4124234b7d80855ccdf077b683d3f348b76454090f060800000000000004b008000000000000002222020000000000001976a914ac554d6f334d82809124a91b947271db67c0b80088ac22020000000000001976a914ac554d6f334d82809124a91b947271db67c0b80088ac85470000000000001976a914ac554d6f334d82809124a91b947271db67c0b80088ac00000000") 61 | .toBlocking() 62 | .single() 63 | fail() 64 | } catch RestService.RestError.REST_SEND_RAW_TX { 65 | // Success 66 | } catch { 67 | fail() 68 | } 69 | } 70 | } 71 | 72 | context("Fetch TxDetails") { 73 | it("should success + valid") { 74 | let txValidations = try! RestService 75 | .fetchTxValidations(["7657b6eb3dbd13ceb0c02a027a44118ede354768689aebd8ebf7007e5a21ae42"]) 76 | .toBlocking() 77 | .single() 78 | expect(txValidations).notTo(beNil()) 79 | expect(txValidations.count).to(equal(1)) 80 | expect(txValidations.first?.txid).to(equal("7657b6eb3dbd13ceb0c02a027a44118ede354768689aebd8ebf7007e5a21ae42")) 81 | expect(txValidations.first?.valid).to(equal(true)) 82 | } 83 | 84 | it("should success + invalid") { 85 | let txValidations = try! RestService 86 | .fetchTxValidations(["b42876f55585019f588a39d24a664f8d93fba224e65eef2c1c1979f14069d102"]) 87 | .toBlocking() 88 | .single() 89 | expect(txValidations).notTo(beNil()) 90 | expect(txValidations.count).to(equal(1)) 91 | expect(txValidations.first?.txid).to(equal("b42876f55585019f588a39d24a664f8d93fba224e65eef2c1c1979f14069d102")) 92 | expect(txValidations.first?.valid).to(equal(false)) 93 | } 94 | 95 | it("should fail") { 96 | do { 97 | let txValidations = try RestService 98 | .fetchTxValidations(["test"]) 99 | .toBlocking() 100 | .single() 101 | fail() 102 | } catch { 103 | // success 104 | } 105 | } 106 | } 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /SLPWallet/Services/RestService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RestService.swift 3 | // SLPWallet 4 | // 5 | // Created by Jean-Baptiste Dominguez on 2019/02/26. 6 | // Copyright © 2019 Bitcoin.com. All rights reserved. 7 | // 8 | 9 | import Moya 10 | import RxSwift 11 | 12 | public class RestService { 13 | static let bag = DisposeBag() 14 | 15 | enum RestError: String, Error { 16 | case REST_UTXOS = "Failed to fetch UTXOs" 17 | case REST_TX_DETAILS = "Failed to fetch TX details" 18 | case REST_SEND_RAW_TX = "Failed to send TX" 19 | case REST_TX_VALIDATIONS = "Failed to validate TXs" 20 | } 21 | } 22 | 23 | // Fetch UTXOs 24 | // 25 | extension RestService { 26 | 27 | public struct ResponseUTXOs: Codable { 28 | public let utxos: [ResponseUTXO] 29 | public let scriptPubKey: String? 30 | } 31 | 32 | public struct ResponseUTXO: Codable { 33 | public let txid: String 34 | public let vout: Int 35 | public let satoshis: Int 36 | public let confirmations: Int 37 | } 38 | 39 | static public func fetchUTXOs(_ addresses: [String]) -> Single<[ResponseUTXOs]> { 40 | 41 | return Single<[ResponseUTXOs]>.create(subscribe: { (observer) -> Disposable in 42 | // Get a utxo 43 | // 44 | let provider = MoyaProvider() 45 | provider.rx 46 | .request(.fetchUTXOs(addresses)) 47 | .retry(3) 48 | .map([ResponseUTXOs].self) 49 | .asObservable() 50 | .subscribe { event in 51 | switch event { 52 | case .next(let utxos): 53 | observer(.success(utxos)) 54 | case .error( _): 55 | observer(.error(RestError.REST_UTXOS)) 56 | default: break 57 | } 58 | } 59 | .disposed(by: RestService.bag) 60 | return Disposables.create() 61 | }) 62 | } 63 | } 64 | 65 | // Fetch TxDetails 66 | // 67 | extension RestService { 68 | 69 | public struct ResponseTx: Codable { 70 | public let txid: String 71 | public let vin: [ResponseInput] 72 | public let vout: [ResponseOutput] 73 | public let confirmations: Int 74 | public let time: Int 75 | public let fees: Double 76 | 77 | public struct ResponseInput: Codable { 78 | public let cashAddress: String 79 | public let value: Int 80 | } 81 | 82 | public struct ResponseOutput: Codable { 83 | public let value: String 84 | public let n: Int 85 | public let scriptPubKey: ResponseScriptPubKey 86 | } 87 | 88 | public struct ResponseScriptPubKey: Codable { 89 | public let addresses: [String]? 90 | public let hex: String 91 | public let asm: String 92 | } 93 | } 94 | 95 | public static func fetchTxDetails(_ txids: [String]) -> Single<[ResponseTx]> { 96 | return Single<[ResponseTx]>.create(subscribe: { (observer) -> Disposable in 97 | // Get tx details 98 | // 99 | let provider = MoyaProvider() 100 | provider.rx 101 | .request(.fetchTxDetails(txids)) 102 | .retry(3) 103 | .map([ResponseTx].self) 104 | .asObservable() 105 | .subscribe { event in 106 | switch event { 107 | case .next(let txs): 108 | observer(.success(txs)) 109 | case .error( _): 110 | observer(.error(RestError.REST_TX_DETAILS)) 111 | default: break 112 | } 113 | } 114 | .disposed(by: RestService.bag) 115 | return Disposables.create() 116 | }) 117 | } 118 | } 119 | 120 | // broadcast 121 | // 122 | extension RestService { 123 | 124 | public static func broadcast(_ rawTx: String) -> Single { 125 | return Single.create(subscribe: { (observer) -> Disposable in 126 | let provider = MoyaProvider() 127 | provider.rx 128 | .request(.broadcast(rawTx)) 129 | .retry(3) 130 | .asObservable() 131 | .subscribe { event in 132 | switch event { 133 | case .next(let response): 134 | guard let json = try? response.mapJSON() 135 | , let txid = json as? String 136 | , response.statusCode == 200 else { 137 | observer(.error(RestError.REST_SEND_RAW_TX)) 138 | return 139 | } 140 | observer(.success(txid)) 141 | case .error( _): 142 | observer(.error(RestError.REST_SEND_RAW_TX)) 143 | default: break 144 | } 145 | } 146 | .disposed(by: RestService.bag) 147 | return Disposables.create() 148 | }) 149 | } 150 | } 151 | 152 | // ValidateTxs 153 | // 154 | extension RestService { 155 | 156 | public struct ResponseTxValidation: Codable { 157 | public let txid: String 158 | public let valid: Bool 159 | } 160 | 161 | public static func fetchTxValidations(_ txIds: [String]) -> Single<[ResponseTxValidation]> { 162 | return Single<[ResponseTxValidation]>.create(subscribe: { (observer) -> Disposable in 163 | let provider = MoyaProvider() 164 | provider.rx 165 | .request(.fetchTxValidations(txIds)) 166 | .retry(3) 167 | .map([ResponseTxValidation].self) 168 | .asObservable() 169 | .subscribe { event in 170 | switch event { 171 | case .next(let txValidations): 172 | observer(.success(txValidations)) 173 | case .error( _): 174 | observer(.error(RestError.REST_TX_VALIDATIONS)) 175 | default: break 176 | } 177 | } 178 | .disposed(by: RestService.bag) 179 | return Disposables.create() 180 | }) 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /SLPWalletTests/SLPWalletTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SLPWalletTest.swift 3 | // SLPWalletTests 4 | // 5 | // Created by Jean-Baptiste Dominguez on 2019/02/28. 6 | // Copyright © 2019 Bitcoin.com. All rights reserved. 7 | // 8 | 9 | import Nimble 10 | import Quick 11 | import RxBlocking 12 | @testable import SLPWallet 13 | 14 | class SLPWalletTest: QuickSpec { 15 | override func spec() { 16 | 17 | describe("SLPWallet") { 18 | 19 | beforeEach { 20 | SLPWalletConfig.setRestURL("https://rest.bitcoin.com/v2") 21 | } 22 | 23 | context("Create wallet") { 24 | it("should success") { 25 | let wallet = try! SLPWallet("machine cannon man rail best deliver draw course time tape violin tone", network: .mainnet) 26 | 27 | expect(wallet.mnemonic).to(equal(["machine", "cannon", "man", "rail", "best", "deliver", "draw", "course", "time", "tape", "violin", "tone"])) 28 | expect(wallet.cashAddress).to(equal("bitcoincash:qzd5sk803xqxlmcs6yfwtpwzesq75s5m9c3x6gjl8n")) 29 | expect(wallet.slpAddress).to(equal("simpleledger:qzk92nt0xdxc9qy3yj53h9rjw8dk0s9cqqsrzm5cny")) 30 | expect(wallet.tokens.values.count).to(equal(0)) 31 | expect(wallet.getGas()).to(equal(0)) 32 | expect(wallet.getPrivKeyByCashAddress("bitcoincash:qzd5sk803xqxlmcs6yfwtpwzesq75s5m9c3x6gjl8n")).toNot(beNil()) 33 | expect(wallet.getPrivKeyByCashAddress("bitcoincash:qzk92nt0xdxc9qy3yj53h9rjw8dk0s9cqqucfqpcd6")).toNot(beNil()) 34 | expect(wallet.getPrivKeyByCashAddress("test")).to(beNil()) 35 | 36 | wallet.scheduler.resume() 37 | wallet.schedulerInterval = 1 38 | 39 | expect(wallet.schedulerInterval).to(equal(1)) 40 | } 41 | } 42 | 43 | context("Fetch tokens") { 44 | it("should success") { 45 | let wallet = try! SLPWallet("machine cannon man rail best deliver draw course time tape violin tone", network: .mainnet) 46 | var tokens = try! wallet 47 | .fetchTokens() 48 | .toBlocking() 49 | .single() 50 | 51 | tokens.forEach { tokenId, token in 52 | expect(token.tokenId).toNot(beNil()) 53 | expect(token.tokenTicker).toNot(beNil()) 54 | expect(token.tokenName).toNot(beNil()) 55 | expect(token.decimal).toNot(beNil()) 56 | expect(token.getBalance()).toNot(beNil()) 57 | expect(token.getGas()).toNot(beNil()) 58 | } 59 | 60 | // Fetch a second time to parse utxos 61 | try! wallet 62 | .fetchTokens() 63 | .toBlocking() 64 | .single() 65 | } 66 | } 67 | 68 | context("Add token") { 69 | it("should success") { 70 | let wallet = try! SLPWallet("machine cannon man rail best deliver draw course time tape violin tone", network: .mainnet) 71 | let token = SLPToken("ce7f87ac5d086ad1c736c472ce5bc75f020bf22d3e2ed8603c675a6517b9c1cd") 72 | let newToken = try! wallet 73 | .addToken(token) 74 | .toBlocking() 75 | .single() 76 | 77 | expect(newToken.tokenTicker).to(equal("BCC")) 78 | expect(newToken.tokenName).to(equal("Bitcoin.com Coin")) 79 | expect(newToken.decimal).to(equal(2)) 80 | expect(newToken.getBalance()).toNot(beNil()) 81 | expect(newToken.getGas()).toNot(beNil()) 82 | } 83 | } 84 | 85 | context("Secure storage") { 86 | it("should success") { 87 | let createdWallet = try! SLPWallet(.mainnet, force: true) 88 | let restoredWallet = try! SLPWallet(.mainnet) 89 | expect(restoredWallet.cashAddress).to(equal(createdWallet.cashAddress)) 90 | expect(restoredWallet.slpAddress).to(equal(createdWallet.slpAddress)) 91 | } 92 | } 93 | 94 | context("Send token") { 95 | it("should success") { 96 | let wallet = try! SLPWallet("machine cannon man rail best deliver draw course time tape violin tone", network: .mainnet) 97 | 98 | let token = SLPToken("e3422fa70647b659272e4124234b7d80855ccdf077b683d3f348b76454090f06") 99 | token._decimal = 2 100 | 101 | let utxo = SLPTokenUTXO("e3422fa70647b659272e4124234b7d80855ccdf077b683d3f348b76454090f06", satoshis: 20000, cashAddress: "bitcoincash:qzk92nt0xdxc9qy3yj53h9rjw8dk0s9cqqucfqpcd6", scriptPubKey: "483045022100e36b594680823bcf7f4a872611cb7652032e92793f2eadae9ad87a57e4854e3602203ffb057332f47bf9f68738f668acad8ae1d2d3265c34e75c9158c9e9be2ae1f0412103b8ac3da9a09a58444291ce21c68a6b279fe33d3e46a879a4c1ed64bd87146506", index: 1, rawTokenQty: 1234) 102 | utxo._isValid = true 103 | 104 | token.addUTXO(utxo) 105 | 106 | wallet._tokens["e3422fa70647b659272e4124234b7d80855ccdf077b683d3f348b76454090f06"] = token 107 | 108 | do { 109 | let value = try SLPTransactionBuilder.build(wallet, tokenId: "e3422fa70647b659272e4124234b7d80855ccdf077b683d3f348b76454090f06", amount: 12, toAddress: "simpleledger:qqs5mxuxr9kaukncpgdc7z64zp6t87rk7cwtkvhpjv") 110 | 111 | wallet.updateUTXOsAfterSending(token, usedUTXOs: value.usedUTXOs, newUTXOs: value.newUTXOs) 112 | expect(wallet.getGas()).to(be(18385)) 113 | expect(wallet._tokens["e3422fa70647b659272e4124234b7d80855ccdf077b683d3f348b76454090f06"]?.getBalance()).to(equal(0.34)) 114 | } catch { 115 | fail() 116 | } 117 | } 118 | } 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Logo](github_logo.png) 2 | 3 | # SLPWallet iOS SDK 4 | 5 | [![Build Status](https://travis-ci.com/Bitcoin-com/slp-wallet-sdk-ios.svg?branch=master)](https://travis-ci.com/Bitcoin-com/slp-wallet-sdk-ios) 6 | [![codecov](https://codecov.io/gh/bitcoin-com/slp-wallet-sdk-ios/branch/master/graph/badge.svg?token=FRvZH4tttT)](https://codecov.io/gh/bitcoin-com/slp-wallet-sdk-ios) 7 | [![Version](https://img.shields.io/badge/pod-v0.1.0-blue.svg)](https://github.com/Bitcoin-com/CocoaPods/tree/master/SLPWallet/0.1.0) 8 | ![Platform](https://img.shields.io/badge/platform-ios-black.svg) 9 | ![Compatibility](https://img.shields.io/badge/iOS-+10.0-orange.svg) 10 | ![Compatibility](https://img.shields.io/badge/Swift-4.0-orange.svg) 11 | ![License](https://img.shields.io/badge/License-MIT-lightgrey.svg) 12 | 13 | ## Installation 14 | 15 | ### CocoaPods 16 | 17 | #### Podfile 18 | 19 | ```ruby 20 | # Add our BitcoinKit fork that handles SLP address 21 | source 'https://github.com/Bitcoin-com/CocoaPods.git' 22 | source 'https://github.com/CocoaPods/Specs.git' 23 | 24 | platform :ios, '10.0' 25 | 26 | target 'SLPWalletTestApp' do 27 | use_frameworks! 28 | 29 | # Pods for SLPWalletTestApp 30 | pod 'SLPWallet' 31 | 32 | end 33 | ``` 34 | #### Commands 35 | 36 | ```bash 37 | $ brew install autoconf automake // Required with BitcoinKit 38 | $ brew install libtool // Required with BitcoinKit 39 | $ pod install 40 | ``` 41 | 42 | #### Pod install issue 43 | 44 | ```bash 45 | sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer/ 46 | ``` 47 | 48 | ### Configuration 49 | 50 | SLPWallet uses the Keychain to safely store the mnemonic seed phrase on your device. However, you need to create an entitlement file to allow the access to the Keychain. You can have a look at the sample project anytime you need to check the configuration [here.](./Sample/SLPWalletDemo/) 51 | 52 | Under the hood, the SDK is using [KeychainAccess](https://github.com/kishikawakatsumi/KeychainAccess). 53 | 54 | ```xml 55 | 56 | 57 | 58 | 59 | keychain-access-groups 60 | 61 | $(AppIdentifierPrefix)your.bundle.id 62 | 63 | 64 | 65 | ``` 66 | 67 | ## Get Started 68 | 69 | ### Setup URL + API Key (Not required :warning:, nice to have :dash:) 70 | 71 | The SDK uses https://rest.bitcoin.com which by default is limited to up to 60 calls per minute per IP address. If you would like to increase your REST calls limit rate, please contact [Bitcoin.com's team](https://developer.bitcoin.com/rest/) to obtain an API key. You may configure the SDK to work with an API key or with your own REST API, as shown below: 72 | 73 | Add your setup to your ```AppDelegate.swift``` as follows: 74 | 75 | 1. Add the following import statement: 76 | 77 | ```Swift 78 | Import SLPWallet 79 | ``` 80 | 81 | 2. Setup in the ```application(_:didFinishLaunchingWithOptions:)``` 82 | 83 | ```Swift 84 | // Optional setup 85 | SLPWalletConfig.setRestAPIKey("MY_API_KEY") // Optional 86 | SLPWalletConfig.setRestURL("https://rest.bitcoin.com") // By default => https://rest.bitcoin.com 87 | ``` 88 | 89 | ### Creating new wallet with/without mnemonic 90 | 91 | The wallet works with only 2 addresses, using: 92 | - the SLP recommended path 44'/245'/0' + m/0/0 (handling tokens - bch with token + token change address) 93 | - the BCH recommended path 44'/145'/0' + m/0/0 (handling gas - bch without token + bch change address) 94 | 95 | However, both paths are scanned to get any bch or tokens available. 96 | 97 | ```swift 98 | // Init 1 99 | // Generate/Restore a wallet + Save/Get in Keychain 100 | // If mnemonic in Keychain 101 | // Restore wallet 102 | // else 103 | // Generate mnemonic 104 | let wallet = try SLPWallet(.testnet) // .mainnet or .testnet 105 | 106 | // Init 2 107 | // Restore a wallet from Mnemonic + Save in Keychain 108 | let wallet = try SLPWallet("My Mnemonic", network: .testnet) // .mainnet or .testnet 109 | 110 | // Init 3 111 | // Generate a wallet 112 | // If force == true 113 | // Generate everytime a new wallet 114 | // else 115 | // => Init 1 116 | let wallet = try SLPWallet(.testnet, force: Bool) // .mainnet or .testnet 117 | ``` 118 | 119 | ### Addresses + tokens 120 | 121 | ```swift 122 | wallet.mnemonic // [String] 123 | wallet.slpAddress // String 124 | wallet.cashAddress // String 125 | wallet.tokens // [String:SLPToken] Tokens are accessible after an initial fetch or if you have started the scheduler 126 | ``` 127 | ### Fetch my tokens 128 | 129 | ```swift 130 | wallet 131 | .fetchTokens() // RxSwift => Single<[String:Token]> 132 | .subscribe(onSuccess: { tokens in 133 | // My tokens 134 | tokens.forEach { tokenId, token in 135 | token.tokenId 136 | token.tokenName 137 | token.tokenTicker 138 | token.decimal 139 | token.getBalance() 140 | token.getGas() 141 | } 142 | }, onError: { error in 143 | // ... 144 | }) 145 | ``` 146 | ### Send token 147 | 148 | ```swift 149 | wallet 150 | .sendToken(tokenId, amount: amount, toAddress: toAddress) // toAddress can be a slp / cash address or legacy 151 | .subscribe(onSuccess: { txid in // RxSwift => Single 152 | // ... 153 | }, onError: { error in 154 | // ... 155 | }) 156 | ``` 157 | ### Auto update wallet/tokens (balances + gas) 158 | 159 | ```swift 160 | // Start & Stop 161 | wallet.scheduler.resume() 162 | wallet.scheduler.suspend() 163 | 164 | // Change the interval 165 | wallet.schedulerInterval = 10 // in seconds (30 by default) 166 | ``` 167 | 168 | ### WalletDelegate called when : 169 | + scheduler is started + token balance changed 170 | 171 | ```swift 172 | class MyViewController: SLPWalletDelegate { 173 | 174 | 175 | override func viewDidLoad() { 176 | super.viewDidLoad() 177 | 178 | let wallet = ... // Setup a wallet 179 | wallet.delegate = self 180 | } 181 | 182 | func onUpdatedToken(_ token: SLPToken) { 183 | // My updated token 184 | token.tokenId 185 | token.tokenName 186 | token.tokenTicker 187 | token.decimal 188 | token.getBalance() 189 | token.getGas() 190 | } 191 | } 192 | ``` 193 | 194 | ## Sample Project 195 | 196 | [iOS project developed with SLPWallet SDK](https://github.com/Bitcoin-com/slp-wallet-sdk-ios-demo) 197 | 198 | ![Alt Text](https://github.com/Bitcoin-com/slp-wallet-sdk-ios-demo/raw/master/demo-app.gif) 199 | 200 | ## Authors & Maintainers 201 | - Jean-Baptiste Dominguez [[Github](https://github.com/jbdtky), [Twitter](https://twitter.com/jbdtky)] 202 | 203 | ## References 204 | - [BIP44](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki) 205 | - [Simple Ledger Protocol (SLP)](https://github.com/simpleledger/slp-specifications/blob/master/slp-token-type-1.md) 206 | 207 | ## Credits 208 | - [KeychainAccess](https://github.com/kishikawakatsumi/KeychainAccess) 209 | - [RxSwift](https://github.com/ReactiveX/RxSwift) 210 | - [Moya](https://github.com/Moya/Moya) 211 | - [BitcoinKit](https://github.com/Bitcoin-com/BitcoinKit) 212 | - [Kishikawa Katsumi](https://github.com/kishikawakatsumi) for BitcoinKit + KeychainAccess 213 | 214 | ## License 215 | 216 | SLPWallet iOS SDK is available under the MIT license. See the LICENSE file for more info. 217 | -------------------------------------------------------------------------------- /SLPWallet/Utils/SLPTransactionParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SLPTransactionParser.swift 3 | // SLPWallet 4 | // 5 | // Created by Jean-Baptiste Dominguez on 2019/03/01. 6 | // Copyright © 2019 Bitcoin.com. All rights reserved. 7 | // 8 | 9 | // TODO: Move all the parsering here to clean SLPWallet 10 | // 11 | 12 | import BitcoinKit 13 | 14 | enum SLPTransactionType: String { 15 | case GENESIS 16 | case SEND 17 | case MINT 18 | } 19 | 20 | struct SLPTransactionParserResponse { 21 | var token: SLPToken 22 | var utxos: [SLPWalletUTXO] 23 | } 24 | 25 | class SLPTransactionParser { 26 | 27 | static func parse(_ tx: RestService.ResponseTx, vouts: [Int]) -> SLPTransactionParserResponse? { 28 | 29 | let parsedToken = SLPToken() 30 | var parsedUTXOs = [SLPWalletUTXO]() 31 | 32 | // TODO: Parse the tx in another place 33 | let script = Script(hex: tx.vout[0].scriptPubKey.hex) 34 | 35 | var voutToTokenQty = [Int]() 36 | voutToTokenQty.append(0) // To have the same mapping with the vouts 37 | 38 | var mintVout = 0 39 | 40 | if var chunks = script?.scriptChunks 41 | , chunks.removeFirst().opCode == .OP_RETURN { 42 | 43 | // 0 : lokad id 4 bytes ASCII 44 | // Good 45 | guard let lokadId = chunks[0].chunkData.removeLeft().removeRight().stringASCII else { 46 | return nil 47 | } 48 | 49 | if lokadId == "SLP" { 50 | 51 | // 1 : token_type 1 bytes Integer 52 | // Good 53 | var chunk = chunks[1].chunkData.removeLeft() 54 | let tokenType = chunk.uint8 // Unused for now 55 | 56 | // 2 : transaction_type 4 bytes ASCII 57 | // Good 58 | chunk = chunks[2].chunkData.removeLeft() 59 | 60 | guard let transactionType = chunks[2].chunkData.removeLeft().stringASCII else { 61 | return nil 62 | } 63 | 64 | switch transactionType { 65 | case SLPTransactionType.GENESIS.rawValue: 66 | 67 | // Genesis => Txid 68 | let tokenId = tx.txid 69 | parsedToken._tokenId = tokenId 70 | 71 | // 3 : token_ticker UTF8 72 | // Good 73 | chunk = chunks[3].chunkData.removeLeft() 74 | guard let tokenTicker = chunk.stringUTF8 else { 75 | return nil 76 | } 77 | parsedToken._tokenTicker = tokenTicker 78 | 79 | // 4 : token_name UTF8 80 | // Good 81 | chunk = chunks[4].chunkData.removeLeft() 82 | guard let tokenName = chunk.stringUTF8 else { 83 | return nil 84 | } 85 | parsedToken._tokenName = tokenName 86 | 87 | // 7 : decimal 1 Byte 88 | // Good 89 | chunk = chunks[7].chunkData.removeLeft() 90 | guard let decimal = Int(chunk.hex, radix: 16) else { 91 | return nil 92 | } 93 | parsedToken._decimal = decimal 94 | 95 | // 8 : Mint 2 Bytes 96 | // Good 97 | chunk = chunks[8].chunkData.removeLeft() 98 | if let mv = Int(chunk.hex, radix: 16) { 99 | mintVout = mv 100 | } 101 | 102 | // 9 to .. : initial_token_mint_quantity 8 Bytes 103 | // Good 104 | chunk = chunks[9].chunkData.removeLeft() 105 | if let balance = Int(chunk.hex, radix: 16) { 106 | voutToTokenQty.append(balance) 107 | } 108 | 109 | case SLPTransactionType.SEND.rawValue: 110 | 111 | // 3 : token_id 32 bytes hex 112 | // Good 113 | chunk = chunks[3].chunkData.removeLeft() 114 | let tokenId = chunk.hex 115 | parsedToken._tokenId = tokenId 116 | 117 | // 4 to .. : token_output_quantity 1..19 8 Bytes / qty 118 | for i in 4...chunks.count - 1 { 119 | chunk = chunks[i].chunkData.removeLeft() 120 | if let balance = Int(chunk.hex, radix: 16) { 121 | voutToTokenQty.append(balance) 122 | } else { 123 | break 124 | } 125 | } 126 | case SLPTransactionType.MINT.rawValue: 127 | 128 | // 3 : token_id 32 bytes hex 129 | // Good 130 | chunk = chunks[3].chunkData.removeLeft() 131 | let tokenId = chunk.hex 132 | parsedToken._tokenId = tokenId 133 | 134 | // 4 : Mint 2 Bytes 135 | // Good 136 | chunk = chunks[4].chunkData.removeLeft() 137 | if let mv = Int(chunk.hex, radix: 16) { 138 | mintVout = mv 139 | } 140 | 141 | // 5 : additional_token_quantity 8 Bytes 142 | // Good 143 | chunk = chunks[5].chunkData.removeLeft() 144 | if let balance = Int(chunk.hex, radix: 16) { 145 | voutToTokenQty.append(balance) 146 | } 147 | default: break 148 | } 149 | } 150 | } 151 | 152 | // Get the vouts that we are interested in 153 | vouts.forEach { i in 154 | let vout = tx.vout[i] 155 | 156 | guard let rawAddress = vout.scriptPubKey.addresses?.first 157 | , let address = try? AddressFactory.create(rawAddress) else { 158 | return 159 | } 160 | 161 | let cashAddress = address.cashaddr 162 | 163 | guard vout.n < voutToTokenQty.count 164 | , voutToTokenQty.count > 1 165 | , voutToTokenQty[vout.n] > 0 else { // Because we push 1 vout qty by default for the OP_RETURN 166 | 167 | // We need to avoid using the mint baton 168 | if vout.n == mintVout && mintVout > 0 { 169 | // UTXO with baton 170 | parsedToken._mintUTXO = SLPWalletUTXO(tx.txid, satoshis: vout.value.toSatoshis(), cashAddress: cashAddress, scriptPubKey: vout.scriptPubKey.hex, index: vout.n) 171 | } else { 172 | // UTXO without token 173 | let utxo = SLPWalletUTXO(tx.txid, satoshis: vout.value.toSatoshis(), cashAddress: cashAddress, scriptPubKey: vout.scriptPubKey.hex, index: vout.n) 174 | parsedUTXOs.append(utxo) 175 | } 176 | 177 | return 178 | } 179 | 180 | // UTXO with a token 181 | let rawTokenQty = voutToTokenQty[vout.n] 182 | let tokenUTXO = SLPTokenUTXO(tx.txid, satoshis: vout.value.toSatoshis(), cashAddress: cashAddress, scriptPubKey: vout.scriptPubKey.hex, index: vout.n, rawTokenQty: rawTokenQty) 183 | parsedToken.addUTXO(tokenUTXO) 184 | } 185 | 186 | return SLPTransactionParserResponse(token: parsedToken, utxos: parsedUTXOs) 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /SLPWalletTests/SLPTransactionParserTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SLPTransactionParserTest.swift 3 | // SLPWalletTests 4 | // 5 | // Created by Jean-Baptiste Dominguez on 2019/03/25. 6 | // Copyright © 2019 Bitcoin.com. All rights reserved. 7 | // 8 | 9 | import Nimble 10 | import Quick 11 | @testable import SLPWallet 12 | 13 | class SLPTransactionParserTest: QuickSpec { 14 | override func spec() { 15 | describe("SLPTransactionParser") { 16 | context("Parse a transaction GENESIS") { 17 | 18 | it("should success") { 19 | let path = Bundle(for: type(of: self)).path(forResource: "tx_details_genesis_tst", ofType: "json") 20 | let url = URL(fileURLWithPath: path!) 21 | 22 | let data = try! Data(contentsOf: url) 23 | let tx = try! JSONDecoder().decode(RestService.ResponseTx.self, from: data) 24 | 25 | guard let parsedData = SLPTransactionParser.parse(tx, vouts: [1, 2]) else { 26 | fail() 27 | return 28 | } 29 | 30 | // Token 31 | expect(parsedData.token.tokenId).to(equal("9cc1cf24e502554d2d3d09918c27decda2c260762961acd469c5473fbcfe192e")) 32 | expect(parsedData.token.tokenName).to(equal("Try! Swift Token")) 33 | expect(parsedData.token.tokenTicker).to(equal("TST")) 34 | expect(parsedData.token.decimal).to(equal(2)) 35 | 36 | // Token UTXOs 37 | expect(parsedData.token._utxos.first).toNot(beNil()) 38 | 39 | guard let utxo = parsedData.token._utxos.first else { 40 | fail() 41 | return 42 | } 43 | 44 | expect(utxo.txid).to(equal("9cc1cf24e502554d2d3d09918c27decda2c260762961acd469c5473fbcfe192e")) 45 | expect(utxo.rawTokenQty).to(equal(100000000000)) 46 | expect(utxo.satoshis).to(equal(546)) 47 | expect(utxo.cashAddress).to(equal("bitcoincash:qp4zxnnce2wy8ackzc29wtjp2xw9e6sgyvuv77vvmh")) 48 | expect(utxo.index).to(equal(1)) 49 | 50 | // Baton 51 | expect(parsedData.token.mintUTXO).toNot(beNil()) 52 | expect(parsedData.token.mintUTXO?.txid).to(equal("9cc1cf24e502554d2d3d09918c27decda2c260762961acd469c5473fbcfe192e")) 53 | expect(parsedData.token.mintUTXO?.cashAddress).to(equal("bitcoincash:qp4zxnnce2wy8ackzc29wtjp2xw9e6sgyvuv77vvmh")) 54 | expect(parsedData.token.mintUTXO?.satoshis).to(equal(546)) 55 | expect(parsedData.token.mintUTXO?.index).to(equal(2)) 56 | 57 | // UTXOs 58 | expect(parsedData.utxos.count).to(equal(0)) 59 | } 60 | } 61 | } 62 | 63 | context("Parse a transaction SEND") { 64 | 65 | it("should success") { 66 | let path = Bundle(for: type(of: self)).path(forResource: "tx_details_send_tst", ofType: "json") 67 | let url = URL(fileURLWithPath: path!) 68 | 69 | let data = try! Data(contentsOf: url) 70 | let tx = try! JSONDecoder().decode(RestService.ResponseTx.self, from: data) 71 | 72 | guard let parsedData = SLPTransactionParser.parse(tx, vouts: [2, 3]) else { 73 | fail() 74 | return 75 | } 76 | 77 | // Token 78 | expect(parsedData.token.tokenId).to(equal("9cc1cf24e502554d2d3d09918c27decda2c260762961acd469c5473fbcfe192e")) 79 | expect(parsedData.token.tokenName).to(beNil()) 80 | expect(parsedData.token.tokenTicker).to(beNil()) 81 | expect(parsedData.token.decimal).to(beNil()) 82 | 83 | // Token UTXOs 84 | expect(parsedData.token._utxos.first).toNot(beNil()) 85 | 86 | guard let tokenUTXO = parsedData.token._utxos.first else { 87 | fail() 88 | return 89 | } 90 | 91 | expect(tokenUTXO.txid).to(equal("a9f639148662ca6376c3650f3d7e6dffbe9a477cf947499bfcb2c85412331c2e")) 92 | expect(tokenUTXO.rawTokenQty).to(equal(15080066)) 93 | expect(tokenUTXO.satoshis).to(equal(546)) 94 | expect(tokenUTXO.cashAddress).to(equal("bitcoincash:qzdwxdtprwxhynpvcqngtupp3tn58smte5z2yfqe0v")) 95 | expect(tokenUTXO.index).to(equal(2)) 96 | 97 | // Baton 98 | expect(parsedData.token.mintUTXO).to(beNil()) 99 | 100 | // UTXOs 101 | expect(parsedData.utxos.first).toNot(beNil()) 102 | 103 | guard let utxo = parsedData.utxos.first else { 104 | fail() 105 | return 106 | } 107 | 108 | expect(utxo.txid).to(equal("a9f639148662ca6376c3650f3d7e6dffbe9a477cf947499bfcb2c85412331c2e")) 109 | expect(utxo.satoshis).to(equal(192966)) 110 | expect(utxo.cashAddress).to(equal("bitcoincash:qqga5ljshfug5g27532d9xc0w55yxdjzwygw62c0nk")) 111 | expect(utxo.index).to(equal(3)) 112 | } 113 | } 114 | 115 | context("Parse a transaction MINT") { 116 | 117 | it("should success") { 118 | let path = Bundle(for: type(of: self)).path(forResource: "tx_details_mint_lvl001", ofType: "json") 119 | let url = URL(fileURLWithPath: path!) 120 | 121 | let data = try! Data(contentsOf: url) 122 | let tx = try! JSONDecoder().decode(RestService.ResponseTx.self, from: data) 123 | 124 | guard let parsedData = SLPTransactionParser.parse(tx, vouts: [1, 2]) else { 125 | fail() 126 | return 127 | } 128 | 129 | // Token 130 | expect(parsedData.token.tokenId).to(equal("d5efb237f43a822ede2086bbefca44f1157b7adf2ddeed87c4b294bd136d1d36")) 131 | expect(parsedData.token.tokenName).to(beNil()) 132 | expect(parsedData.token.tokenTicker).to(beNil()) 133 | expect(parsedData.token.decimal).to(beNil()) 134 | 135 | // Token UTXOs 136 | expect(parsedData.token._utxos.first).toNot(beNil()) 137 | 138 | guard let tokenUTXO = parsedData.token._utxos.first else { 139 | fail() 140 | return 141 | } 142 | 143 | expect(tokenUTXO.txid).to(equal("c3b72361cee1a7ed5d0911714da7439313eaf22fde842f871656e4d438eba7d1")) 144 | expect(tokenUTXO.rawTokenQty).to(equal(1)) 145 | expect(tokenUTXO.satoshis).to(equal(546)) 146 | expect(tokenUTXO.cashAddress).to(equal("bitcoincash:qzsn8qeupph6pf8kyn2x79afff7pygzfvqnjwvhmzm")) 147 | expect(tokenUTXO.index).to(equal(1)) 148 | 149 | // Baton 150 | expect(parsedData.token.mintUTXO).toNot(beNil()) 151 | expect(parsedData.token.mintUTXO?.txid).to(equal("c3b72361cee1a7ed5d0911714da7439313eaf22fde842f871656e4d438eba7d1")) 152 | expect(parsedData.token.mintUTXO?.cashAddress).to(equal("bitcoincash:qrgwrx7hvd27jqz8q5fmgr4kg5cy76yp05a3prllmr")) 153 | expect(parsedData.token.mintUTXO?.satoshis).to(equal(546)) 154 | expect(parsedData.token.mintUTXO?.index).to(equal(2)) 155 | 156 | // UTXOs 157 | expect(parsedData.utxos.first).to(beNil()) 158 | } 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /SLPWalletTests/SLPTransactionBuilderTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SLPTransactionBuilderTest.swift 3 | // SLPWalletTests 4 | // 5 | // Created by Jean-Baptiste Dominguez on 2019/03/12. 6 | // Copyright © 2019 Bitcoin.com. All rights reserved. 7 | // 8 | 9 | import Nimble 10 | import Quick 11 | @testable import SLPWallet 12 | 13 | class SLPTransactionBuilderTest: QuickSpec { 14 | override func spec() { 15 | describe("SLPTransactionBuilder") { 16 | context("Build a transaction") { 17 | 18 | it("should fail TOKEN_NOT_FOUND") { 19 | let wallet = try! SLPWallet("machine cannon man rail best deliver draw course time tape violin tone", network: .mainnet) 20 | 21 | do { 22 | _ = try SLPTransactionBuilder.build(wallet, tokenId: "test", amount: 12, toAddress: "simpleledger:qzk92nt0xdxc9qy3yj53h9rjw8dk0s9cqqsrzm5cny") 23 | fail() 24 | } catch SLPTransactionBuilderError.TOKEN_NOT_FOUND { 25 | // Success 26 | } catch { 27 | fail() 28 | } 29 | } 30 | 31 | it("should fail DECIMAL_NOT_AVAILABLE") { 32 | let wallet = try! SLPWallet("machine cannon man rail best deliver draw course time tape violin tone", network: .mainnet) 33 | 34 | let token = SLPToken("e3422fa70647b659272e4124234b7d80855ccdf077b683d3f348b76454090f06") 35 | 36 | let utxo = SLPTokenUTXO("e3422fa70647b659272e4124234b7d80855ccdf077b683d3f348b76454090f06", satoshis: 1000, cashAddress: "bitcoincash:qzk92nt0xdxc9qy3yj53h9rjw8dk0s9cqqucfqpcd6", scriptPubKey: "483045022100e36b594680823bcf7f4a872611cb7652032e92793f2eadae9ad87a57e4854e3602203ffb057332f47bf9f68738f668acad8ae1d2d3265c34e75c9158c9e9be2ae1f0412103b8ac3da9a09a58444291ce21c68a6b279fe33d3e46a879a4c1ed64bd87146506", index: 1, rawTokenQty: 1234) 37 | token.addUTXO(utxo) 38 | 39 | wallet._tokens["e3422fa70647b659272e4124234b7d80855ccdf077b683d3f348b76454090f06"] = token 40 | 41 | do { 42 | _ = try SLPTransactionBuilder.build(wallet, tokenId: "e3422fa70647b659272e4124234b7d80855ccdf077b683d3f348b76454090f06", amount: 12, toAddress: "simpleledger:qzk92nt0xdxc9qy3yj53h9rjw8dk0s9cqqsrzm5cny") 43 | fail() 44 | } catch SLPTransactionBuilderError.DECIMAL_NOT_AVAILABLE { 45 | // Success 46 | } catch { 47 | fail() 48 | } 49 | } 50 | 51 | it("should fail INSUFFICIENT_FUNDS") { 52 | let wallet = try! SLPWallet("machine cannon man rail best deliver draw course time tape violin tone", network: .mainnet) 53 | 54 | let token = SLPToken("e3422fa70647b659272e4124234b7d80855ccdf077b683d3f348b76454090f06") 55 | token._decimal = 0 56 | 57 | let utxo = SLPTokenUTXO("e3422fa70647b659272e4124234b7d80855ccdf077b683d3f348b76454090f06", satoshis: 1000, cashAddress: "bitcoincash:qzk92nt0xdxc9qy3yj53h9rjw8dk0s9cqqucfqpcd6", scriptPubKey: "483045022100e36b594680823bcf7f4a872611cb7652032e92793f2eadae9ad87a57e4854e3602203ffb057332f47bf9f68738f668acad8ae1d2d3265c34e75c9158c9e9be2ae1f0412103b8ac3da9a09a58444291ce21c68a6b279fe33d3e46a879a4c1ed64bd87146506", index: 1, rawTokenQty: 1234) 58 | utxo._isValid = true 59 | 60 | token.addUTXO(utxo) 61 | 62 | wallet._tokens["e3422fa70647b659272e4124234b7d80855ccdf077b683d3f348b76454090f06"] = token 63 | 64 | do { 65 | _ = try SLPTransactionBuilder.build(wallet, tokenId: "e3422fa70647b659272e4124234b7d80855ccdf077b683d3f348b76454090f06", amount: 1235, toAddress: "simpleledger:qzk92nt0xdxc9qy3yj53h9rjw8dk0s9cqqsrzm5cny") 66 | fail() 67 | } catch SLPTransactionBuilderError.INSUFFICIENT_FUNDS { 68 | // Success 69 | } catch { 70 | fail() 71 | } 72 | } 73 | 74 | it("should fail CONVERSION_METADATA") { 75 | let wallet = try! SLPWallet("machine cannon man rail best deliver draw course time tape violin tone", network: .mainnet) 76 | 77 | let token = SLPToken("test") 78 | token._decimal = 0 79 | 80 | let utxo = SLPTokenUTXO("e3422fa70647b659272e4124234b7d80855ccdf077b683d3f348b76454090f06", satoshis: 1000, cashAddress: "bitcoincash:qzk92nt0xdxc9qy3yj53h9rjw8dk0s9cqqucfqpcd6", scriptPubKey: "483045022100e36b594680823bcf7f4a872611cb7652032e92793f2eadae9ad87a57e4854e3602203ffb057332f47bf9f68738f668acad8ae1d2d3265c34e75c9158c9e9be2ae1f0412103b8ac3da9a09a58444291ce21c68a6b279fe33d3e46a879a4c1ed64bd87146506", index: 1, rawTokenQty: 1234) 81 | utxo._isValid = true 82 | 83 | token.addUTXO(utxo) 84 | 85 | wallet._tokens["test"] = token 86 | 87 | do { 88 | _ = try SLPTransactionBuilder.build(wallet, tokenId: "test", amount: 1234, toAddress: "simpleledger:qzk92nt0xdxc9qy3yj53h9rjw8dk0s9cqqsrzm5cny") 89 | fail() 90 | } catch SLPTransactionBuilderError.CONVERSION_METADATA { 91 | // Success 92 | } catch { 93 | fail() 94 | } 95 | } 96 | 97 | it("should fail GAS_INSUFFISANT") { 98 | let wallet = try! SLPWallet("machine cannon man rail best deliver draw course time tape violin tone", network: .mainnet) 99 | 100 | let token = SLPToken("e3422fa70647b659272e4124234b7d80855ccdf077b683d3f348b76454090f06") 101 | token._decimal = 2 102 | 103 | let utxo = SLPTokenUTXO("e3422fa70647b659272e4124234b7d80855ccdf077b683d3f348b76454090f06", satoshis: 546, cashAddress: "bitcoincash:qzk92nt0xdxc9qy3yj53h9rjw8dk0s9cqqucfqpcd6", scriptPubKey: "483045022100e36b594680823bcf7f4a872611cb7652032e92793f2eadae9ad87a57e4854e3602203ffb057332f47bf9f68738f668acad8ae1d2d3265c34e75c9158c9e9be2ae1f0412103b8ac3da9a09a58444291ce21c68a6b279fe33d3e46a879a4c1ed64bd87146506", index: 1, rawTokenQty: 1234) 104 | utxo._isValid = true 105 | 106 | token.addUTXO(utxo) 107 | 108 | wallet._tokens["e3422fa70647b659272e4124234b7d80855ccdf077b683d3f348b76454090f06"] = token 109 | 110 | do { 111 | _ = try SLPTransactionBuilder.build(wallet, tokenId: "e3422fa70647b659272e4124234b7d80855ccdf077b683d3f348b76454090f06", amount: 11, toAddress: "simpleledger:qzk92nt0xdxc9qy3yj53h9rjw8dk0s9cqqsrzm5cny") 112 | fail() 113 | } catch SLPTransactionBuilderError.GAS_INSUFFICIENT { 114 | // Success 115 | } catch ( _){ 116 | fail() 117 | } 118 | } 119 | 120 | it("should success") { 121 | let wallet = try! SLPWallet("machine cannon man rail best deliver draw course time tape violin tone", network: .mainnet) 122 | 123 | let token = SLPToken("e3422fa70647b659272e4124234b7d80855ccdf077b683d3f348b76454090f06") 124 | token._decimal = 2 125 | 126 | let utxo = SLPTokenUTXO("e3422fa70647b659272e4124234b7d80855ccdf077b683d3f348b76454090f06", satoshis: 20000, cashAddress: "bitcoincash:qzk92nt0xdxc9qy3yj53h9rjw8dk0s9cqqucfqpcd6", scriptPubKey: "483045022100e36b594680823bcf7f4a872611cb7652032e92793f2eadae9ad87a57e4854e3602203ffb057332f47bf9f68738f668acad8ae1d2d3265c34e75c9158c9e9be2ae1f0412103b8ac3da9a09a58444291ce21c68a6b279fe33d3e46a879a4c1ed64bd87146506", index: 1, rawTokenQty: 1234) 127 | utxo._isValid = true 128 | 129 | token.addUTXO(utxo) 130 | 131 | wallet._tokens["e3422fa70647b659272e4124234b7d80855ccdf077b683d3f348b76454090f06"] = token 132 | 133 | do { 134 | let value = try SLPTransactionBuilder.build(wallet, tokenId: "e3422fa70647b659272e4124234b7d80855ccdf077b683d3f348b76454090f06", amount: 12, toAddress: "simpleledger:qqs5mxuxr9kaukncpgdc7z64zp6t87rk7cwtkvhpjv") 135 | 136 | expect(value.usedUTXOs.count).to(equal(1)) 137 | expect(value.newUTXOs.count).to(equal(2)) 138 | expect(value.rawTx).to(equal("0100000001060f095464b748f3d383b677f0cd5c85807d4b2324412e2759b64706a72f42e3010000006a4730440220271ddd30e1b0326fcb47c983e4138b9532f457dde525eded2b6edb63d986504d02200c6d8cbf7e0d3b8b089312f6b9aa3e3dff6baa556611ea86fd0557e770eae57e41210329d5ffda1250d97614cfd3a5cb1c89d0a255c59584c091915b21b3e64137fe7affffffff040000000000000000406a04534c500001010453454e4420e3422fa70647b659272e4124234b7d80855ccdf077b683d3f348b76454090f060800000000000004b008000000000000002222020000000000001976a914214d9b86196dde5a780a1b8f0b551074b3f876f688ac22020000000000001976a914ac554d6f334d82809124a91b947271db67c0b80088acd1470000000000001976a9149b4858ef89806fef10d112e585c2cc01ea429b2e88ac00000000")) 139 | } catch { 140 | fail() 141 | } 142 | } 143 | } 144 | } 145 | } 146 | } 147 | 148 | -------------------------------------------------------------------------------- /SLPWallet/Utils/SLPTransactionBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SLPTransactionBuilder.swift 3 | // SLPWallet 4 | // 5 | // Created by Jean-Baptiste Dominguez on 2019/03/04. 6 | // Copyright © 2019 Bitcoin.com. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import BitcoinKit 11 | 12 | public enum SLPTransactionBuilderError: String, Error { 13 | case CONVERSION_METADATA 14 | case CONVERSION_AMOUNT 15 | case CONVERSION_CHANGE 16 | case DECIMAL_NOT_AVAILABLE 17 | case GAS_INSUFFICIENT 18 | case INSUFFICIENT_FUNDS 19 | case SCRIPT_TO 20 | case SCRIPT_TOKEN_CHANGE 21 | case SCRIPT_CHANGE 22 | case TO_ADDRESS_INVALID 23 | case TOKEN_NOT_FOUND 24 | case WALLET_ADDRESS_INVALID 25 | } 26 | 27 | struct SLPTransactionBuilderResponse { 28 | var rawTx: String 29 | var usedUTXOs: [SLPWalletUTXO] 30 | var newUTXOs: [SLPWalletUTXO] 31 | } 32 | 33 | class SLPTransactionBuilder { 34 | 35 | static func build(_ wallet: SLPWallet, tokenId: String, amount: Double, toAddress: String) throws -> SLPTransactionBuilderResponse { 36 | 37 | let minSatoshisForToken = UInt64(546) 38 | var satoshisForTokens: UInt64 = minSatoshisForToken 39 | let satoshisForInput = 148 + 200 40 | var tokenInputs = 1 41 | var privKeys = [PrivateKey]() 42 | var newUTXOs = [SLPWalletUTXO]() 43 | var usedUTXOs = [SLPWalletUTXO]() 44 | 45 | guard let token = wallet.tokens[tokenId] else { 46 | // Token doesn't exist 47 | throw SLPTransactionBuilderError.TOKEN_NOT_FOUND 48 | } 49 | 50 | guard let decimal = token.decimal else { 51 | throw SLPTransactionBuilderError.DECIMAL_NOT_AVAILABLE 52 | } 53 | 54 | guard token.getBalance() >= amount else { 55 | // Insufficent balance 56 | throw SLPTransactionBuilderError.INSUFFICIENT_FUNDS 57 | } 58 | 59 | // change amount 60 | let rawTokenAmount = TokenQtyConverter.convertToRawQty(amount, decimal: decimal) 61 | 62 | guard let tokenId = token.tokenId 63 | , let tokenIdInData = Data(hex: tokenId) 64 | , let lokadIdInData = Data(hex: "534c5000") 65 | , let tokenTypeInData = Data(hex: "01") 66 | , let actionInData = "SEND".data(using: String.Encoding.ascii) else { 67 | throw SLPTransactionBuilderError.CONVERSION_METADATA 68 | } 69 | 70 | guard let amountInData = TokenQtyConverter.convertToData(rawTokenAmount) else { 71 | throw SLPTransactionBuilderError.CONVERSION_AMOUNT 72 | } 73 | 74 | let newScript = try Script() 75 | .append(.OP_RETURN) 76 | .appendData(lokadIdInData) 77 | .appendData(tokenTypeInData) 78 | .appendData(actionInData) 79 | .appendData(tokenIdInData) 80 | .appendData(amountInData) 81 | 82 | // I can start to create my transaction here :) 83 | 84 | // UTXOs selection from SLPTokenUTXOs 85 | var sum = 0 86 | var selectedTokenUTXOs: [SLPTokenUTXO] = token.utxos 87 | .filter { utxo -> Bool in 88 | guard sum < rawTokenAmount 89 | , let privKey = wallet.getPrivKeyByCashAddress(utxo.cashAddress) else { 90 | return false 91 | } 92 | 93 | privKeys.append(privKey) 94 | sum += utxo.rawTokenQty 95 | return true 96 | } 97 | .compactMap { $0 } 98 | 99 | let rawTokenChange = sum - rawTokenAmount 100 | 101 | // Case we don't have the PrivKey of utxos and didn't get enough tokens 102 | if rawTokenChange < 0 { 103 | throw SLPTransactionBuilderError.INSUFFICIENT_FUNDS 104 | } 105 | 106 | if rawTokenChange > 0 { 107 | guard let changeInData = TokenQtyConverter.convertToData(rawTokenChange) else { 108 | // throw an exception 109 | throw SLPTransactionBuilderError.CONVERSION_CHANGE 110 | } 111 | 112 | try newScript.appendData(changeInData) 113 | satoshisForTokens += minSatoshisForToken 114 | tokenInputs += 1 115 | } 116 | 117 | usedUTXOs.append(contentsOf: selectedTokenUTXOs) 118 | var selectedUTXOs = selectedTokenUTXOs.map { utxo -> UnspentTransaction in 119 | return utxo.asUnspentTransaction() 120 | } 121 | 122 | guard let tokenChangeAddress = try? AddressFactory.create(wallet.SLPAccount.cashAddress) else { 123 | throw SLPTransactionBuilderError.WALLET_ADDRESS_INVALID 124 | } 125 | 126 | guard let lockScriptTokenChange = Script(address: tokenChangeAddress) else { 127 | // throw exception 128 | throw SLPTransactionBuilderError.SCRIPT_TOKEN_CHANGE 129 | } 130 | 131 | guard let cashChangeAddress = try? AddressFactory.create(wallet.BCHAccount.cashAddress) else { 132 | throw SLPTransactionBuilderError.WALLET_ADDRESS_INVALID 133 | } 134 | 135 | guard let lockScriptCashChange = Script(address: cashChangeAddress) else { 136 | // throw exception 137 | throw SLPTransactionBuilderError.SCRIPT_CHANGE 138 | } 139 | 140 | guard let toAddress = try? AddressFactory.create(toAddress) else { 141 | throw SLPTransactionBuilderError.TO_ADDRESS_INVALID 142 | } 143 | 144 | guard let lockScriptTo = Script(address: toAddress) else { 145 | // throw exception 146 | throw SLPTransactionBuilderError.SCRIPT_TO 147 | } 148 | 149 | 150 | let opOutput = TransactionOutput(value: 0, lockingScript: newScript.data) 151 | let toOutput = TransactionOutput(value: minSatoshisForToken, lockingScript: lockScriptTo.data) 152 | 153 | var outputs: [TransactionOutput] = [opOutput, toOutput] 154 | 155 | if rawTokenChange > 0 { 156 | let tokenChangeOutput = TransactionOutput(value: minSatoshisForToken, lockingScript: lockScriptTokenChange.data) 157 | outputs.append(tokenChangeOutput) 158 | } 159 | 160 | let totalAmount: UInt64 = selectedUTXOs.reduce(0) { $0 + $1.output.value } 161 | 162 | // 9 = 8 + 1 unsigned Int quantity of tokens 163 | // 9 = value of OP_RETURN (same as previously) 164 | // 46 = value of OP_RETURN data 165 | // 34 = value of output 166 | // 148 = value of input + 200 for propagation 167 | 168 | let txFee = UInt64(selectedUTXOs.count * satoshisForInput + outputs.count * 34 + 46 + 9 * tokenInputs + 9) 169 | var change: Int64 = Int64(totalAmount) - Int64(satoshisForTokens) - Int64(txFee) 170 | 171 | // If there is not enough gas, lets grab utxos from the wallet to refill 172 | if change < 0 { 173 | var sum = Int(change) 174 | let gasTokenUTXOs: [SLPWalletUTXO] = wallet.utxos 175 | .filter { utxo -> Bool in 176 | guard sum < 0 177 | , let privKey = wallet.getPrivKeyByCashAddress(utxo.cashAddress) else { 178 | return false 179 | } 180 | privKeys.append(privKey) 181 | 182 | sum = sum + Int(utxo.satoshis) - satoshisForInput // Minus the future fee for an input 183 | return true 184 | } 185 | .compactMap { $0 } 186 | 187 | let gasUTXOs = gasTokenUTXOs.map { utxo -> UnspentTransaction in 188 | return utxo.asUnspentTransaction() 189 | } 190 | 191 | let gas: Int64 = Int64(gasUTXOs.reduce(0) { $0 + $1.output.value }) 192 | 193 | change = change + gas 194 | 195 | if change < 0 { 196 | // Throw exception not enough gas 197 | throw SLPTransactionBuilderError.GAS_INSUFFICIENT 198 | } 199 | 200 | // Add my gasUTXOs in my selectedUTXOs 201 | selectedUTXOs.append(contentsOf: gasUTXOs) 202 | usedUTXOs.append(contentsOf: gasTokenUTXOs) 203 | } 204 | 205 | let unsignedInputs = selectedUTXOs.map { TransactionInput(previousOutput: $0.outpoint, signatureScript: Data(), sequence: UInt32.max) } 206 | 207 | if change > minSatoshisForToken { // Minimum for expensable utxo 208 | let changeOutput = TransactionOutput(value: UInt64(change), lockingScript: lockScriptCashChange.data) 209 | outputs.append(changeOutput) 210 | } 211 | 212 | let tx = Transaction(version: 1, inputs: unsignedInputs, outputs: outputs, lockTime: 0) 213 | let unsignedTx = UnsignedTransaction(tx: tx, utxos: selectedUTXOs) 214 | 215 | 216 | var inputsToSign = unsignedTx.tx.inputs 217 | var transactionToSign: Transaction { 218 | return Transaction(version: unsignedTx.tx.version, inputs: inputsToSign, outputs: unsignedTx.tx.outputs, lockTime: unsignedTx.tx.lockTime) 219 | } 220 | 221 | // Signing 222 | let hashType = SighashType.BCH.ALL 223 | for (i, utxo) in unsignedTx.utxos.enumerated() { 224 | let sighash: Data = transactionToSign.signatureHash(for: utxo.output, inputIndex: i, hashType: SighashType.BCH.ALL) 225 | let signature: Data = try! Crypto.sign(sighash, privateKey: privKeys[i]) 226 | let txin = inputsToSign[i] 227 | let pubkey = privKeys[i].publicKey() 228 | 229 | let unlockingScript = Script.buildPublicKeyUnlockingScript(signature: signature, pubkey: pubkey, hashType: hashType) 230 | 231 | inputsToSign[i] = TransactionInput(previousOutput: txin.previousOutput, signatureScript: unlockingScript, sequence: txin.sequence) 232 | } 233 | 234 | let signedTx = transactionToSign.serialized() 235 | 236 | // 237 | // Check Destination 238 | // 239 | 240 | if toAddress.cashaddr == tokenChangeAddress.cashaddr { 241 | let newUTXO = SLPTokenUTXO(unsignedTx.tx.txID, satoshis: Int64(minSatoshisForToken), cashAddress: tokenChangeAddress.cashaddr, scriptPubKey: lockScriptTo.hex, index: 1, rawTokenQty: rawTokenAmount) 242 | newUTXO._isValid = true 243 | newUTXOs.append(newUTXO) 244 | } 245 | 246 | if toAddress.cashaddr == cashChangeAddress.cashaddr { 247 | let newUTXO = SLPWalletUTXO(unsignedTx.tx.txID, satoshis: Int64(minSatoshisForToken), cashAddress: cashChangeAddress.cashaddr, scriptPubKey: lockScriptTo.hex, index: 1) 248 | newUTXOs.append(newUTXO) 249 | } 250 | 251 | // 252 | // Check Change 253 | // 254 | 255 | var index = 2 256 | if rawTokenChange > 0 { 257 | let newUTXO = SLPTokenUTXO(unsignedTx.tx.txID, satoshis: Int64(minSatoshisForToken), cashAddress: tokenChangeAddress.cashaddr, scriptPubKey: lockScriptTokenChange.hex, index: index, rawTokenQty: rawTokenChange) 258 | newUTXO._isValid = true 259 | newUTXOs.append(newUTXO) 260 | index += 1 261 | } 262 | 263 | if change > minSatoshisForToken { // Minimum for expensable utxo 264 | let newUTXO = SLPWalletUTXO(unsignedTx.tx.txID, satoshis: Int64(change), cashAddress: cashChangeAddress.cashaddr, scriptPubKey: lockScriptCashChange.hex, index: index) 265 | newUTXOs.append(newUTXO) 266 | } 267 | 268 | // Return rawTx, inputs used, new outputs 269 | return SLPTransactionBuilderResponse(rawTx: signedTx.hex, usedUTXOs: usedUTXOs, newUTXOs: newUTXOs) 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /SLPWallet/SLPWallet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SLPWallet.swift 3 | // SLPWallet 4 | // 5 | // Created by Jean-Baptiste Dominguez on 2019/02/27. 6 | // Copyright © 2019 Bitcoin.com. All rights reserved. 7 | // 8 | 9 | import BitcoinKit 10 | import RxSwift 11 | import RxCocoa 12 | import KeychainAccess 13 | 14 | public enum SLPWalletError : String, Error { 15 | case TOKEN_ID_REQUIRED 16 | case MNEMONIC_NOT_FOUND 17 | case PRIVKEY_NOT_FOUND 18 | } 19 | 20 | public protocol SLPWalletDelegate { 21 | func onUpdatedToken(_ token: SLPToken) 22 | } 23 | 24 | public class SLPWallet { 25 | 26 | fileprivate static let bag = DisposeBag() 27 | fileprivate static let storageProvider = SecureStorageProvider() 28 | fileprivate let semaphore = DispatchSemaphore(value: 1) 29 | 30 | let _mnemonic: [String] 31 | var _tokens: [String:SLPToken] 32 | 33 | let _BCHAccount: SLPWalletAccount 34 | let _SLPAccount: SLPWalletAccount 35 | 36 | let _slpAddress: String 37 | 38 | let _network: Network 39 | var _utxos: [SLPWalletUTXO] 40 | 41 | // Garbage 42 | var _usedUTXOs: [SLPWalletUTXO] 43 | 44 | var BCHAccount: SLPWalletAccount { 45 | get { return _BCHAccount } 46 | } 47 | 48 | var SLPAccount: SLPWalletAccount { 49 | get { return _SLPAccount } 50 | } 51 | 52 | public var utxos: [SLPWalletUTXO] { 53 | get { return _utxos } 54 | } 55 | 56 | public var mnemonic: [String] { 57 | get { return _mnemonic } 58 | } 59 | public var cashAddress: String { 60 | get { return _BCHAccount.cashAddress } 61 | } 62 | public var slpAddress: String { 63 | get { return _slpAddress } 64 | } 65 | public var tokens: [String:SLPToken] { 66 | get { return _tokens } 67 | } 68 | 69 | public var delegate: SLPWalletDelegate? 70 | 71 | public var schedulerInterval: Double = 20 { 72 | didSet { 73 | scheduler.cancel() 74 | scheduler.schedule(deadline: .now(), repeating: schedulerInterval) 75 | } 76 | } 77 | public lazy var scheduler: DispatchSourceTimer = { 78 | let t = DispatchSource.makeTimerSource() 79 | t.schedule(deadline: .now(), repeating: schedulerInterval) 80 | t.setEventHandler(handler: { [weak self] in 81 | self?.fetchTokens() 82 | .subscribe() 83 | .disposed(by: SLPWallet.bag) 84 | }) 85 | return t 86 | }() 87 | 88 | public convenience init(_ network: Network) throws { 89 | try self.init(network, force: false) 90 | } 91 | 92 | public convenience init(_ network: Network, force: Bool = false) throws { 93 | if force { 94 | let mnemonic = try Mnemonic.generate() 95 | let mnemonicStr = mnemonic.joined(separator: " ") 96 | try self.init(mnemonicStr, network: network) 97 | } else { 98 | // Get in keychain 99 | guard let mnemonic = try SLPWallet.storageProvider.getString("mnemonic") else { 100 | try self.init(network, force: true) 101 | return 102 | } 103 | try self.init(mnemonic, network: network) 104 | } 105 | } 106 | 107 | public init(_ mnemonic: String, network: Network) throws { 108 | 109 | // Store in keychain 110 | try SLPWallet.storageProvider.setString(mnemonic, key: "mnemonic") 111 | 112 | // Then go forward 113 | let arrayOfwords = mnemonic.components(separatedBy: " ") 114 | 115 | let seed = Mnemonic.seed(mnemonic: mnemonic.components(separatedBy: " ")) 116 | let hdPrivKey = HDPrivateKey(seed: seed, network: network) 117 | 118 | // 145 119 | var xPrivKey = try! hdPrivKey.derived(at: 44, hardened: true).derived(at: 145, hardened: true).derived(at: 0, hardened: true) 120 | var privKey = try! xPrivKey.derived(at: UInt32(0)).derived(at: UInt32(0)).privateKey() 121 | 122 | self._BCHAccount = SLPWalletAccount(privKey: privKey, cashAddress: privKey.publicKey().toCashaddr().cashaddr) 123 | 124 | // 245 125 | xPrivKey = try! hdPrivKey.derived(at: 44, hardened: true).derived(at: 245, hardened: true).derived(at: 0, hardened: true) 126 | privKey = try! xPrivKey.derived(at: UInt32(0)).derived(at: UInt32(0)).privateKey() 127 | 128 | self._SLPAccount = SLPWalletAccount(privKey: privKey, cashAddress: privKey.publicKey().toCashaddr().cashaddr) 129 | 130 | self._mnemonic = arrayOfwords 131 | self._network = network 132 | 133 | self._slpAddress = privKey.publicKey().toSlpaddr().slpaddr 134 | self._tokens = [String:SLPToken]() 135 | self._utxos = [SLPWalletUTXO]() 136 | self._usedUTXOs = [SLPWalletUTXO]() 137 | } 138 | } 139 | 140 | public extension SLPWallet { 141 | 142 | func getGas() -> Int { 143 | return _utxos.reduce(0, { $0 + Int($1.satoshis) }) 144 | } 145 | 146 | func fetchTokens() -> Single<[String:SLPToken]> { 147 | 148 | let cashAddresses = [BCHAccount.cashAddress, SLPAccount.cashAddress] 149 | 150 | return Single<[String:SLPToken]>.create { single in 151 | RestService 152 | .fetchUTXOs(cashAddresses) 153 | .subscribe { event in 154 | switch event { 155 | case .success(let rawUtxos): 156 | let utxos = rawUtxos 157 | .flatMap { $0.utxos } 158 | 159 | var myUTXOs: [String] = self.tokens 160 | .flatMap { $1._utxos } 161 | .compactMap { "\($0.txid)-\($0.index)" } 162 | myUTXOs.append(contentsOf: self.utxos.compactMap { "\($0.txid)-\($0.index)" }) 163 | 164 | let requests = utxos 165 | .filter { !myUTXOs.contains("\($0.txid)-\($0.vout)") } 166 | .compactMap { $0.txid } 167 | .removeDuplicates() 168 | .chunk(20) 169 | 170 | guard requests.count > 0 else { 171 | single(.success(self._tokens)) 172 | return 173 | } 174 | 175 | let observable = Observable 176 | .from(requests) 177 | .flatMap { request in 178 | Observable.zip( 179 | RestService.fetchTxDetails(request).asObservable() 180 | , RestService.fetchTxValidations(request).asObservable() 181 | , resultSelector: { (txs, validations) in 182 | return txs 183 | .enumerated() 184 | .compactMap { (index, tx) in 185 | return (tx, validations[index].valid) 186 | } 187 | }) 188 | } 189 | 190 | observable 191 | .subscribe { event in 192 | switch event { 193 | case .next(let txs): 194 | 195 | self.semaphore.wait() 196 | 197 | var updatedTokens = [String:SLPToken]() 198 | var updatedUTXOs = [SLPWalletUTXO]() 199 | 200 | txs.forEach { (tx, isValid) in 201 | 202 | // Get the vouts that we are interested in 203 | let vouts = utxos 204 | .filter { $0.txid == tx.txid } 205 | .map { $0.vout } 206 | 207 | // Parse tx 208 | guard let parsedData = SLPTransactionParser.parse(tx, vouts: vouts) else { 209 | return 210 | } 211 | 212 | if let tokenId = parsedData.token.tokenId { 213 | 214 | // Validate the utxos if it should be 215 | parsedData.token._utxos = parsedData.token._utxos.filter { !self._usedUTXOs.contains($0) } 216 | 217 | // I don't remove it to avoid flickering, in case the API doesn't answer well 218 | parsedData.token._utxos.forEach { $0._isValid = isValid } 219 | 220 | if let token = updatedTokens[tokenId] { 221 | token.merge(parsedData.token) 222 | } else { 223 | updatedTokens[tokenId] = parsedData.token 224 | } 225 | } 226 | 227 | let newUtxos = parsedData.utxos.filter { !self._usedUTXOs.contains($0) } 228 | updatedUTXOs.append(contentsOf: newUtxos) 229 | } 230 | 231 | // 232 | // 233 | // Parse finished 234 | // Update data 235 | // 236 | // 237 | 238 | // Update the UTXOs used as gas :) 239 | self._utxos.mergeElements(newElements: updatedUTXOs) 240 | 241 | // Check which one is new and need to get the info from Genesis 242 | var newTokens = [SLPToken]() 243 | var tokensHaveChanged = [SLPToken]() 244 | 245 | updatedTokens.forEach { tokenId, token in 246 | guard let t = self._tokens[tokenId] else { 247 | if token._utxos.count > 0 { 248 | newTokens.append(token) 249 | } 250 | return 251 | } 252 | 253 | var hasChanged = false 254 | if t._utxos.count != token._utxos.count { 255 | hasChanged = true 256 | } else { 257 | let hash1 = t._utxos 258 | .sorted(by: { (u1, u2) -> Bool in 259 | return u1.txid < u2.txid && u1.index < u2.index 260 | }) 261 | .compactMap { "\($0.hashValue)" } 262 | .joined(separator: "") 263 | 264 | let hash2 = token._utxos 265 | .sorted(by: { (u1, u2) -> Bool in 266 | return u1.txid < u2.txid && u1.index < u2.index 267 | }) 268 | .compactMap { "\($0.hashValue)" } 269 | .joined(separator: "") 270 | 271 | if hash1 != hash2 { 272 | hasChanged = true 273 | } 274 | } 275 | 276 | // If it has changed, notify 277 | if hasChanged { 278 | t._utxos.mergeElements(newElements: token._utxos) 279 | tokensHaveChanged.append(t) 280 | } 281 | } 282 | 283 | // Notify changed tokens 284 | tokensHaveChanged.forEach { self.delegate?.onUpdatedToken($0) } 285 | 286 | self.semaphore.signal() 287 | 288 | // 289 | // 290 | // Update data finished 291 | // Get info on unknown tokens 292 | // 293 | // 294 | 295 | Observable 296 | .zip(newTokens.map { self.addToken($0).asObservable() }) 297 | .subscribe { event in 298 | switch event { 299 | case .next(let tokens): 300 | // Notify new tokens 301 | tokens.forEach { self.delegate?.onUpdatedToken($0) } 302 | case .completed: 303 | single(.success(self._tokens)) 304 | case .error(let error): 305 | single(.error(error)) 306 | } 307 | } 308 | .disposed(by: SLPWallet.bag) 309 | 310 | case .error(let error): 311 | single(.error(error)) 312 | case .completed: break 313 | } 314 | } 315 | .disposed(by: SLPWallet.bag) 316 | case .error(let error): 317 | single(.error(error)) 318 | } 319 | } 320 | .disposed(by: SLPWallet.bag) 321 | return Disposables.create() 322 | } 323 | } 324 | 325 | func sendToken(_ tokenId: String, amount: Double, toAddress: String) -> Single { 326 | return Single.create { single in 327 | self.fetchTokens() 328 | .subscribe(onSuccess: { _ in 329 | do { 330 | let value = try SLPTransactionBuilder.build(self, tokenId: tokenId, amount: amount, toAddress: toAddress) 331 | RestService 332 | .broadcast(value.rawTx) 333 | .subscribe { response in 334 | switch response { 335 | case.success(let txid): 336 | 337 | guard let token = self.tokens[tokenId] else { 338 | return single(.success(txid)) 339 | } 340 | 341 | // TODO: Debug why the TXID is wrong in the builder 342 | // Add the right txid 343 | value.newUTXOs.forEach { $0._txid = txid } 344 | 345 | self.updateUTXOsAfterSending(token, usedUTXOs: value.usedUTXOs, newUTXOs: value.newUTXOs) 346 | 347 | // Update delegate 348 | self.delegate?.onUpdatedToken(token) 349 | 350 | single(.success(txid)) 351 | case .error(let error): 352 | single(.error(error)) 353 | } 354 | } 355 | .disposed(by: SLPWallet.bag) 356 | } catch (let error) { 357 | single(.error(error)) 358 | } 359 | }, onError: { error in 360 | single(.error(error)) 361 | }) 362 | .disposed(by: SLPWallet.bag) 363 | 364 | return Disposables.create() 365 | } 366 | } 367 | 368 | func addToken(_ token: SLPToken) -> Single { 369 | return Single.create { single in 370 | guard let tokenId = token.tokenId else { 371 | single(.error(SLPWalletError.TOKEN_ID_REQUIRED)) 372 | return Disposables.create() 373 | } 374 | RestService 375 | .fetchTxDetails([tokenId]) 376 | .subscribe { response in 377 | switch response { 378 | case.success(let txs): 379 | txs.forEach { tx in 380 | 381 | // Parse tx 382 | guard let parsedData = SLPTransactionParser.parse(tx, vouts: []) else { 383 | return 384 | } 385 | 386 | if let _ = parsedData.token.tokenId { 387 | token.merge(parsedData.token) 388 | } 389 | } 390 | 391 | // Add the token in the list 392 | self._tokens[tokenId] = token 393 | 394 | single(.success(token)) 395 | case .error(let error): 396 | single(.error(error)) 397 | } 398 | } 399 | .disposed(by: SLPWallet.bag) 400 | return Disposables.create() 401 | } 402 | } 403 | } 404 | 405 | extension SLPWallet { 406 | func getPrivKeyByCashAddress(_ cashAddress: String) -> PrivateKey? { 407 | switch cashAddress { 408 | case BCHAccount.cashAddress: 409 | return BCHAccount.privKey 410 | case SLPAccount.cashAddress: 411 | return SLPAccount.privKey 412 | default: 413 | return nil 414 | } 415 | } 416 | 417 | func updateUTXOsAfterSending(_ token: SLPToken, usedUTXOs: [SLPWalletUTXO], newUTXOs: [SLPWalletUTXO]) { 418 | // Add a lock to be sure I am not adding or removing in the same time with the fetchTokens 419 | semaphore.wait() 420 | 421 | newUTXOs.forEach { UTXO in 422 | guard let newUTXO = UTXO as? SLPTokenUTXO else { 423 | return self.addUTXO(UTXO) 424 | } 425 | return token.addUTXO(newUTXO) 426 | } 427 | 428 | _usedUTXOs.append(contentsOf: usedUTXOs) 429 | usedUTXOs.forEach { UTXO in 430 | guard let newUTXO = UTXO as? SLPTokenUTXO else { 431 | return self.removeUTXO(UTXO) 432 | } 433 | return token.removeUTXO(newUTXO) 434 | } 435 | 436 | semaphore.signal() 437 | } 438 | 439 | func addUTXO(_ utxo: SLPWalletUTXO) { 440 | _utxos.append(utxo) 441 | } 442 | 443 | func removeUTXO(_ utxo: SLPWalletUTXO) { 444 | guard let i = _utxos.firstIndex(where: { $0.index == utxo.index && $0.txid == utxo.txid }) else { 445 | return 446 | } 447 | _utxos.remove(at: i) 448 | } 449 | } 450 | -------------------------------------------------------------------------------- /SLPWallet.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 51; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 20F9189D876A537C9921C41D /* Pods_All_SLPWallet.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3D8FBE6A85C0A5C87BA47239 /* Pods_All_SLPWallet.framework */; }; 11 | 28F506C21B69A85C511D3BEF /* Pods_SLPWalletTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7BF528C674BCE9EEB5E90520 /* Pods_SLPWalletTests.framework */; }; 12 | 3B288C49222C387700C1AC81 /* TokenQtyConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B288C46222C300E00C1AC81 /* TokenQtyConverter.swift */; }; 13 | 3B6D8278224241B000119AF0 /* SLPWalletAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B6D8277224241B000119AF0 /* SLPWalletAccount.swift */; }; 14 | 3B79D223223B160100FF3AFB /* Double+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B79D222223B160100FF3AFB /* Double+Extensions.swift */; }; 15 | 3B7FE8D52226A67E00CB5755 /* Data+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B7FE8D42226A67E00CB5755 /* Data+Extensions.swift */; }; 16 | 3B7FE8FE2227ABD900CB5755 /* SLPWalletTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B7FE8FD2227ABD900CB5755 /* SLPWalletTest.swift */; }; 17 | 3B7FE9002227BF8300CB5755 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B7FE8FF2227BF8300CB5755 /* String+Extensions.swift */; }; 18 | 3B7FE90722283C2B00CB5755 /* SLPWallet.podspec in Resources */ = {isa = PBXBuildFile; fileRef = 3B7FE90622283C2B00CB5755 /* SLPWallet.podspec */; }; 19 | 3B7FE9292229225200CB5755 /* SLPTransactionParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B7FE9282229225200CB5755 /* SLPTransactionParser.swift */; }; 20 | 3B7FE92B2229892F00CB5755 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 3B7FE92A2229892E00CB5755 /* README.md */; }; 21 | 3B7FE92D222990FB00CB5755 /* SLPToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B7FE92C222990FB00CB5755 /* SLPToken.swift */; }; 22 | 3B7FE92F2229913100CB5755 /* SLPWalletUTXO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B7FE92E2229913100CB5755 /* SLPWalletUTXO.swift */; }; 23 | 3B7FE9312229915600CB5755 /* SLPTokenUTXO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B7FE9302229915600CB5755 /* SLPTokenUTXO.swift */; }; 24 | 3B7FE933222A895E00CB5755 /* Array+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B7FE932222A895E00CB5755 /* Array+Extensions.swift */; }; 25 | 3B99B05F2226251B00A1B599 /* SLPWallet.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B99B0552226251B00A1B599 /* SLPWallet.framework */; }; 26 | 3B99B07E2226290100A1B599 /* UserDefaults+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B99B0722226290100A1B599 /* UserDefaults+Extensions.swift */; }; 27 | 3B99B0802226290100A1B599 /* RestService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B99B0762226290100A1B599 /* RestService.swift */; }; 28 | 3B99B0812226290100A1B599 /* RestNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B99B0782226290100A1B599 /* RestNetwork.swift */; }; 29 | 3B99B08B22262DC700A1B599 /* SLPWallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B99B08A22262DC700A1B599 /* SLPWallet.swift */; }; 30 | 3BB10CB1224B59E6009BE56D /* SLPWalletConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BB10CB0224B59E6009BE56D /* SLPWalletConfig.swift */; }; 31 | 3BB10CB3224B5AB8009BE56D /* StorageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BB10CB2224B5AB8009BE56D /* StorageProvider.swift */; }; 32 | 3BB10CB5224B5ADB009BE56D /* InternalStorageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BB10CB4224B5ADB009BE56D /* InternalStorageProvider.swift */; }; 33 | 3BB10CB8224B6AE2009BE56D /* SLPWalletConfigTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BB10CB7224B6AE2009BE56D /* SLPWalletConfigTest.swift */; }; 34 | 3BB10CBA224B6CC7009BE56D /* InternalStorageProviderTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BB10CB9224B6CC6009BE56D /* InternalStorageProviderTest.swift */; }; 35 | 3BB10CBC224B6CE7009BE56D /* SecureStorageProviderTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BB10CBB224B6CE7009BE56D /* SecureStorageProviderTest.swift */; }; 36 | 3BC2880B222D022F00EB3375 /* SLPTransactionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BC2880A222D022F00EB3375 /* SLPTransactionBuilder.swift */; }; 37 | 3BD622602248943300503956 /* SLPTransactionParserTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BD6225F2248943300503956 /* SLPTransactionParserTest.swift */; }; 38 | 3BD6226722489C6100503956 /* tx_details_genesis_tst.json in Resources */ = {isa = PBXBuildFile; fileRef = 3BD6226622489C6000503956 /* tx_details_genesis_tst.json */; }; 39 | 3BD622692248B05900503956 /* tx_details_send_tst.json in Resources */ = {isa = PBXBuildFile; fileRef = 3BD622682248B05900503956 /* tx_details_send_tst.json */; }; 40 | 3BD6226B2248B5F500503956 /* tx_details_mint_lvl001.json in Resources */ = {isa = PBXBuildFile; fileRef = 3BD6226A2248B5F500503956 /* tx_details_mint_lvl001.json */; }; 41 | 3BD7508522358D9A00CF5072 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BD7508422358D9A00CF5072 /* AppDelegate.swift */; }; 42 | 3BD7508722358D9A00CF5072 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BD7508622358D9A00CF5072 /* ViewController.swift */; }; 43 | 3BD7508A22358D9A00CF5072 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3BD7508822358D9A00CF5072 /* Main.storyboard */; }; 44 | 3BD7508C22358D9A00CF5072 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3BD7508B22358D9A00CF5072 /* Assets.xcassets */; }; 45 | 3BD7508F22358D9A00CF5072 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3BD7508D22358D9A00CF5072 /* LaunchScreen.storyboard */; }; 46 | 3BD750992237B0E400CF5072 /* TokenQtyConverterTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BD750982237B0E400CF5072 /* TokenQtyConverterTest.swift */; }; 47 | 3BD7509B2237B5AF00CF5072 /* SLPWalletUTXOTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BD7509A2237B5AF00CF5072 /* SLPWalletUTXOTest.swift */; }; 48 | 3BD7509D2237BEAB00CF5072 /* Double+ExtensionsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BD7509C2237BEAB00CF5072 /* Double+ExtensionsTest.swift */; }; 49 | 3BD7509F2237BF9000CF5072 /* String+ExtensionsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BD7509E2237BF9000CF5072 /* String+ExtensionsTest.swift */; }; 50 | 3BD750A12237C06D00CF5072 /* SLPTokenTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BD750A02237C06D00CF5072 /* SLPTokenTest.swift */; }; 51 | 3BD750A32237C3E800CF5072 /* UserDefault+ExtensionsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BD750A22237C3E800CF5072 /* UserDefault+ExtensionsTest.swift */; }; 52 | 3BD750A52237C72900CF5072 /* SLPTransactionBuilderTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BD750A42237C72900CF5072 /* SLPTransactionBuilderTest.swift */; }; 53 | 3BD750AF2237E93400CF5072 /* SecureStorageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BD750AE2237E93400CF5072 /* SecureStorageProvider.swift */; }; 54 | 3BDFC1572233A91E00DC167E /* SLPWalletTests.entitlements in Sources */ = {isa = PBXBuildFile; fileRef = 3BDFC1562233A91E00DC167E /* SLPWalletTests.entitlements */; }; 55 | 3BE242AC22266165003432CE /* Podfile in Resources */ = {isa = PBXBuildFile; fileRef = 3BE242AB22266165003432CE /* Podfile */; }; 56 | 3BE242AE222667F2003432CE /* RestServiceTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BE242AD222667F2003432CE /* RestServiceTest.swift */; }; 57 | /* End PBXBuildFile section */ 58 | 59 | /* Begin PBXContainerItemProxy section */ 60 | 3B99B0602226251B00A1B599 /* PBXContainerItemProxy */ = { 61 | isa = PBXContainerItemProxy; 62 | containerPortal = 3B99B04C2226251B00A1B599 /* Project object */; 63 | proxyType = 1; 64 | remoteGlobalIDString = 3B99B0542226251B00A1B599; 65 | remoteInfo = SLPWallet; 66 | }; 67 | 3BD7509422358DA400CF5072 /* PBXContainerItemProxy */ = { 68 | isa = PBXContainerItemProxy; 69 | containerPortal = 3B99B04C2226251B00A1B599 /* Project object */; 70 | proxyType = 1; 71 | remoteGlobalIDString = 3BD7508122358D9A00CF5072; 72 | remoteInfo = SLPWalletHostTests; 73 | }; 74 | /* End PBXContainerItemProxy section */ 75 | 76 | /* Begin PBXFileReference section */ 77 | 1DFFAD2AFD166179BF17DA9E /* Pods-SLPWalletTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SLPWalletTests.release.xcconfig"; path = "Target Support Files/Pods-SLPWalletTests/Pods-SLPWalletTests.release.xcconfig"; sourceTree = ""; }; 78 | 3B288C46222C300E00C1AC81 /* TokenQtyConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenQtyConverter.swift; sourceTree = ""; }; 79 | 3B6D8277224241B000119AF0 /* SLPWalletAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SLPWalletAccount.swift; sourceTree = ""; }; 80 | 3B79D222223B160100FF3AFB /* Double+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Double+Extensions.swift"; sourceTree = ""; }; 81 | 3B7FE8D42226A67E00CB5755 /* Data+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Extensions.swift"; sourceTree = ""; }; 82 | 3B7FE8FD2227ABD900CB5755 /* SLPWalletTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SLPWalletTest.swift; sourceTree = ""; }; 83 | 3B7FE8FF2227BF8300CB5755 /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = ""; }; 84 | 3B7FE90622283C2B00CB5755 /* SLPWallet.podspec */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = SLPWallet.podspec; sourceTree = ""; }; 85 | 3B7FE9282229225200CB5755 /* SLPTransactionParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SLPTransactionParser.swift; sourceTree = ""; }; 86 | 3B7FE92A2229892E00CB5755 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 87 | 3B7FE92C222990FB00CB5755 /* SLPToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SLPToken.swift; sourceTree = ""; }; 88 | 3B7FE92E2229913100CB5755 /* SLPWalletUTXO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SLPWalletUTXO.swift; sourceTree = ""; }; 89 | 3B7FE9302229915600CB5755 /* SLPTokenUTXO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SLPTokenUTXO.swift; sourceTree = ""; }; 90 | 3B7FE932222A895E00CB5755 /* Array+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Extensions.swift"; sourceTree = ""; }; 91 | 3B99B0552226251B00A1B599 /* SLPWallet.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SLPWallet.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 92 | 3B99B0592226251B00A1B599 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 93 | 3B99B05E2226251B00A1B599 /* SLPWalletTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SLPWalletTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 94 | 3B99B0652226251B00A1B599 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 95 | 3B99B0722226290100A1B599 /* UserDefaults+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UserDefaults+Extensions.swift"; sourceTree = ""; }; 96 | 3B99B0762226290100A1B599 /* RestService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestService.swift; sourceTree = ""; }; 97 | 3B99B0782226290100A1B599 /* RestNetwork.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestNetwork.swift; sourceTree = ""; }; 98 | 3B99B08A22262DC700A1B599 /* SLPWallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SLPWallet.swift; sourceTree = ""; }; 99 | 3BB10CB0224B59E6009BE56D /* SLPWalletConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SLPWalletConfig.swift; sourceTree = ""; }; 100 | 3BB10CB2224B5AB8009BE56D /* StorageProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageProvider.swift; sourceTree = ""; }; 101 | 3BB10CB4224B5ADB009BE56D /* InternalStorageProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalStorageProvider.swift; sourceTree = ""; }; 102 | 3BB10CB7224B6AE2009BE56D /* SLPWalletConfigTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SLPWalletConfigTest.swift; sourceTree = ""; }; 103 | 3BB10CB9224B6CC6009BE56D /* InternalStorageProviderTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalStorageProviderTest.swift; sourceTree = ""; }; 104 | 3BB10CBB224B6CE7009BE56D /* SecureStorageProviderTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureStorageProviderTest.swift; sourceTree = ""; }; 105 | 3BC2880A222D022F00EB3375 /* SLPTransactionBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SLPTransactionBuilder.swift; sourceTree = ""; }; 106 | 3BD6225F2248943300503956 /* SLPTransactionParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SLPTransactionParserTest.swift; sourceTree = ""; }; 107 | 3BD6226622489C6000503956 /* tx_details_genesis_tst.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tx_details_genesis_tst.json; sourceTree = ""; }; 108 | 3BD622682248B05900503956 /* tx_details_send_tst.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tx_details_send_tst.json; sourceTree = ""; }; 109 | 3BD6226A2248B5F500503956 /* tx_details_mint_lvl001.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tx_details_mint_lvl001.json; sourceTree = ""; }; 110 | 3BD7504E2235745D00CF5072 /* BitcoinKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = BitcoinKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 111 | 3BD7508222358D9A00CF5072 /* SLPWalletHostTests.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SLPWalletHostTests.app; sourceTree = BUILT_PRODUCTS_DIR; }; 112 | 3BD7508422358D9A00CF5072 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 113 | 3BD7508622358D9A00CF5072 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 114 | 3BD7508922358D9A00CF5072 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 115 | 3BD7508B22358D9A00CF5072 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 116 | 3BD7508E22358D9A00CF5072 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 117 | 3BD7509022358D9A00CF5072 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 118 | 3BD750982237B0E400CF5072 /* TokenQtyConverterTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenQtyConverterTest.swift; sourceTree = ""; }; 119 | 3BD7509A2237B5AF00CF5072 /* SLPWalletUTXOTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SLPWalletUTXOTest.swift; sourceTree = ""; }; 120 | 3BD7509C2237BEAB00CF5072 /* Double+ExtensionsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+ExtensionsTest.swift"; sourceTree = ""; }; 121 | 3BD7509E2237BF9000CF5072 /* String+ExtensionsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+ExtensionsTest.swift"; sourceTree = ""; }; 122 | 3BD750A02237C06D00CF5072 /* SLPTokenTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SLPTokenTest.swift; sourceTree = ""; }; 123 | 3BD750A22237C3E800CF5072 /* UserDefault+ExtensionsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefault+ExtensionsTest.swift"; sourceTree = ""; }; 124 | 3BD750A42237C72900CF5072 /* SLPTransactionBuilderTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SLPTransactionBuilderTest.swift; sourceTree = ""; }; 125 | 3BD750AE2237E93400CF5072 /* SecureStorageProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureStorageProvider.swift; sourceTree = ""; }; 126 | 3BDFC1562233A91E00DC167E /* SLPWalletTests.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SLPWalletTests.entitlements; sourceTree = ""; }; 127 | 3BE242AB22266165003432CE /* Podfile */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Podfile; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; 128 | 3BE242AD222667F2003432CE /* RestServiceTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestServiceTest.swift; sourceTree = ""; }; 129 | 3D8FBE6A85C0A5C87BA47239 /* Pods_All_SLPWallet.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_All_SLPWallet.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 130 | 7BF528C674BCE9EEB5E90520 /* Pods_SLPWalletTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SLPWalletTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 131 | 890110000F200FAF616C6E2E /* Pods-All-SLPWallet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-All-SLPWallet.debug.xcconfig"; path = "Target Support Files/Pods-All-SLPWallet/Pods-All-SLPWallet.debug.xcconfig"; sourceTree = ""; }; 132 | F1DBD6941A749C2400954E1F /* Pods-SLPWalletTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SLPWalletTests.debug.xcconfig"; path = "Target Support Files/Pods-SLPWalletTests/Pods-SLPWalletTests.debug.xcconfig"; sourceTree = ""; }; 133 | FB3A348EC237FAE26363CAC1 /* Pods-All-SLPWallet.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-All-SLPWallet.release.xcconfig"; path = "Target Support Files/Pods-All-SLPWallet/Pods-All-SLPWallet.release.xcconfig"; sourceTree = ""; }; 134 | /* End PBXFileReference section */ 135 | 136 | /* Begin PBXFrameworksBuildPhase section */ 137 | 3B99B0522226251B00A1B599 /* Frameworks */ = { 138 | isa = PBXFrameworksBuildPhase; 139 | buildActionMask = 2147483647; 140 | files = ( 141 | 20F9189D876A537C9921C41D /* Pods_All_SLPWallet.framework in Frameworks */, 142 | ); 143 | runOnlyForDeploymentPostprocessing = 0; 144 | }; 145 | 3B99B05B2226251B00A1B599 /* Frameworks */ = { 146 | isa = PBXFrameworksBuildPhase; 147 | buildActionMask = 2147483647; 148 | files = ( 149 | 3B99B05F2226251B00A1B599 /* SLPWallet.framework in Frameworks */, 150 | 28F506C21B69A85C511D3BEF /* Pods_SLPWalletTests.framework in Frameworks */, 151 | ); 152 | runOnlyForDeploymentPostprocessing = 0; 153 | }; 154 | 3BD7507F22358D9A00CF5072 /* Frameworks */ = { 155 | isa = PBXFrameworksBuildPhase; 156 | buildActionMask = 2147483647; 157 | files = ( 158 | ); 159 | runOnlyForDeploymentPostprocessing = 0; 160 | }; 161 | /* End PBXFrameworksBuildPhase section */ 162 | 163 | /* Begin PBXGroup section */ 164 | 0BD85E2FC2EB379579C59110 /* Frameworks */ = { 165 | isa = PBXGroup; 166 | children = ( 167 | 3BD7504E2235745D00CF5072 /* BitcoinKit.framework */, 168 | 3D8FBE6A85C0A5C87BA47239 /* Pods_All_SLPWallet.framework */, 169 | 7BF528C674BCE9EEB5E90520 /* Pods_SLPWalletTests.framework */, 170 | ); 171 | name = Frameworks; 172 | sourceTree = ""; 173 | }; 174 | 1BE36C2D73396B1F26487CBB /* Pods */ = { 175 | isa = PBXGroup; 176 | children = ( 177 | 890110000F200FAF616C6E2E /* Pods-All-SLPWallet.debug.xcconfig */, 178 | FB3A348EC237FAE26363CAC1 /* Pods-All-SLPWallet.release.xcconfig */, 179 | F1DBD6941A749C2400954E1F /* Pods-SLPWalletTests.debug.xcconfig */, 180 | 1DFFAD2AFD166179BF17DA9E /* Pods-SLPWalletTests.release.xcconfig */, 181 | ); 182 | path = Pods; 183 | sourceTree = ""; 184 | }; 185 | 3B288C4A222C388500C1AC81 /* Utils */ = { 186 | isa = PBXGroup; 187 | children = ( 188 | 3BC2880A222D022F00EB3375 /* SLPTransactionBuilder.swift */, 189 | 3B7FE9282229225200CB5755 /* SLPTransactionParser.swift */, 190 | 3B288C46222C300E00C1AC81 /* TokenQtyConverter.swift */, 191 | ); 192 | path = Utils; 193 | sourceTree = ""; 194 | }; 195 | 3B99B04B2226251B00A1B599 = { 196 | isa = PBXGroup; 197 | children = ( 198 | 3B7FE92A2229892E00CB5755 /* README.md */, 199 | 3B7FE90622283C2B00CB5755 /* SLPWallet.podspec */, 200 | 3BE242AB22266165003432CE /* Podfile */, 201 | 3B99B0572226251B00A1B599 /* SLPWallet */, 202 | 3B99B0622226251B00A1B599 /* SLPWalletTests */, 203 | 3BD7508322358D9A00CF5072 /* SLPWalletHostTests */, 204 | 3B99B0562226251B00A1B599 /* Products */, 205 | 0BD85E2FC2EB379579C59110 /* Frameworks */, 206 | 1BE36C2D73396B1F26487CBB /* Pods */, 207 | ); 208 | sourceTree = ""; 209 | }; 210 | 3B99B0562226251B00A1B599 /* Products */ = { 211 | isa = PBXGroup; 212 | children = ( 213 | 3B99B0552226251B00A1B599 /* SLPWallet.framework */, 214 | 3B99B05E2226251B00A1B599 /* SLPWalletTests.xctest */, 215 | 3BD7508222358D9A00CF5072 /* SLPWalletHostTests.app */, 216 | ); 217 | name = Products; 218 | sourceTree = ""; 219 | }; 220 | 3B99B0572226251B00A1B599 /* SLPWallet */ = { 221 | isa = PBXGroup; 222 | children = ( 223 | 3BB10CB6224B5BCD009BE56D /* StorageProvider */, 224 | 3BB10CB2224B5AB8009BE56D /* StorageProvider.swift */, 225 | 3BB10CBE224B76F5009BE56D /* Token */, 226 | 3BB10CBD224B76DA009BE56D /* Wallet */, 227 | 3B99B08A22262DC700A1B599 /* SLPWallet.swift */, 228 | 3BB10CB0224B59E6009BE56D /* SLPWalletConfig.swift */, 229 | 3B288C4A222C388500C1AC81 /* Utils */, 230 | 3B99B06F2226290100A1B599 /* Extensions */, 231 | 3B99B0772226290100A1B599 /* Networks */, 232 | 3B99B0752226290100A1B599 /* Services */, 233 | 3B99B0592226251B00A1B599 /* Info.plist */, 234 | ); 235 | path = SLPWallet; 236 | sourceTree = ""; 237 | }; 238 | 3B99B0622226251B00A1B599 /* SLPWalletTests */ = { 239 | isa = PBXGroup; 240 | children = ( 241 | 3BB10C472248B621009BE56D /* Assets */, 242 | 3BE242AD222667F2003432CE /* RestServiceTest.swift */, 243 | 3B7FE8FD2227ABD900CB5755 /* SLPWalletTest.swift */, 244 | 3BD750A02237C06D00CF5072 /* SLPTokenTest.swift */, 245 | 3BD7509A2237B5AF00CF5072 /* SLPWalletUTXOTest.swift */, 246 | 3BB10CB7224B6AE2009BE56D /* SLPWalletConfigTest.swift */, 247 | 3BD6225F2248943300503956 /* SLPTransactionParserTest.swift */, 248 | 3BD750A42237C72900CF5072 /* SLPTransactionBuilderTest.swift */, 249 | 3BB10CBB224B6CE7009BE56D /* SecureStorageProviderTest.swift */, 250 | 3BB10CB9224B6CC6009BE56D /* InternalStorageProviderTest.swift */, 251 | 3BD750982237B0E400CF5072 /* TokenQtyConverterTest.swift */, 252 | 3BD7509C2237BEAB00CF5072 /* Double+ExtensionsTest.swift */, 253 | 3BD7509E2237BF9000CF5072 /* String+ExtensionsTest.swift */, 254 | 3BD750A22237C3E800CF5072 /* UserDefault+ExtensionsTest.swift */, 255 | 3B99B0652226251B00A1B599 /* Info.plist */, 256 | 3BDFC1562233A91E00DC167E /* SLPWalletTests.entitlements */, 257 | ); 258 | path = SLPWalletTests; 259 | sourceTree = ""; 260 | }; 261 | 3B99B06F2226290100A1B599 /* Extensions */ = { 262 | isa = PBXGroup; 263 | children = ( 264 | 3B79D222223B160100FF3AFB /* Double+Extensions.swift */, 265 | 3B7FE8D42226A67E00CB5755 /* Data+Extensions.swift */, 266 | 3B99B0722226290100A1B599 /* UserDefaults+Extensions.swift */, 267 | 3B7FE8FF2227BF8300CB5755 /* String+Extensions.swift */, 268 | 3B7FE932222A895E00CB5755 /* Array+Extensions.swift */, 269 | ); 270 | path = Extensions; 271 | sourceTree = ""; 272 | }; 273 | 3B99B0752226290100A1B599 /* Services */ = { 274 | isa = PBXGroup; 275 | children = ( 276 | 3B99B0762226290100A1B599 /* RestService.swift */, 277 | ); 278 | path = Services; 279 | sourceTree = ""; 280 | }; 281 | 3B99B0772226290100A1B599 /* Networks */ = { 282 | isa = PBXGroup; 283 | children = ( 284 | 3B99B0782226290100A1B599 /* RestNetwork.swift */, 285 | ); 286 | path = Networks; 287 | sourceTree = ""; 288 | }; 289 | 3BB10C472248B621009BE56D /* Assets */ = { 290 | isa = PBXGroup; 291 | children = ( 292 | 3BD6226A2248B5F500503956 /* tx_details_mint_lvl001.json */, 293 | 3BD622682248B05900503956 /* tx_details_send_tst.json */, 294 | 3BD6226622489C6000503956 /* tx_details_genesis_tst.json */, 295 | ); 296 | path = Assets; 297 | sourceTree = ""; 298 | }; 299 | 3BB10CB6224B5BCD009BE56D /* StorageProvider */ = { 300 | isa = PBXGroup; 301 | children = ( 302 | 3BD750AE2237E93400CF5072 /* SecureStorageProvider.swift */, 303 | 3BB10CB4224B5ADB009BE56D /* InternalStorageProvider.swift */, 304 | ); 305 | path = StorageProvider; 306 | sourceTree = ""; 307 | }; 308 | 3BB10CBD224B76DA009BE56D /* Wallet */ = { 309 | isa = PBXGroup; 310 | children = ( 311 | 3B7FE92E2229913100CB5755 /* SLPWalletUTXO.swift */, 312 | 3B6D8277224241B000119AF0 /* SLPWalletAccount.swift */, 313 | ); 314 | path = Wallet; 315 | sourceTree = ""; 316 | }; 317 | 3BB10CBE224B76F5009BE56D /* Token */ = { 318 | isa = PBXGroup; 319 | children = ( 320 | 3B7FE92C222990FB00CB5755 /* SLPToken.swift */, 321 | 3B7FE9302229915600CB5755 /* SLPTokenUTXO.swift */, 322 | ); 323 | path = Token; 324 | sourceTree = ""; 325 | }; 326 | 3BD7508322358D9A00CF5072 /* SLPWalletHostTests */ = { 327 | isa = PBXGroup; 328 | children = ( 329 | 3BD7508422358D9A00CF5072 /* AppDelegate.swift */, 330 | 3BD7508622358D9A00CF5072 /* ViewController.swift */, 331 | 3BD7508822358D9A00CF5072 /* Main.storyboard */, 332 | 3BD7508B22358D9A00CF5072 /* Assets.xcassets */, 333 | 3BD7508D22358D9A00CF5072 /* LaunchScreen.storyboard */, 334 | 3BD7509022358D9A00CF5072 /* Info.plist */, 335 | ); 336 | path = SLPWalletHostTests; 337 | sourceTree = ""; 338 | }; 339 | /* End PBXGroup section */ 340 | 341 | /* Begin PBXHeadersBuildPhase section */ 342 | 3B99B0502226251B00A1B599 /* Headers */ = { 343 | isa = PBXHeadersBuildPhase; 344 | buildActionMask = 2147483647; 345 | files = ( 346 | ); 347 | runOnlyForDeploymentPostprocessing = 0; 348 | }; 349 | /* End PBXHeadersBuildPhase section */ 350 | 351 | /* Begin PBXNativeTarget section */ 352 | 3B99B0542226251B00A1B599 /* SLPWallet */ = { 353 | isa = PBXNativeTarget; 354 | buildConfigurationList = 3B99B0692226251B00A1B599 /* Build configuration list for PBXNativeTarget "SLPWallet" */; 355 | buildPhases = ( 356 | 1378A790A23B5C82273005D5 /* [CP] Check Pods Manifest.lock */, 357 | 3B99B0502226251B00A1B599 /* Headers */, 358 | 3B99B0512226251B00A1B599 /* Sources */, 359 | 3B99B0522226251B00A1B599 /* Frameworks */, 360 | 3B99B0532226251B00A1B599 /* Resources */, 361 | ); 362 | buildRules = ( 363 | ); 364 | dependencies = ( 365 | ); 366 | name = SLPWallet; 367 | productName = SLPWallet; 368 | productReference = 3B99B0552226251B00A1B599 /* SLPWallet.framework */; 369 | productType = "com.apple.product-type.framework"; 370 | }; 371 | 3B99B05D2226251B00A1B599 /* SLPWalletTests */ = { 372 | isa = PBXNativeTarget; 373 | buildConfigurationList = 3B99B06C2226251B00A1B599 /* Build configuration list for PBXNativeTarget "SLPWalletTests" */; 374 | buildPhases = ( 375 | 5D80F066CE9F49464D55CC8D /* [CP] Check Pods Manifest.lock */, 376 | 3B99B05A2226251B00A1B599 /* Sources */, 377 | 3B99B05B2226251B00A1B599 /* Frameworks */, 378 | 3B99B05C2226251B00A1B599 /* Resources */, 379 | 25CE8F2B85109CF3A9716934 /* [CP] Embed Pods Frameworks */, 380 | ); 381 | buildRules = ( 382 | ); 383 | dependencies = ( 384 | 3B99B0612226251B00A1B599 /* PBXTargetDependency */, 385 | 3BD7509522358DA400CF5072 /* PBXTargetDependency */, 386 | ); 387 | name = SLPWalletTests; 388 | productName = SLPWalletTests; 389 | productReference = 3B99B05E2226251B00A1B599 /* SLPWalletTests.xctest */; 390 | productType = "com.apple.product-type.bundle.unit-test"; 391 | }; 392 | 3BD7508122358D9A00CF5072 /* SLPWalletHostTests */ = { 393 | isa = PBXNativeTarget; 394 | buildConfigurationList = 3BD7509122358D9A00CF5072 /* Build configuration list for PBXNativeTarget "SLPWalletHostTests" */; 395 | buildPhases = ( 396 | 3BD7507E22358D9A00CF5072 /* Sources */, 397 | 3BD7507F22358D9A00CF5072 /* Frameworks */, 398 | 3BD7508022358D9A00CF5072 /* Resources */, 399 | ); 400 | buildRules = ( 401 | ); 402 | dependencies = ( 403 | ); 404 | name = SLPWalletHostTests; 405 | productName = SLPWalletHostTests; 406 | productReference = 3BD7508222358D9A00CF5072 /* SLPWalletHostTests.app */; 407 | productType = "com.apple.product-type.application"; 408 | }; 409 | /* End PBXNativeTarget section */ 410 | 411 | /* Begin PBXProject section */ 412 | 3B99B04C2226251B00A1B599 /* Project object */ = { 413 | isa = PBXProject; 414 | attributes = { 415 | LastSwiftUpdateCheck = 1010; 416 | LastUpgradeCheck = 1010; 417 | ORGANIZATIONNAME = Bitcoin.com; 418 | TargetAttributes = { 419 | 3B99B0542226251B00A1B599 = { 420 | CreatedOnToolsVersion = 10.1; 421 | LastSwiftMigration = 1010; 422 | }; 423 | 3B99B05D2226251B00A1B599 = { 424 | CreatedOnToolsVersion = 10.1; 425 | TestTargetID = 3BD7508122358D9A00CF5072; 426 | }; 427 | 3BD7508122358D9A00CF5072 = { 428 | CreatedOnToolsVersion = 10.1; 429 | }; 430 | }; 431 | }; 432 | buildConfigurationList = 3B99B04F2226251B00A1B599 /* Build configuration list for PBXProject "SLPWallet" */; 433 | compatibilityVersion = "Xcode 9.3"; 434 | developmentRegion = en; 435 | hasScannedForEncodings = 0; 436 | knownRegions = ( 437 | en, 438 | Base, 439 | ); 440 | mainGroup = 3B99B04B2226251B00A1B599; 441 | productRefGroup = 3B99B0562226251B00A1B599 /* Products */; 442 | projectDirPath = ""; 443 | projectRoot = ""; 444 | targets = ( 445 | 3B99B0542226251B00A1B599 /* SLPWallet */, 446 | 3B99B05D2226251B00A1B599 /* SLPWalletTests */, 447 | 3BD7508122358D9A00CF5072 /* SLPWalletHostTests */, 448 | ); 449 | }; 450 | /* End PBXProject section */ 451 | 452 | /* Begin PBXResourcesBuildPhase section */ 453 | 3B99B0532226251B00A1B599 /* Resources */ = { 454 | isa = PBXResourcesBuildPhase; 455 | buildActionMask = 2147483647; 456 | files = ( 457 | 3B7FE92B2229892F00CB5755 /* README.md in Resources */, 458 | 3BE242AC22266165003432CE /* Podfile in Resources */, 459 | 3B7FE90722283C2B00CB5755 /* SLPWallet.podspec in Resources */, 460 | ); 461 | runOnlyForDeploymentPostprocessing = 0; 462 | }; 463 | 3B99B05C2226251B00A1B599 /* Resources */ = { 464 | isa = PBXResourcesBuildPhase; 465 | buildActionMask = 2147483647; 466 | files = ( 467 | 3BD622692248B05900503956 /* tx_details_send_tst.json in Resources */, 468 | 3BD6226722489C6100503956 /* tx_details_genesis_tst.json in Resources */, 469 | 3BD6226B2248B5F500503956 /* tx_details_mint_lvl001.json in Resources */, 470 | ); 471 | runOnlyForDeploymentPostprocessing = 0; 472 | }; 473 | 3BD7508022358D9A00CF5072 /* Resources */ = { 474 | isa = PBXResourcesBuildPhase; 475 | buildActionMask = 2147483647; 476 | files = ( 477 | 3BD7508F22358D9A00CF5072 /* LaunchScreen.storyboard in Resources */, 478 | 3BD7508C22358D9A00CF5072 /* Assets.xcassets in Resources */, 479 | 3BD7508A22358D9A00CF5072 /* Main.storyboard in Resources */, 480 | ); 481 | runOnlyForDeploymentPostprocessing = 0; 482 | }; 483 | /* End PBXResourcesBuildPhase section */ 484 | 485 | /* Begin PBXShellScriptBuildPhase section */ 486 | 1378A790A23B5C82273005D5 /* [CP] Check Pods Manifest.lock */ = { 487 | isa = PBXShellScriptBuildPhase; 488 | buildActionMask = 2147483647; 489 | files = ( 490 | ); 491 | inputFileListPaths = ( 492 | ); 493 | inputPaths = ( 494 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 495 | "${PODS_ROOT}/Manifest.lock", 496 | ); 497 | name = "[CP] Check Pods Manifest.lock"; 498 | outputFileListPaths = ( 499 | ); 500 | outputPaths = ( 501 | "$(DERIVED_FILE_DIR)/Pods-All-SLPWallet-checkManifestLockResult.txt", 502 | ); 503 | runOnlyForDeploymentPostprocessing = 0; 504 | shellPath = /bin/sh; 505 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 506 | showEnvVarsInLog = 0; 507 | }; 508 | 25CE8F2B85109CF3A9716934 /* [CP] Embed Pods Frameworks */ = { 509 | isa = PBXShellScriptBuildPhase; 510 | buildActionMask = 2147483647; 511 | files = ( 512 | ); 513 | inputFileListPaths = ( 514 | "${PODS_ROOT}/Target Support Files/Pods-SLPWalletTests/Pods-SLPWalletTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", 515 | ); 516 | name = "[CP] Embed Pods Frameworks"; 517 | outputFileListPaths = ( 518 | "${PODS_ROOT}/Target Support Files/Pods-SLPWalletTests/Pods-SLPWalletTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", 519 | ); 520 | runOnlyForDeploymentPostprocessing = 0; 521 | shellPath = /bin/sh; 522 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-SLPWalletTests/Pods-SLPWalletTests-frameworks.sh\"\n"; 523 | showEnvVarsInLog = 0; 524 | }; 525 | 5D80F066CE9F49464D55CC8D /* [CP] Check Pods Manifest.lock */ = { 526 | isa = PBXShellScriptBuildPhase; 527 | buildActionMask = 2147483647; 528 | files = ( 529 | ); 530 | inputFileListPaths = ( 531 | ); 532 | inputPaths = ( 533 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 534 | "${PODS_ROOT}/Manifest.lock", 535 | ); 536 | name = "[CP] Check Pods Manifest.lock"; 537 | outputFileListPaths = ( 538 | ); 539 | outputPaths = ( 540 | "$(DERIVED_FILE_DIR)/Pods-SLPWalletTests-checkManifestLockResult.txt", 541 | ); 542 | runOnlyForDeploymentPostprocessing = 0; 543 | shellPath = /bin/sh; 544 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 545 | showEnvVarsInLog = 0; 546 | }; 547 | /* End PBXShellScriptBuildPhase section */ 548 | 549 | /* Begin PBXSourcesBuildPhase section */ 550 | 3B99B0512226251B00A1B599 /* Sources */ = { 551 | isa = PBXSourcesBuildPhase; 552 | buildActionMask = 2147483647; 553 | files = ( 554 | 3BC2880B222D022F00EB3375 /* SLPTransactionBuilder.swift in Sources */, 555 | 3B99B07E2226290100A1B599 /* UserDefaults+Extensions.swift in Sources */, 556 | 3B99B08B22262DC700A1B599 /* SLPWallet.swift in Sources */, 557 | 3B7FE92F2229913100CB5755 /* SLPWalletUTXO.swift in Sources */, 558 | 3BB10CB3224B5AB8009BE56D /* StorageProvider.swift in Sources */, 559 | 3BD750AF2237E93400CF5072 /* SecureStorageProvider.swift in Sources */, 560 | 3B99B0812226290100A1B599 /* RestNetwork.swift in Sources */, 561 | 3B288C49222C387700C1AC81 /* TokenQtyConverter.swift in Sources */, 562 | 3BB10CB5224B5ADB009BE56D /* InternalStorageProvider.swift in Sources */, 563 | 3B6D8278224241B000119AF0 /* SLPWalletAccount.swift in Sources */, 564 | 3BB10CB1224B59E6009BE56D /* SLPWalletConfig.swift in Sources */, 565 | 3B7FE9002227BF8300CB5755 /* String+Extensions.swift in Sources */, 566 | 3B7FE9312229915600CB5755 /* SLPTokenUTXO.swift in Sources */, 567 | 3B79D223223B160100FF3AFB /* Double+Extensions.swift in Sources */, 568 | 3B99B0802226290100A1B599 /* RestService.swift in Sources */, 569 | 3B7FE8D52226A67E00CB5755 /* Data+Extensions.swift in Sources */, 570 | 3B7FE933222A895E00CB5755 /* Array+Extensions.swift in Sources */, 571 | 3B7FE9292229225200CB5755 /* SLPTransactionParser.swift in Sources */, 572 | 3B7FE92D222990FB00CB5755 /* SLPToken.swift in Sources */, 573 | ); 574 | runOnlyForDeploymentPostprocessing = 0; 575 | }; 576 | 3B99B05A2226251B00A1B599 /* Sources */ = { 577 | isa = PBXSourcesBuildPhase; 578 | buildActionMask = 2147483647; 579 | files = ( 580 | 3BB10CBA224B6CC7009BE56D /* InternalStorageProviderTest.swift in Sources */, 581 | 3B7FE8FE2227ABD900CB5755 /* SLPWalletTest.swift in Sources */, 582 | 3BD7509F2237BF9000CF5072 /* String+ExtensionsTest.swift in Sources */, 583 | 3BD750A32237C3E800CF5072 /* UserDefault+ExtensionsTest.swift in Sources */, 584 | 3BD750992237B0E400CF5072 /* TokenQtyConverterTest.swift in Sources */, 585 | 3BD7509D2237BEAB00CF5072 /* Double+ExtensionsTest.swift in Sources */, 586 | 3BD7509B2237B5AF00CF5072 /* SLPWalletUTXOTest.swift in Sources */, 587 | 3BB10CBC224B6CE7009BE56D /* SecureStorageProviderTest.swift in Sources */, 588 | 3BB10CB8224B6AE2009BE56D /* SLPWalletConfigTest.swift in Sources */, 589 | 3BD750A52237C72900CF5072 /* SLPTransactionBuilderTest.swift in Sources */, 590 | 3BD750A12237C06D00CF5072 /* SLPTokenTest.swift in Sources */, 591 | 3BD622602248943300503956 /* SLPTransactionParserTest.swift in Sources */, 592 | 3BDFC1572233A91E00DC167E /* SLPWalletTests.entitlements in Sources */, 593 | 3BE242AE222667F2003432CE /* RestServiceTest.swift in Sources */, 594 | ); 595 | runOnlyForDeploymentPostprocessing = 0; 596 | }; 597 | 3BD7507E22358D9A00CF5072 /* Sources */ = { 598 | isa = PBXSourcesBuildPhase; 599 | buildActionMask = 2147483647; 600 | files = ( 601 | 3BD7508722358D9A00CF5072 /* ViewController.swift in Sources */, 602 | 3BD7508522358D9A00CF5072 /* AppDelegate.swift in Sources */, 603 | ); 604 | runOnlyForDeploymentPostprocessing = 0; 605 | }; 606 | /* End PBXSourcesBuildPhase section */ 607 | 608 | /* Begin PBXTargetDependency section */ 609 | 3B99B0612226251B00A1B599 /* PBXTargetDependency */ = { 610 | isa = PBXTargetDependency; 611 | target = 3B99B0542226251B00A1B599 /* SLPWallet */; 612 | targetProxy = 3B99B0602226251B00A1B599 /* PBXContainerItemProxy */; 613 | }; 614 | 3BD7509522358DA400CF5072 /* PBXTargetDependency */ = { 615 | isa = PBXTargetDependency; 616 | target = 3BD7508122358D9A00CF5072 /* SLPWalletHostTests */; 617 | targetProxy = 3BD7509422358DA400CF5072 /* PBXContainerItemProxy */; 618 | }; 619 | /* End PBXTargetDependency section */ 620 | 621 | /* Begin PBXVariantGroup section */ 622 | 3BD7508822358D9A00CF5072 /* Main.storyboard */ = { 623 | isa = PBXVariantGroup; 624 | children = ( 625 | 3BD7508922358D9A00CF5072 /* Base */, 626 | ); 627 | name = Main.storyboard; 628 | sourceTree = ""; 629 | }; 630 | 3BD7508D22358D9A00CF5072 /* LaunchScreen.storyboard */ = { 631 | isa = PBXVariantGroup; 632 | children = ( 633 | 3BD7508E22358D9A00CF5072 /* Base */, 634 | ); 635 | name = LaunchScreen.storyboard; 636 | sourceTree = ""; 637 | }; 638 | /* End PBXVariantGroup section */ 639 | 640 | /* Begin XCBuildConfiguration section */ 641 | 3B99B0672226251B00A1B599 /* Debug */ = { 642 | isa = XCBuildConfiguration; 643 | buildSettings = { 644 | ALWAYS_SEARCH_USER_PATHS = NO; 645 | CLANG_ANALYZER_NONNULL = YES; 646 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 647 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 648 | CLANG_CXX_LIBRARY = "libc++"; 649 | CLANG_ENABLE_MODULES = YES; 650 | CLANG_ENABLE_OBJC_ARC = YES; 651 | CLANG_ENABLE_OBJC_WEAK = YES; 652 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 653 | CLANG_WARN_BOOL_CONVERSION = YES; 654 | CLANG_WARN_COMMA = YES; 655 | CLANG_WARN_CONSTANT_CONVERSION = YES; 656 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 657 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 658 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 659 | CLANG_WARN_EMPTY_BODY = YES; 660 | CLANG_WARN_ENUM_CONVERSION = YES; 661 | CLANG_WARN_INFINITE_RECURSION = YES; 662 | CLANG_WARN_INT_CONVERSION = YES; 663 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 664 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 665 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 666 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 667 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 668 | CLANG_WARN_STRICT_PROTOTYPES = YES; 669 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 670 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 671 | CLANG_WARN_UNREACHABLE_CODE = YES; 672 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 673 | CODE_SIGN_IDENTITY = "iPhone Developer"; 674 | COPY_PHASE_STRIP = NO; 675 | CURRENT_PROJECT_VERSION = 1; 676 | DEBUG_INFORMATION_FORMAT = dwarf; 677 | ENABLE_STRICT_OBJC_MSGSEND = YES; 678 | ENABLE_TESTABILITY = YES; 679 | GCC_C_LANGUAGE_STANDARD = gnu11; 680 | GCC_DYNAMIC_NO_PIC = NO; 681 | GCC_NO_COMMON_BLOCKS = YES; 682 | GCC_OPTIMIZATION_LEVEL = 0; 683 | GCC_PREPROCESSOR_DEFINITIONS = ( 684 | "DEBUG=1", 685 | "$(inherited)", 686 | ); 687 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 688 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 689 | GCC_WARN_UNDECLARED_SELECTOR = YES; 690 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 691 | GCC_WARN_UNUSED_FUNCTION = YES; 692 | GCC_WARN_UNUSED_VARIABLE = YES; 693 | IPHONEOS_DEPLOYMENT_TARGET = 12.1; 694 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 695 | MTL_FAST_MATH = YES; 696 | ONLY_ACTIVE_ARCH = YES; 697 | SDKROOT = iphoneos; 698 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 699 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 700 | SWIFT_VERSION = 4.0; 701 | VERSIONING_SYSTEM = "apple-generic"; 702 | VERSION_INFO_PREFIX = ""; 703 | }; 704 | name = Debug; 705 | }; 706 | 3B99B0682226251B00A1B599 /* Release */ = { 707 | isa = XCBuildConfiguration; 708 | buildSettings = { 709 | ALWAYS_SEARCH_USER_PATHS = NO; 710 | CLANG_ANALYZER_NONNULL = YES; 711 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 712 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 713 | CLANG_CXX_LIBRARY = "libc++"; 714 | CLANG_ENABLE_MODULES = YES; 715 | CLANG_ENABLE_OBJC_ARC = YES; 716 | CLANG_ENABLE_OBJC_WEAK = YES; 717 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 718 | CLANG_WARN_BOOL_CONVERSION = YES; 719 | CLANG_WARN_COMMA = YES; 720 | CLANG_WARN_CONSTANT_CONVERSION = YES; 721 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 722 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 723 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 724 | CLANG_WARN_EMPTY_BODY = YES; 725 | CLANG_WARN_ENUM_CONVERSION = YES; 726 | CLANG_WARN_INFINITE_RECURSION = YES; 727 | CLANG_WARN_INT_CONVERSION = YES; 728 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 729 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 730 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 731 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 732 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 733 | CLANG_WARN_STRICT_PROTOTYPES = YES; 734 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 735 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 736 | CLANG_WARN_UNREACHABLE_CODE = YES; 737 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 738 | CODE_SIGN_IDENTITY = "iPhone Developer"; 739 | COPY_PHASE_STRIP = NO; 740 | CURRENT_PROJECT_VERSION = 1; 741 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 742 | ENABLE_NS_ASSERTIONS = NO; 743 | ENABLE_STRICT_OBJC_MSGSEND = YES; 744 | GCC_C_LANGUAGE_STANDARD = gnu11; 745 | GCC_NO_COMMON_BLOCKS = YES; 746 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 747 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 748 | GCC_WARN_UNDECLARED_SELECTOR = YES; 749 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 750 | GCC_WARN_UNUSED_FUNCTION = YES; 751 | GCC_WARN_UNUSED_VARIABLE = YES; 752 | IPHONEOS_DEPLOYMENT_TARGET = 12.1; 753 | MTL_ENABLE_DEBUG_INFO = NO; 754 | MTL_FAST_MATH = YES; 755 | SDKROOT = iphoneos; 756 | SWIFT_COMPILATION_MODE = wholemodule; 757 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 758 | SWIFT_VERSION = 4.0; 759 | VALIDATE_PRODUCT = YES; 760 | VERSIONING_SYSTEM = "apple-generic"; 761 | VERSION_INFO_PREFIX = ""; 762 | }; 763 | name = Release; 764 | }; 765 | 3B99B06A2226251B00A1B599 /* Debug */ = { 766 | isa = XCBuildConfiguration; 767 | baseConfigurationReference = 890110000F200FAF616C6E2E /* Pods-All-SLPWallet.debug.xcconfig */; 768 | buildSettings = { 769 | CLANG_ENABLE_MODULES = YES; 770 | CODE_SIGN_IDENTITY = ""; 771 | CODE_SIGN_STYLE = Automatic; 772 | DEFINES_MODULE = YES; 773 | DEVELOPMENT_TEAM = 299HJ3G3BP; 774 | DYLIB_COMPATIBILITY_VERSION = 1; 775 | DYLIB_CURRENT_VERSION = 1; 776 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 777 | INFOPLIST_FILE = SLPWallet/Info.plist; 778 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 779 | LD_RUNPATH_SEARCH_PATHS = ( 780 | "$(inherited)", 781 | "@executable_path/Frameworks", 782 | "@loader_path/Frameworks", 783 | ); 784 | LIBRARY_SEARCH_PATHS = ( 785 | "$(inherited)", 786 | "$(PROJECT_DIR)/Sample/SLPWalletTestApp/Pods/BitcoinKit/Libraries/secp256k1/lib", 787 | "$(PROJECT_DIR)/Sample/SLPWalletTestApp/Pods/BitcoinKit/Libraries/openssl/lib", 788 | ); 789 | PRODUCT_BUNDLE_IDENTIFIER = com.bitcoin.SLPWallet; 790 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 791 | SKIP_INSTALL = YES; 792 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 793 | SWIFT_VERSION = 4.0; 794 | TARGETED_DEVICE_FAMILY = "1,2"; 795 | }; 796 | name = Debug; 797 | }; 798 | 3B99B06B2226251B00A1B599 /* Release */ = { 799 | isa = XCBuildConfiguration; 800 | baseConfigurationReference = FB3A348EC237FAE26363CAC1 /* Pods-All-SLPWallet.release.xcconfig */; 801 | buildSettings = { 802 | CLANG_ENABLE_MODULES = YES; 803 | CODE_SIGN_IDENTITY = ""; 804 | CODE_SIGN_STYLE = Automatic; 805 | DEFINES_MODULE = YES; 806 | DEVELOPMENT_TEAM = 299HJ3G3BP; 807 | DYLIB_COMPATIBILITY_VERSION = 1; 808 | DYLIB_CURRENT_VERSION = 1; 809 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 810 | INFOPLIST_FILE = SLPWallet/Info.plist; 811 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 812 | LD_RUNPATH_SEARCH_PATHS = ( 813 | "$(inherited)", 814 | "@executable_path/Frameworks", 815 | "@loader_path/Frameworks", 816 | ); 817 | LIBRARY_SEARCH_PATHS = ( 818 | "$(inherited)", 819 | "$(PROJECT_DIR)/Sample/SLPWalletTestApp/Pods/BitcoinKit/Libraries/secp256k1/lib", 820 | "$(PROJECT_DIR)/Sample/SLPWalletTestApp/Pods/BitcoinKit/Libraries/openssl/lib", 821 | ); 822 | PRODUCT_BUNDLE_IDENTIFIER = com.bitcoin.SLPWallet; 823 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 824 | SKIP_INSTALL = YES; 825 | SWIFT_VERSION = 4.0; 826 | TARGETED_DEVICE_FAMILY = "1,2"; 827 | }; 828 | name = Release; 829 | }; 830 | 3B99B06D2226251B00A1B599 /* Debug */ = { 831 | isa = XCBuildConfiguration; 832 | baseConfigurationReference = F1DBD6941A749C2400954E1F /* Pods-SLPWalletTests.debug.xcconfig */; 833 | buildSettings = { 834 | BUNDLE_LOADER = "$(TEST_HOST)"; 835 | CODE_SIGN_ENTITLEMENTS = "${SRCROOT}/SLPWalletTests/SLPWalletTests.entitlements"; 836 | CODE_SIGN_STYLE = Automatic; 837 | DEVELOPMENT_TEAM = 299HJ3G3BP; 838 | INFOPLIST_FILE = SLPWalletTests/Info.plist; 839 | LD_RUNPATH_SEARCH_PATHS = ( 840 | "$(inherited)", 841 | "@executable_path/Frameworks", 842 | "@loader_path/Frameworks", 843 | ); 844 | PRODUCT_BUNDLE_IDENTIFIER = com.bitcoin.SLPWalletTests; 845 | PRODUCT_NAME = "$(TARGET_NAME)"; 846 | SWIFT_VERSION = 4.0; 847 | TARGETED_DEVICE_FAMILY = "1,2"; 848 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SLPWalletHostTests.app/SLPWalletHostTests"; 849 | }; 850 | name = Debug; 851 | }; 852 | 3B99B06E2226251B00A1B599 /* Release */ = { 853 | isa = XCBuildConfiguration; 854 | baseConfigurationReference = 1DFFAD2AFD166179BF17DA9E /* Pods-SLPWalletTests.release.xcconfig */; 855 | buildSettings = { 856 | BUNDLE_LOADER = "$(TEST_HOST)"; 857 | CODE_SIGN_ENTITLEMENTS = "${SRCROOT}/SLPWalletTests/SLPWalletTests.entitlements"; 858 | CODE_SIGN_STYLE = Automatic; 859 | DEVELOPMENT_TEAM = 299HJ3G3BP; 860 | INFOPLIST_FILE = SLPWalletTests/Info.plist; 861 | LD_RUNPATH_SEARCH_PATHS = ( 862 | "$(inherited)", 863 | "@executable_path/Frameworks", 864 | "@loader_path/Frameworks", 865 | ); 866 | PRODUCT_BUNDLE_IDENTIFIER = com.bitcoin.SLPWalletTests; 867 | PRODUCT_NAME = "$(TARGET_NAME)"; 868 | SWIFT_VERSION = 4.0; 869 | TARGETED_DEVICE_FAMILY = "1,2"; 870 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SLPWalletHostTests.app/SLPWalletHostTests"; 871 | }; 872 | name = Release; 873 | }; 874 | 3BD7509222358D9A00CF5072 /* Debug */ = { 875 | isa = XCBuildConfiguration; 876 | buildSettings = { 877 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 878 | CODE_SIGN_STYLE = Automatic; 879 | DEVELOPMENT_TEAM = 299HJ3G3BP; 880 | INFOPLIST_FILE = SLPWalletHostTests/Info.plist; 881 | LD_RUNPATH_SEARCH_PATHS = ( 882 | "$(inherited)", 883 | "@executable_path/Frameworks", 884 | ); 885 | PRODUCT_BUNDLE_IDENTIFIER = com.bitcoin.SLPWalletHostTests; 886 | PRODUCT_NAME = "$(TARGET_NAME)"; 887 | SWIFT_VERSION = 4.2; 888 | TARGETED_DEVICE_FAMILY = "1,2"; 889 | }; 890 | name = Debug; 891 | }; 892 | 3BD7509322358D9A00CF5072 /* Release */ = { 893 | isa = XCBuildConfiguration; 894 | buildSettings = { 895 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 896 | CODE_SIGN_STYLE = Automatic; 897 | DEVELOPMENT_TEAM = 299HJ3G3BP; 898 | INFOPLIST_FILE = SLPWalletHostTests/Info.plist; 899 | LD_RUNPATH_SEARCH_PATHS = ( 900 | "$(inherited)", 901 | "@executable_path/Frameworks", 902 | ); 903 | PRODUCT_BUNDLE_IDENTIFIER = com.bitcoin.SLPWalletHostTests; 904 | PRODUCT_NAME = "$(TARGET_NAME)"; 905 | SWIFT_VERSION = 4.2; 906 | TARGETED_DEVICE_FAMILY = "1,2"; 907 | }; 908 | name = Release; 909 | }; 910 | /* End XCBuildConfiguration section */ 911 | 912 | /* Begin XCConfigurationList section */ 913 | 3B99B04F2226251B00A1B599 /* Build configuration list for PBXProject "SLPWallet" */ = { 914 | isa = XCConfigurationList; 915 | buildConfigurations = ( 916 | 3B99B0672226251B00A1B599 /* Debug */, 917 | 3B99B0682226251B00A1B599 /* Release */, 918 | ); 919 | defaultConfigurationIsVisible = 0; 920 | defaultConfigurationName = Release; 921 | }; 922 | 3B99B0692226251B00A1B599 /* Build configuration list for PBXNativeTarget "SLPWallet" */ = { 923 | isa = XCConfigurationList; 924 | buildConfigurations = ( 925 | 3B99B06A2226251B00A1B599 /* Debug */, 926 | 3B99B06B2226251B00A1B599 /* Release */, 927 | ); 928 | defaultConfigurationIsVisible = 0; 929 | defaultConfigurationName = Release; 930 | }; 931 | 3B99B06C2226251B00A1B599 /* Build configuration list for PBXNativeTarget "SLPWalletTests" */ = { 932 | isa = XCConfigurationList; 933 | buildConfigurations = ( 934 | 3B99B06D2226251B00A1B599 /* Debug */, 935 | 3B99B06E2226251B00A1B599 /* Release */, 936 | ); 937 | defaultConfigurationIsVisible = 0; 938 | defaultConfigurationName = Release; 939 | }; 940 | 3BD7509122358D9A00CF5072 /* Build configuration list for PBXNativeTarget "SLPWalletHostTests" */ = { 941 | isa = XCConfigurationList; 942 | buildConfigurations = ( 943 | 3BD7509222358D9A00CF5072 /* Debug */, 944 | 3BD7509322358D9A00CF5072 /* Release */, 945 | ); 946 | defaultConfigurationIsVisible = 0; 947 | defaultConfigurationName = Release; 948 | }; 949 | /* End XCConfigurationList section */ 950 | }; 951 | rootObject = 3B99B04C2226251B00A1B599 /* Project object */; 952 | } 953 | --------------------------------------------------------------------------------