├── .github └── workflows │ ├── build-beta.yml │ └── build-main.yml ├── .gitignore ├── .swift-format ├── .swiftlint.yml ├── .swiftpm └── xcode │ ├── package.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ └── xcschemes │ ├── XyoClient.xcscheme │ └── sdk-xyo-client-swift.xcscheme ├── .vscode └── settings.json ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── XyoClient │ ├── Address │ │ ├── Account.swift │ │ ├── AccountInstance.swift │ │ ├── AccountServices.swift │ │ ├── AccountStatic.swift │ │ ├── Address.swift │ │ ├── Error.swift │ │ ├── XyoAddress.swift │ │ └── generateRandomBytes.swift │ ├── Api │ │ ├── ApiConfig.swift │ │ ├── ApiResponseEnvelope.swift │ │ └── Archivist │ │ │ ├── ArchivistApiClient.swift │ │ │ └── ArchivistApiConfig.swift │ ├── BoundWitness │ │ ├── BoundWitness.swift │ │ ├── BoundWitnessBuilder.swift │ │ └── Meta │ │ │ └── BoundWitnessMeta.swift │ ├── Hash │ │ └── Hash.swift │ ├── Keychain │ │ └── Keychain.swift │ ├── Model.xcdatamodeld │ │ └── Model.xcdatamodel │ │ │ └── contents │ ├── Module │ │ ├── AbstractModule.swift │ │ ├── Module.swift │ │ └── ModuleQueryResult.swift │ ├── Panel │ │ └── Panel.swift │ ├── Payload │ │ ├── Core │ │ │ └── Id.swift │ │ ├── Payload.swift │ │ ├── PayloadBuilder.swift │ │ ├── PayloadValidator.swift │ │ └── PayloadWrapper.swift │ ├── PersistentContainerProvider │ │ ├── DefaultPersistentContainerProvider.swift │ │ └── PersistentContainerProvider.swift │ ├── PreviousHashStore │ │ ├── CoreData │ │ │ ├── CoreDataPreviousHashStore.swift │ │ │ └── TestPersistentContainerProvider.swift │ │ ├── Memory │ │ │ └── MemoryPreviousHashStore.swift │ │ └── PreviousHashStore.swift │ ├── Schema │ │ └── SchemaValidator.swift │ ├── Wallet │ │ ├── BIP-39 │ │ │ ├── Bip39.swift │ │ │ └── Bip39Words.swift │ │ ├── Extensions.swift │ │ ├── Hmac.swift │ │ ├── Secp256k1.swift │ │ ├── Wallet.swift │ │ ├── WalletInstance.swift │ │ └── WalletStatic.swift │ ├── Witness │ │ ├── Basic │ │ │ └── BasicWitness.swift │ │ ├── Event │ │ │ ├── EventPayload.swift │ │ │ └── EventWitness.swift │ │ ├── Location │ │ │ ├── Generic │ │ │ │ ├── Coordinates.swift │ │ │ │ ├── CurrentLocation.swift │ │ │ │ └── LocationPayload.swift │ │ │ ├── LocationService.swift │ │ │ ├── LocationServiceProtocol.swift │ │ │ ├── LocationWitness.swift │ │ │ └── iOS │ │ │ │ ├── IosLocationCoordinatePayloadStruct.swift │ │ │ │ ├── IosLocationFloorPayloadStruct.swift │ │ │ │ ├── IosLocationPayload.swift │ │ │ │ └── IosLocationSourceInformationPayloadStruct.swift │ │ ├── SystemInfo │ │ │ ├── OsName.swift │ │ │ ├── PathMonitorManager.swift │ │ │ ├── SystemInfoCellularProviderPayloadStruct.swift │ │ │ ├── SystemInfoDevicePayloadStruct.swift │ │ │ ├── SystemInfoNetworkCellularPayloadStruct.swift │ │ │ ├── SystemInfoNetworkPayloadStruct.swift │ │ │ ├── SystemInfoNetworkWifiPayloadStruct.swift │ │ │ ├── SystemInfoNetworkWiredPayloadStruct.swift │ │ │ ├── SystemInfoOsPayloadStruct.swift │ │ │ ├── SystemInfoOsVersionPayloadStruct.swift │ │ │ ├── SystemInfoPayload.swift │ │ │ ├── SystemInfoWitness.swift │ │ │ └── WifiInformation.swift │ │ ├── Witness.swift │ │ └── WitnessModule.swift │ └── extensions │ │ ├── Data.swift │ │ ├── KeyedEncodingContainer.swift │ │ └── String.swift └── keccak │ ├── include │ └── keccak.h │ └── keccak.c └── Tests ├── LinuxMain.swift └── XyoClientTests ├── Account ├── Account.swift ├── AccountServices.swift └── Address.swift ├── BoundWitness └── BoundWitnessBuilder.swift ├── Panel └── Panel.swift ├── PrevioiusHashStore └── CoreDataPreviousHashStoreTests.swift ├── TestData ├── TestBoundWitnessSequence.swift ├── TestPayload1.swift └── TestPayload2.swift ├── Wallet ├── Wallet.swift └── WalletVectors.swift ├── Witness └── Location │ ├── Generic │ └── LocationPayloadTests.swift │ └── LocationWitness.swift └── XCTestManifests.swift /.github/workflows/build-beta.yml: -------------------------------------------------------------------------------- 1 | name: build-beta 2 | 3 | on: 4 | push: 5 | branches: 6 | - beta 7 | pull_request: 8 | branches: 9 | - beta 10 | 11 | jobs: 12 | build: 13 | runs-on: macos-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | # Use until swift 6 support is added to the default setup-swift action 18 | # https://github.com/swift-actions/setup-swift 19 | # https://github.com/swift-actions/setup-swift/pull/684 20 | # - uses: swift-actions/setup-swift@v2.2 21 | - uses: JoelBCarter/setup-swift@feat/swift-6 22 | with: 23 | swift-version: "6.0" 24 | - name: Build 25 | run: swift build -v 26 | - name: Run tests 27 | run: swift test -v 28 | -------------------------------------------------------------------------------- /.github/workflows/build-main.yml: -------------------------------------------------------------------------------- 1 | name: build-main 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | # pull_request: 8 | # branches: 9 | # - main 10 | 11 | jobs: 12 | build: 13 | runs-on: macos-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | # Use until swift 6 support is added to the default setup-swift action 18 | # https://github.com/swift-actions/setup-swift 19 | # https://github.com/swift-actions/setup-swift/pull/684 20 | # - uses: swift-actions/setup-swift@v2.2 21 | - uses: JoelBCarter/setup-swift@feat/swift-6 22 | with: 23 | swift-version: "6.0" 24 | - name: Build 25 | run: swift build -v 26 | - name: Run tests 27 | run: swift test -v 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /.swift-format: -------------------------------------------------------------------------------- 1 | { 2 | "fileScopedDeclarationPrivacy": { 3 | "accessLevel": "private" 4 | }, 5 | "indentConditionalCompilationBlocks": true, 6 | "indentSwitchCaseLabels": false, 7 | "indentation": { 8 | "spaces": 4 9 | }, 10 | "lineBreakAroundMultilineExpressionChainComponents": false, 11 | "lineBreakBeforeControlFlowKeywords": false, 12 | "lineBreakBeforeEachArgument": false, 13 | "lineBreakBeforeEachGenericRequirement": false, 14 | "lineLength": 100, 15 | "maximumBlankLines": 1, 16 | "multiElementCollectionTrailingCommas": true, 17 | "noAssignmentInExpressions": { 18 | "allowedFunctions": [ 19 | "XCTAssertNoThrow" 20 | ] 21 | }, 22 | "prioritizeKeepingFunctionOutputTogether": false, 23 | "respectsExistingLineBreaks": true, 24 | "rules": { 25 | "AllPublicDeclarationsHaveDocumentation": false, 26 | "AlwaysUseLiteralForEmptyCollectionInit": false, 27 | "AlwaysUseLowerCamelCase": true, 28 | "AmbiguousTrailingClosureOverload": true, 29 | "BeginDocumentationCommentWithOneLineSummary": false, 30 | "DoNotUseSemicolons": true, 31 | "DontRepeatTypeInStaticProperties": true, 32 | "FileScopedDeclarationPrivacy": true, 33 | "FullyIndirectEnum": true, 34 | "GroupNumericLiterals": true, 35 | "IdentifiersMustBeASCII": true, 36 | "NeverForceUnwrap": false, 37 | "NeverUseForceTry": false, 38 | "NeverUseImplicitlyUnwrappedOptionals": false, 39 | "NoAccessLevelOnExtensionDeclaration": true, 40 | "NoAssignmentInExpressions": true, 41 | "NoBlockComments": true, 42 | "NoCasesWithOnlyFallthrough": true, 43 | "NoEmptyTrailingClosureParentheses": true, 44 | "NoLabelsInCasePatterns": true, 45 | "NoLeadingUnderscores": false, 46 | "NoParensAroundConditions": true, 47 | "NoPlaygroundLiterals": true, 48 | "NoVoidReturnOnFunctionSignature": true, 49 | "OmitExplicitReturns": false, 50 | "OneCasePerLine": true, 51 | "OneVariableDeclarationPerLine": true, 52 | "OnlyOneTrailingClosureArgument": true, 53 | "OrderedImports": true, 54 | "ReplaceForEachWithForLoop": true, 55 | "ReturnVoidInsteadOfEmptyTuple": true, 56 | "TypeNamesShouldBeCapitalized": true, 57 | "UseEarlyExits": false, 58 | "UseExplicitNilCheckInConditions": true, 59 | "UseLetInEveryBoundCaseVariable": true, 60 | "UseShorthandTypeNames": true, 61 | "UseSingleLinePropertyGetter": true, 62 | "UseSynthesizedInitializer": true, 63 | "UseTripleSlashForDocumentationComments": true, 64 | "UseWhereClausesInForLoops": false, 65 | "ValidateDocumentationComments": false 66 | }, 67 | "spacesAroundRangeFormationOperators": false, 68 | "tabWidth": 8, 69 | "version": 1 70 | } 71 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/XyoClient.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 35 | 41 | 42 | 43 | 44 | 45 | 55 | 56 | 60 | 61 | 65 | 66 | 67 | 68 | 74 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/sdk-xyo-client-swift.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 67 | 68 | 74 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "Alamofire", 4 | "Arie", 5 | "boundwitness", 6 | "hmac", 7 | "keccak", 8 | "Keychain", 9 | "Protobuf", 10 | "secp", 11 | "Trouw", 12 | "unkeyed" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | GNU LESSER GENERAL PUBLIC LICENSE 3 | Version 3, 29 June 2007 4 | 5 | Copyright (C) 2007 Free Software Foundation, Inc. 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | 10 | This version of the GNU Lesser General Public License incorporates 11 | the terms and conditions of version 3 of the GNU General Public 12 | License, supplemented by the additional permissions listed below. 13 | 14 | 0. Additional Definitions. 15 | 16 | As used herein, "this License" refers to version 3 of the GNU Lesser 17 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 18 | General Public License. 19 | 20 | "The Library" refers to a covered work governed by this License, 21 | other than an Application or a Combined Work as defined below. 22 | 23 | An "Application" is any work that makes use of an interface provided 24 | by the Library, but which is not otherwise based on the Library. 25 | Defining a subclass of a class defined by the Library is deemed a mode 26 | of using an interface provided by the Library. 27 | 28 | A "Combined Work" is a work produced by combining or linking an 29 | Application with the Library. The particular version of the Library 30 | with which the Combined Work was made is also called the "Linked 31 | Version". 32 | 33 | The "Minimal Corresponding Source" for a Combined Work means the 34 | Corresponding Source for the Combined Work, excluding any source code 35 | for portions of the Combined Work that, considered in isolation, are 36 | based on the Application, and not on the Linked Version. 37 | 38 | The "Corresponding Application Code" for a Combined Work means the 39 | object code and/or source code for the Application, including any data 40 | and utility programs needed for reproducing the Combined Work from the 41 | Application, but excluding the System Libraries of the Combined Work. 42 | 43 | 1. Exception to Section 3 of the GNU GPL. 44 | 45 | You may convey a covered work under sections 3 and 4 of this License 46 | without being bound by section 3 of the GNU GPL. 47 | 48 | 2. Conveying Modified Versions. 49 | 50 | If you modify a copy of the Library, and, in your modifications, a 51 | facility refers to a function or data to be supplied by an Application 52 | that uses the facility (other than as an argument passed when the 53 | facility is invoked), then you may convey a copy of the modified 54 | version: 55 | 56 | a) under this License, provided that you make a good faith effort to 57 | ensure that, in the event an Application does not supply the 58 | function or data, the facility still operates, and performs 59 | whatever part of its purpose remains meaningful, or 60 | 61 | b) under the GNU GPL, with none of the additional permissions of 62 | this License applicable to that copy. 63 | 64 | 3. Object Code Incorporating Material from Library Header Files. 65 | 66 | The object code form of an Application may incorporate material from 67 | a header file that is part of the Library. You may convey such object 68 | code under terms of your choice, provided that, if the incorporated 69 | material is not limited to numerical parameters, data structure 70 | layouts and accessors, or small macros, inline functions and templates 71 | (ten or fewer lines in length), you do both of the following: 72 | 73 | a) Give prominent notice with each copy of the object code that the 74 | Library is used in it and that the Library and its use are 75 | covered by this License. 76 | 77 | b) Accompany the object code with a copy of the GNU GPL and this license 78 | document. 79 | 80 | 4. Combined Works. 81 | 82 | You may convey a Combined Work under terms of your choice that, 83 | taken together, effectively do not restrict modification of the 84 | portions of the Library contained in the Combined Work and reverse 85 | engineering for debugging such modifications, if you also do each of 86 | the following: 87 | 88 | a) Give prominent notice with each copy of the Combined Work that 89 | the Library is used in it and that the Library and its use are 90 | covered by this License. 91 | 92 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 93 | document. 94 | 95 | c) For a Combined Work that displays copyright notices during 96 | execution, include the copyright notice for the Library among 97 | these notices, as well as a reference directing the user to the 98 | copies of the GNU GPL and this license document. 99 | 100 | d) Do one of the following: 101 | 102 | 0) Convey the Minimal Corresponding Source under the terms of this 103 | License, and the Corresponding Application Code in a form 104 | suitable for, and under terms that permit, the user to 105 | recombine or relink the Application with a modified version of 106 | the Linked Version to produce a modified Combined Work, in the 107 | manner specified by section 6 of the GNU GPL for conveying 108 | Corresponding Source. 109 | 110 | 1) Use a suitable shared library mechanism for linking with the 111 | Library. A suitable mechanism is one that (a) uses at run time 112 | a copy of the Library already present on the user's computer 113 | system, and (b) will operate properly with a modified version 114 | of the Library that is interface-compatible with the Linked 115 | Version. 116 | 117 | e) Provide Installation Information, but only if you would otherwise 118 | be required to provide such information under section 6 of the 119 | GNU GPL, and only to the extent that such information is 120 | necessary to install and execute a modified version of the 121 | Combined Work produced by recombining or relinking the 122 | Application with a modified version of the Linked Version. (If 123 | you use option 4d0, the Installation Information must accompany 124 | the Minimal Corresponding Source and Corresponding Application 125 | Code. If you use option 4d1, you must provide the Installation 126 | Information in the manner specified by section 6 of the GNU GPL 127 | for conveying Corresponding Source.) 128 | 129 | 5. Combined Libraries. 130 | 131 | You may place library facilities that are a work based on the 132 | Library side by side in a single library together with other library 133 | facilities that are not Applications and are not covered by this 134 | License, and convey such a combined library under terms of your 135 | choice, if you do both of the following: 136 | 137 | a) Accompany the combined library with a copy of the same work based 138 | on the Library, uncombined with any other library facilities, 139 | conveyed under the terms of this License. 140 | 141 | b) Give prominent notice with the combined library that part of it 142 | is a work based on the Library, and explaining where to find the 143 | accompanying uncombined form of the same work. 144 | 145 | 6. Revised Versions of the GNU Lesser General Public License. 146 | 147 | The Free Software Foundation may publish revised and/or new versions 148 | of the GNU Lesser General Public License from time to time. Such new 149 | versions will be similar in spirit to the present version, but may 150 | differ in detail to address new problems or concerns. 151 | 152 | Each version is given a distinguishing version number. If the 153 | Library as you received it specifies that a certain numbered version 154 | of the GNU Lesser General Public License "or any later version" 155 | applies to it, you have the option of following the terms and 156 | conditions either of that published version or of any later version 157 | published by the Free Software Foundation. If the Library as you 158 | received it does not specify a version number of the GNU Lesser 159 | General Public License, you may choose any version of the GNU Lesser 160 | General Public License ever published by the Free Software Foundation. 161 | 162 | If the Library as you received it specifies that a proxy can decide 163 | whether future versions of the GNU Lesser General Public License shall 164 | apply, that proxy's public statement of acceptance of any version is 165 | permanent authorization for you to choose that version for the 166 | Library. 167 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "2cd4d5900382533f5ce2617d5c3c1948420439df3c97e468635ae8588d758b78", 3 | "pins" : [ 4 | { 5 | "identity" : "alamofire", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/Alamofire/Alamofire.git", 8 | "state" : { 9 | "revision" : "513364f870f6bfc468f9d2ff0a95caccc10044c5", 10 | "version" : "5.10.2" 11 | } 12 | }, 13 | { 14 | "identity" : "bigint", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/attaswift/BigInt.git", 17 | "state" : { 18 | "revision" : "114343a705df4725dfe7ab8a2a326b8883cfd79c", 19 | "version" : "5.5.1" 20 | } 21 | }, 22 | { 23 | "identity" : "cryptoswift", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/krzyzanowskim/CryptoSwift.git", 26 | "state" : { 27 | "revision" : "729e01bc9b9dab466ac85f21fb9ee2bc1c61b258", 28 | "version" : "1.8.4" 29 | } 30 | }, 31 | { 32 | "identity" : "swift-secp256k1", 33 | "kind" : "remoteSourceControl", 34 | "location" : "https://github.com/21-DOT-DEV/swift-secp256k1", 35 | "state" : { 36 | "revision" : "57ce9af6db14e0114af631ace25231a9d0ccccbd", 37 | "version" : "0.18.0" 38 | } 39 | } 40 | ], 41 | "version" : 3 42 | } 43 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:6.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "XyoClient", 7 | platforms: [ 8 | .macOS(.v12), 9 | .iOS(.v13), 10 | .tvOS(.v15), 11 | .watchOS(.v8), 12 | ], 13 | products: [ 14 | .library( 15 | name: "XyoClient", 16 | targets: ["XyoClient"] 17 | ) 18 | ], 19 | dependencies: [ 20 | .package( 21 | url: "https://github.com/21-DOT-DEV/swift-secp256k1", .upToNextMinor(from: "0.18.0")), 22 | .package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.2.0")), 23 | .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", from: "1.8.3"), 24 | .package(url: "https://github.com/attaswift/BigInt.git", from: "5.4.0"), 25 | ], 26 | targets: [ 27 | .target(name: "keccak"), 28 | .target( 29 | name: "XyoClient", 30 | dependencies: [ 31 | .product(name: "secp256k1", package: "swift-secp256k1"), 32 | "Alamofire", 33 | "keccak", 34 | "CryptoSwift", 35 | "BigInt", 36 | ] 37 | ), 38 | .testTarget( 39 | name: "XyoClientTests", 40 | dependencies: ["XyoClient"]), 41 | ], 42 | 43 | swiftLanguageModes: [.v5] 44 | ) 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![logo][]](https://xyo.network) 2 | 3 | # sdk-xyo-client-swift 4 | 5 | [![main-build][]][main-build-link] 6 | [![codacy-badge][]][codacy-link] 7 | [![codeclimate-badge][]][codeclimate-link] 8 | 9 | > The XYO Foundation provides this source code available in our efforts to advance the understanding of the XYO Protocol and its possible uses. We continue to maintain this software in the interest of developer education. Usage of this source code is not intended for production. 10 | 11 | ## Table of Contents 12 | 13 | - [sdk-xyo-client-swift](#sdk-xyo-client-swift) 14 | - [Table of Contents](#table-of-contents) 15 | - [Description](#description) 16 | - [Instructions](#instructions) 17 | - [Add Package](#add-package) 18 | - [Configure API](#configure-api) 19 | - [Configure Witnesses](#configure-witnesses) 20 | - [Configure Panel](#configure-panel) 21 | - [Generate BoundWitness report](#generate-boundwitness-report) 22 | - [Maintainers](#maintainers) 23 | - [License](#license) 24 | - [Credits](#credits) 25 | 26 | ## Description 27 | 28 | Primary SDK for using the XYO Protocol 2.0 from Swift. Designed to work in both Mac OS and iOS. 29 | 30 | ## Instructions 31 | 32 | ### Add Package 33 | 34 | ```swift 35 | dependencies: [ 36 | .package(url: "https://github.com/XYOracleNetwork/app-ios-witness-demo-swiftui.git", .upToNextMajor(from: "3.0.0")), 37 | ], 38 | ``` 39 | 40 | ### Configure API 41 | 42 | Setup which network you'd like to write to by configuring the Domain & Archivist Module Name 43 | 44 | ```swift 45 | let apiDomain = "https://beta.api.archivist.xyo.network" 46 | let archive = "Archivist" 47 | ``` 48 | 49 | ### Configure Witnesses 50 | 51 | Configure your desired witnesses (Basic, System Info, Location, etc.) 52 | 53 | ```swift 54 | let basicWitness = BasicWitness { 55 | Payload("network.xyo.basic") 56 | } 57 | let systemInfoWitness = SystemInfoWitness(allowPathMonitor: true) 58 | let locationWitness = LocationWitness() 59 | ``` 60 | 61 | ### Configure Panel 62 | 63 | Use the Witnesses & Archivist config to create a Panel 64 | 65 | ```swift 66 | let panel = XyoPanel( 67 | account: self.panelAccount, 68 | witnesses: [ 69 | basicWitness, 70 | systemInfoWitness, 71 | locationWitness 72 | ], 73 | apiDomain: apiDomain, 74 | apiModule: apiModule 75 | ) 76 | ``` 77 | 78 | ### Generate BoundWitness report 79 | 80 | Call `.report()` to return the witnessed Payloads 81 | 82 | ```swift 83 | let payloads = await panel.report() 84 | ``` 85 | 86 | or, for more detailed information, call `.reportQuery()` to return a `ModuleQuery` result containing the `BoundWitness`, `Payloads`, & any `Errors` (if present) 87 | 88 | ```swift 89 | let result = await panel.reportQuery() 90 | let bw = result.bw 91 | let payloads = result.payloads 92 | let errors = result.errors 93 | ``` 94 | 95 | ## Maintainers 96 | 97 | - [Arie Trouw](https://arietrouw.com/) 98 | - [Joel Carter](https://joelbcarter.com) 99 | 100 | ## License 101 | 102 | See the [LICENSE](LICENSE) file for license details 103 | 104 | ## Credits 105 | 106 | Made with 🔥 and ❄️ by [XYO](https://xyo.network) 107 | 108 | [logo]: https://cdn.xy.company/img/brand/XYO_full_colored.png 109 | [main-build]: https://github.com/XYOracleNetwork/sdk-xyo-client-swift/actions/workflows/build-main.yml/badge.svg 110 | [main-build-link]: https://github.com/XYOracleNetwork/sdk-xyo-client-swift/actions/workflows/build-main.yml 111 | [codacy-badge]: https://app.codacy.com/project/badge/Grade/c0ba3913b706492f99077eb5e6b4760c 112 | [codacy-link]: https://www.codacy.com/gh/XYOracleNetwork/sdk-xyo-client-swift/dashboard?utm_source=github.com&utm_medium=referral&utm_content=XYOracleNetwork/sdk-xyo-client-swift&utm_campaign=Badge_Grade 113 | [codeclimate-badge]: https://api.codeclimate.com/v1/badges/d051b36c73cd52e4030a/maintainability 114 | [codeclimate-link]: https://codeclimate.com/github/XYOracleNetwork/sdk-xyo-client-swift/maintainability 115 | -------------------------------------------------------------------------------- /Sources/XyoClient/Address/Account.swift: -------------------------------------------------------------------------------- 1 | import BigInt 2 | import Foundation 3 | import secp256k1 4 | 5 | public func dataFromHex(_ hex: String) -> Data? { 6 | let hex = hex.replacingOccurrences(of: " ", with: "") // Remove any spaces 7 | let len = hex.count 8 | 9 | // Ensure even length (hex must be in pairs) 10 | guard len % 2 == 0 else { return nil } 11 | 12 | var data = Data(capacity: len / 2) 13 | 14 | var index = hex.startIndex 15 | for _ in 0..<(len / 2) { 16 | let nextIndex = hex.index(index, offsetBy: 2) 17 | guard let byte = UInt8(hex[index.. any AccountInstance { 99 | return Account(key) 100 | } 101 | 102 | public static func fromPrivateKey(_ key: String) throws -> AccountInstance { 103 | guard let data = Data(key) else { throw AccountError.invalidPrivateKey } 104 | return Account(data) 105 | } 106 | 107 | public static func random() -> AccountInstance { 108 | return Account(generateRandomBytes()) 109 | } 110 | 111 | init(_ privateKey: Data) { 112 | self._privateKey = privateKey 113 | } 114 | 115 | public func sign(_ hash: Hash) throws -> Signature { 116 | let context = try secp256k1.Context.create() 117 | guard let privateKey = self.privateKey else { throw AccountError.invalidPrivateKey } 118 | 119 | defer { secp256k1_context_destroy(context) } 120 | 121 | var signature = secp256k1_ecdsa_signature() 122 | 123 | let arrayHash = Array(hash) 124 | let privateKeyArray = Array(privateKey) 125 | 126 | guard secp256k1_ecdsa_sign(context, &signature, arrayHash, privateKeyArray, nil, nil) == 1 127 | else { 128 | throw secp256k1Error.underlyingCryptoError 129 | } 130 | 131 | var signature2 = secp256k1_ecdsa_signature() 132 | withUnsafeMutableBytes(of: &signature2) { signature2Bytes in 133 | withUnsafeBytes(of: &signature) { signatureBytes in 134 | for i in 0...31 { 135 | signature2Bytes[i] = signatureBytes[31 - i] 136 | signature2Bytes[i + 32] = signatureBytes[63 - i] 137 | } 138 | } 139 | } 140 | 141 | let rawRepresentation = Data( 142 | bytes: &signature2.data, 143 | count: MemoryLayout.size(ofValue: signature2.data) 144 | ) 145 | 146 | let result = try secp256k1.Signing.ECDSASignature(dataRepresentation: rawRepresentation) 147 | .dataRepresentation 148 | try self.storePreviousHash(hash) 149 | return result 150 | } 151 | 152 | public func sign(hash: String) throws -> Signature { 153 | guard let message = hash.hexToData() else { throw AccountError.invalidMessage } 154 | return try self.sign(message) 155 | } 156 | 157 | public func verify(_ msg: Data, _ signature: Signature) -> Bool { 158 | return false 159 | } 160 | 161 | internal func retrievePreviousHash() throws -> Hash? { 162 | guard let address = self.address else { throw AccountError.invalidAddress } 163 | return Account.previousHashStore.getItem(address: address) 164 | } 165 | 166 | internal func storePreviousHash(_ newValue: Hash?) throws { 167 | guard let address = self.address else { throw AccountError.invalidAddress } 168 | if let previousHash = newValue { 169 | Account.previousHashStore.setItem(address: address, previousHash: previousHash) 170 | } 171 | } 172 | 173 | public static func getCompressedPublicKeyFrom(privateKey: Data) throws -> Data { 174 | guard 175 | let uncompressedPublicKey = XyoAddress(privateKey: privateKey.toHexString()) 176 | .publicKeyBytes 177 | else { 178 | throw WalletError.failedToGetPublicKey 179 | } 180 | return try Account.getCompressedPublicKeyFrom(uncompressedPublicKey: uncompressedPublicKey) 181 | } 182 | 183 | public static func getCompressedPublicKeyFrom(uncompressedPublicKey: Data) throws -> Data { 184 | // Ensure the input key is exactly 64 bytes 185 | guard uncompressedPublicKey.count == 64 else { 186 | throw AccountError.invalidPrivateKey 187 | } 188 | 189 | // Extract x and y coordinates 190 | let x = uncompressedPublicKey.prefix(32) // First 32 bytes are x 191 | let y = uncompressedPublicKey.suffix(32) // Last 32 bytes are y 192 | 193 | // Convert y to an integer to determine parity 194 | let yInt = BigInt(y.toHex(), radix: 16)! 195 | let isEven = yInt % 2 == 0 196 | 197 | // Determine the prefix based on the parity of y 198 | let prefix: UInt8 = isEven ? 0x02 : 0x03 199 | 200 | // Construct the compressed key: prefix + x 201 | var compressedKey = Data([prefix]) // Start with the prefix 202 | compressedKey.append(x) // Append the x-coordinate 203 | 204 | return compressedKey 205 | } 206 | 207 | public static func privateKeyObjectFromKey(_ key: Data) throws -> secp256k1.Signing.PrivateKey { 208 | return try secp256k1.Signing.PrivateKey( 209 | dataRepresentation: key, format: .uncompressed) 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /Sources/XyoClient/Address/AccountInstance.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public typealias Signature = Data 4 | 5 | public protocol PublicKeyInstance { 6 | var address: Address? { get } 7 | func verify(_ msg: Data, _ signature: Signature) -> Bool 8 | } 9 | 10 | public protocol PrivateKeyInstance: PublicKeyInstance { 11 | func sign(_ hash: Hash) throws -> Signature 12 | } 13 | 14 | public protocol AccountInstance: PrivateKeyInstance { 15 | var previousHash: Hash? { get } 16 | var privateKey: Data? { get } 17 | var publicKey: Data? { get } 18 | var publicKeyUncompressed: Data? { get } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /Sources/XyoClient/Address/AccountServices.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public class AccountServices { 4 | public static func getNamedAccount(name: String = "DefaultAccount") -> AccountInstance { 5 | // let _ = removeFromKeychain(key: name) 6 | if let existingAccount = getStoredAccount(name: name) { 7 | return existingAccount 8 | } else { 9 | let account = Account.random() 10 | if let privateKey = account.privateKey?.toHex() { 11 | if !saveToKeychain(key: name, value: privateKey) { 12 | // TODO: Avoiding throw here but better handling of this 13 | // case would be desirable 14 | return account 15 | } 16 | } 17 | return account 18 | } 19 | } 20 | 21 | private static func getStoredAccount(name: String) -> AccountInstance? { 22 | // Lookup previously saved private key if it exists 23 | if let storedPrivateKeyString = getFromKeychain(key: name) { 24 | if let parsedPrivateKeyData = Data.dataFrom(hexString: storedPrivateKeyString) { 25 | return Account.fromPrivateKey(parsedPrivateKeyData) 26 | } 27 | } 28 | // Otherwise, return nil 29 | return nil 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /Sources/XyoClient/Address/AccountStatic.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol AccountStatic { 4 | static func fromPrivateKey(_ key: Data) throws -> AccountInstance 5 | static func fromPrivateKey(_ key: String) throws -> AccountInstance 6 | static func random() -> AccountInstance 7 | static var previousHashStore: PreviousHashStore { get set } 8 | } 9 | -------------------------------------------------------------------------------- /Sources/XyoClient/Address/Address.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public typealias Address = Data 4 | -------------------------------------------------------------------------------- /Sources/XyoClient/Address/Error.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum XyoAddressError: Error { 4 | case invalidPrivateKey 5 | case invalidPrivateKeyLength 6 | case invalidHash 7 | case signingFailed 8 | } 9 | -------------------------------------------------------------------------------- /Sources/XyoClient/Address/XyoAddress.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import secp256k1 3 | 4 | public class XyoAddress { 5 | 6 | private var _privateKey: secp256k1.Signing.PrivateKey? 7 | 8 | public init(_ privateKey: Data? = generateRandomBytes()) { 9 | self._privateKey = try? secp256k1.Signing.PrivateKey( 10 | dataRepresentation: privateKey ?? generateRandomBytes(32), format: .uncompressed) 11 | } 12 | 13 | convenience init(privateKey: String) { 14 | self.init(privateKey.hexToData()) 15 | } 16 | 17 | public var privateKey: secp256k1.Signing.PrivateKey? { 18 | return _privateKey 19 | } 20 | 21 | public var privateKeyBytes: Data? { 22 | return _privateKey?.dataRepresentation 23 | } 24 | 25 | public var privateKeyHex: String? { 26 | return privateKeyBytes?.toHex(64) 27 | } 28 | 29 | public var publicKey: secp256k1.Signing.PublicKey? { 30 | return _privateKey?.publicKey 31 | } 32 | 33 | public var publicKeyBytes: Data? { 34 | return _privateKey?.publicKey.dataRepresentation.subdata( 35 | in: 1..<(_privateKey?.publicKey.dataRepresentation.count ?? 0)) 36 | } 37 | 38 | public var publicKeyHex: String? { 39 | return publicKeyBytes?.toHex(128) 40 | } 41 | 42 | public var keccakBytes: Data? { 43 | return publicKeyBytes?.keccak256() 44 | } 45 | 46 | public var keccakHex: String? { 47 | guard let bytes = keccakBytes else { return nil } 48 | return bytes.toHex(64) 49 | } 50 | 51 | public var address: String? { 52 | return self.addressHex 53 | } 54 | 55 | public var addressBytes: Data? { 56 | guard let keccakBytes = keccakBytes else { return nil } 57 | return keccakBytes.subdata(in: 12.. String? { 66 | let message = hash.hexToData() 67 | guard message != nil else { return nil } 68 | let sig = self.signature(message!) 69 | return sig?.dataRepresentation.toHex() 70 | } 71 | 72 | public func signature(_ hash: Data) -> secp256k1.Signing.ECDSASignature? { 73 | do { 74 | let context = try secp256k1.Context.create() 75 | 76 | defer { secp256k1_context_destroy(context) } 77 | 78 | var signature = secp256k1_ecdsa_signature() 79 | 80 | let arrayHash = Array(hash) 81 | let privKey = Array(self._privateKey!.dataRepresentation) 82 | 83 | guard secp256k1_ecdsa_sign(context, &signature, arrayHash, privKey, nil, nil) == 1 84 | else { 85 | throw secp256k1Error.underlyingCryptoError 86 | } 87 | 88 | var signature2 = secp256k1_ecdsa_signature() 89 | withUnsafeMutableBytes(of: &signature2) { signature2Bytes in 90 | withUnsafeBytes(of: &signature) { signatureBytes in 91 | for i in 0...31 { 92 | signature2Bytes[i] = signatureBytes[31 - i] 93 | signature2Bytes[i + 32] = signatureBytes[63 - i] 94 | } 95 | } 96 | } 97 | 98 | let rawRepresentation = Data( 99 | bytes: &signature2.data, 100 | count: MemoryLayout.size(ofValue: signature2.data) 101 | ) 102 | 103 | return try secp256k1.Signing.ECDSASignature(dataRepresentation: rawRepresentation) 104 | } catch { 105 | return nil 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Sources/XyoClient/Address/generateRandomBytes.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public func generateRandomBytes(_ count: Int = 32) -> Data { 4 | 5 | var keyData = Data(count: count) 6 | let result = keyData.withUnsafeMutableBytes { 7 | SecRandomCopyBytes(kSecRandomDefault, 32, $0.baseAddress!) 8 | } 9 | if result == errSecSuccess { 10 | return keyData 11 | } else { 12 | print("Problem generating random bytes") 13 | var simpleRandom = Data() 14 | var randomGenerator = SystemRandomNumberGenerator() 15 | while simpleRandom.count < count { 16 | simpleRandom.append(contentsOf: [UInt8(randomGenerator.next())]) 17 | } 18 | return simpleRandom 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/XyoClient/Api/ApiConfig.swift: -------------------------------------------------------------------------------- 1 | public class ApiConfig { 2 | var apiDomain: String 3 | var token: String? 4 | 5 | public init(_ apiDomain: String, _ token: String? = nil) { 6 | self.apiDomain = apiDomain 7 | self.token = token 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Sources/XyoClient/Api/ApiResponseEnvelope.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public class ApiResponseEnvelope: Decodable { 4 | public var data: T? = nil 5 | 6 | enum CodingKeys: String, CodingKey { 7 | case data 8 | } 9 | 10 | // Required initializer to conform to Decodable 11 | public required init(from decoder: Decoder) throws { 12 | let container = try decoder.container(keyedBy: CodingKeys.self) 13 | self.data = try container.decodeIfPresent(T.self, forKey: .data) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/XyoClient/Api/Archivist/ArchivistApiClient.swift: -------------------------------------------------------------------------------- 1 | import Alamofire 2 | import Foundation 3 | 4 | public class XyoArchivistApiClient { 5 | 6 | public static let DefaultApiDomain: String = 7 | ProcessInfo.processInfo.environment["XYO_API_DOMAIN"] ?? "https://api.archivist.xyo.network" 8 | public static let DefaultArchivist: String = 9 | ProcessInfo.processInfo.environment["XYO_API_MODULE"] ?? "Archivist" 10 | 11 | private static let ArchivistInsertQuerySchema = "network.xyo.query.archivist.insert" 12 | private static let ArchivistInsertQuery: EncodablePayloadInstance = EncodablePayloadInstance( 13 | ArchivistInsertQuerySchema) 14 | 15 | let config: XyoArchivistApiConfig 16 | let queryAccount: AccountInstance 17 | public var authenticated: Bool { 18 | return self.token != nil 19 | } 20 | 21 | public var token: String? { 22 | get { 23 | return self.config.token 24 | } 25 | set { 26 | self.config.token = newValue 27 | } 28 | } 29 | 30 | public var url: String { 31 | return "\(self.config.apiDomain)/\(self.config.apiModule)" 32 | } 33 | 34 | private init(_ config: XyoArchivistApiConfig, _ account: AccountInstance?) { 35 | self.config = config 36 | self.queryAccount = 37 | account ?? AccountServices.getNamedAccount(name: "DefaultArchivistApiClientAccount") 38 | } 39 | 40 | public func insert( 41 | payloads: [EncodablePayloadInstance], 42 | completion: @escaping ([EncodablePayloadInstance]?, Error?) -> Void 43 | ) { 44 | do { 45 | // Build QueryBoundWitness 46 | let (bw, signed) = try BoundWitnessBuilder() 47 | .payloads(payloads) 48 | .signer(self.queryAccount) 49 | .query(XyoArchivistApiClient.ArchivistInsertQuery) 50 | .build() 51 | 52 | // Perform the request 53 | AF.request( 54 | self.url, 55 | method: .post, 56 | parameters: ModuleQueryResult(bw: bw, payloads: signed), 57 | encoder: JSONParameterEncoder.default 58 | ) 59 | .validate() 60 | .responseData { response in 61 | switch response.result { 62 | case .success(let responseData): 63 | do { 64 | // Decode the response data 65 | let decodedResponse = try JSONDecoder().decode( 66 | ApiResponseEnvelope.self, from: responseData 67 | ) 68 | 69 | // Check if the response data matches the expected result 70 | if decodedResponse.data?.bw.typedPayload.payload_hashes.count 71 | == payloads.count 72 | { 73 | // Return the payloads array in case of success 74 | completion(payloads, nil) 75 | } else { 76 | // Return an empty array if the counts don't match 77 | completion([], nil) 78 | } 79 | } catch { 80 | // Pass any decoding errors to the completion handler 81 | completion(nil, error) 82 | } 83 | 84 | case .failure(let error): 85 | // Pass any request errors to the completion handler 86 | completion(nil, error) 87 | } 88 | } 89 | 90 | } catch { 91 | // Handle synchronous errors (like errors from the BoundWitnessBuilder) 92 | completion(nil, error) 93 | } 94 | } 95 | 96 | @available(iOS 15, *) 97 | public func insert(payloads: [EncodablePayloadInstance]) async throws 98 | -> [EncodablePayloadInstance] 99 | { 100 | // Build QueryBoundWitness 101 | let (bw, signed) = try BoundWitnessBuilder() 102 | .payloads(payloads) 103 | .signer(self.queryAccount) 104 | .query(XyoArchivistApiClient.ArchivistInsertQuery) 105 | .build() 106 | 107 | // Perform the request and await the result 108 | let responseData = try await AF.request( 109 | self.url, 110 | method: .post, 111 | parameters: ModuleQueryResult(bw: bw, payloads: signed), 112 | encoder: JSONParameterEncoder.default 113 | ) 114 | .validate() 115 | .serializingData() 116 | .value 117 | 118 | // let responseString = String(data: responseData, encoding: .utf8) 119 | // print(responseString ?? "Failed to convert response data to String") 120 | 121 | // Attempt to decode the response data 122 | let decodedResponse = try JSONDecoder().decode( 123 | ApiResponseEnvelope.self, from: responseData) 124 | if decodedResponse.data?.bw.typedPayload.payload_hashes.count == payloads.count { 125 | // TODO: Deeper guard checks like hash, etc. 126 | // TODO: Return Success 127 | return payloads 128 | } else { 129 | // TODO: Indicate Error 130 | return [] 131 | } 132 | } 133 | 134 | public static func get(_ config: XyoArchivistApiConfig) -> XyoArchivistApiClient { 135 | return XyoArchivistApiClient(config, Account.random()) 136 | } 137 | } 138 | 139 | extension XyoArchivistApiClient { 140 | static fileprivate let queue = DispatchQueue( 141 | label: "network.xyo.requests.queue", 142 | qos: .utility, 143 | attributes: [.concurrent] 144 | ) 145 | static fileprivate let mainQueue = DispatchQueue.main 146 | } 147 | -------------------------------------------------------------------------------- /Sources/XyoClient/Api/Archivist/ArchivistApiConfig.swift: -------------------------------------------------------------------------------- 1 | public class XyoArchivistApiConfig: ApiConfig { 2 | var apiModule: String 3 | public init(_ apiModule: String, _ apiDomain: String, _ token: String? = nil) { 4 | self.apiModule = apiModule 5 | super.init(apiDomain, token) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Sources/XyoClient/BoundWitness/BoundWitness.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | let BoundWitnessSchema = "network.xyo.boundwitness" 4 | 5 | public protocol BoundWitnessFields { 6 | var addresses: [String] { get } 7 | var payload_hashes: [String] { get } 8 | var payload_schemas: [String] { get } 9 | var previous_hashes: [String?] { get } 10 | } 11 | 12 | public protocol EncodableBoundWitness: EncodablePayload, BoundWitnessFields, Encodable {} 13 | 14 | public protocol BoundWitness: EncodableBoundWitness, EncodablePayload, Payload, Codable {} 15 | 16 | public class BoundWitnessInstance: PayloadInstance { 17 | public var signatures: [String]? = nil 18 | 19 | public var addresses: [String] = [] 20 | 21 | public var payload_hashes: [String] = [] 22 | 23 | public var payload_schemas: [String] = [] 24 | 25 | public var previous_hashes: [String?] = [] 26 | 27 | public var query: String? = nil 28 | 29 | init() { 30 | super.init(BoundWitnessSchema) 31 | } 32 | 33 | enum CodingKeys: String, CodingKey { 34 | case addresses 35 | case payload_hashes 36 | case payload_schemas 37 | case previous_hashes 38 | case query 39 | } 40 | 41 | public required init(from decoder: Decoder) throws { 42 | let values = try decoder.container(keyedBy: CodingKeys.self) 43 | addresses = try values.decode([String].self, forKey: .addresses) 44 | payload_hashes = try values.decode([String].self, forKey: .payload_hashes) 45 | payload_schemas = try values.decode([String].self, forKey: .payload_schemas) 46 | previous_hashes = try values.decode([String?].self, forKey: .previous_hashes) 47 | query = try values.decodeIfPresent(String.self, forKey: .query) 48 | super.init(BoundWitnessSchema) 49 | } 50 | 51 | override public func encode(to encoder: Encoder) throws { 52 | var container = encoder.container(keyedBy: CodingKeys.self) 53 | try container.encode(addresses, forKey: .addresses) 54 | try container.encode(payload_hashes, forKey: .payload_hashes) 55 | try container.encode(payload_schemas, forKey: .payload_schemas) 56 | try container.encode(previous_hashes, forKey: .previous_hashes) 57 | try container.encodeIfPresent(query, forKey: .query) 58 | try super.encode(to: encoder) 59 | } 60 | } 61 | 62 | public typealias EncodableBoundWitnessWithMeta = EncodableWithCustomMetaInstance< 63 | BoundWitnessInstance, BoundWitnessMeta 64 | > 65 | 66 | public typealias BoundWitnessWithMeta = WithCustomMetaInstance< 67 | BoundWitnessInstance, BoundWitnessMeta 68 | > 69 | -------------------------------------------------------------------------------- /Sources/XyoClient/BoundWitness/BoundWitnessBuilder.swift: -------------------------------------------------------------------------------- 1 | import CommonCrypto 2 | import Foundation 3 | 4 | public enum BoundWitnessBuilderError: Error { 5 | case encodingError 6 | } 7 | 8 | public class BoundWitnessBuilder { 9 | private var _accounts: [AccountInstance] = [] 10 | private var _previous_hashes: [Hash?] = [] 11 | private var _payload_hashes: [Hash] = [] 12 | private var _payload_schemas: [String] = [] 13 | private var _payloads: [EncodablePayloadInstance] = [] 14 | private var _query: Hash? = nil 15 | 16 | public init() { 17 | } 18 | 19 | public func signer(_ account: AccountInstance) 20 | -> BoundWitnessBuilder 21 | { 22 | _accounts.append(account) 23 | _previous_hashes.append(account.previousHash) 24 | return self 25 | } 26 | 27 | public func signers(_ accounts: [AccountInstance]) -> BoundWitnessBuilder { 28 | _accounts.append(contentsOf: accounts) 29 | _previous_hashes.append(contentsOf: accounts.map { account in account.previousHash }) 30 | return self 31 | } 32 | 33 | public func payload(_ schema: String, _ payload: T) throws 34 | -> BoundWitnessBuilder 35 | { 36 | _payloads.append(payload) 37 | _payload_hashes.append(try PayloadBuilder.dataHash(from: payload)) 38 | _payload_schemas.append(schema) 39 | return self 40 | } 41 | 42 | public func payloads(_ payloads: [EncodablePayloadInstance]) throws -> BoundWitnessBuilder { 43 | _payloads.append(contentsOf: payloads) 44 | _payload_hashes.append( 45 | contentsOf: try payloads.map { payload in try PayloadBuilder.dataHash(from: payload) }) 46 | _payload_schemas.append(contentsOf: payloads.map { payload in payload.schema }) 47 | return self 48 | } 49 | 50 | public func query(_ payload: EncodablePayloadInstance) throws -> BoundWitnessBuilder { 51 | self._query = try PayloadBuilder.dataHash(from: payload) 52 | let _ = try self.payload(payload.schema, payload) 53 | return self 54 | } 55 | 56 | public func sign(hash: Hash) throws -> [Signature] { 57 | return try self._accounts.map { 58 | try $0.sign(hash) 59 | } 60 | } 61 | 62 | public func build() throws -> (EncodableBoundWitnessWithMeta, [EncodablePayloadInstance]) { 63 | let bw = BoundWitnessInstance() 64 | bw.addresses = _accounts.map { account in account.address!.toHex() } 65 | bw.previous_hashes = _previous_hashes.map { hash in hash?.toHex() } 66 | bw.payload_hashes = _payload_hashes.map { hash in hash.toHex() } 67 | bw.payload_schemas = _payload_schemas 68 | if _query != nil { 69 | bw.query = _query?.toHex() 70 | } 71 | let dataHash = try PayloadBuilder.dataHash(from: bw) 72 | let signatures = try self.sign(hash: dataHash).map { signature in signature.toHex() } 73 | let meta = BoundWitnessMeta(signatures) 74 | let bwWithMeta = EncodableWithCustomMetaInstance(from: bw, meta: meta) 75 | return (bwWithMeta, _payloads) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Sources/XyoClient/BoundWitness/Meta/BoundWitnessMeta.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol BoundWitnessMetaProtocol: Decodable { 4 | var client: String? { get set } 5 | var signatures: [String]? { get set } 6 | } 7 | 8 | public class BoundWitnessMeta: EncodableEmptyMeta, BoundWitnessMetaProtocol, Decodable { 9 | public var client: String? 10 | public var signatures: [String]? 11 | 12 | enum CodingKeys: String, CodingKey { 13 | case client = "$client" 14 | case signatures = "$signatures" 15 | } 16 | 17 | public init(_ signatures: [String] = []) { 18 | self.client = "ios" 19 | self.signatures = signatures 20 | } 21 | 22 | public required init(from decoder: Decoder) throws { 23 | let values = try decoder.container(keyedBy: CodingKeys.self) 24 | client = try values.decode(String.self, forKey: .client) 25 | signatures = try values.decode([String].self, forKey: .signatures) 26 | } 27 | 28 | override public func encode(to encoder: Encoder) throws { 29 | var container = encoder.container(keyedBy: CodingKeys.self) 30 | 31 | try container.encode(self.client, forKey: .client) 32 | try container.encode(self.signatures, forKey: .signatures) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/XyoClient/Hash/Hash.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public typealias Hash = Data 4 | -------------------------------------------------------------------------------- /Sources/XyoClient/Keychain/Keychain.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Security 3 | 4 | func saveToKeychain(key: String, value: String) -> Bool { 5 | let data = value.data(using: .utf8)! 6 | let query: [String: Any] = [ 7 | kSecClass as String: kSecClassGenericPassword, 8 | kSecAttrAccount as String: key, 9 | kSecValueData as String: data, 10 | ] 11 | SecItemDelete(query as CFDictionary) // Remove any existing item 12 | let status = SecItemAdd(query as CFDictionary, nil) 13 | return status == errSecSuccess 14 | } 15 | 16 | func getFromKeychain(key: String) -> String? { 17 | let query: [String: Any] = [ 18 | kSecClass as String: kSecClassGenericPassword, 19 | kSecAttrAccount as String: key, 20 | kSecReturnData as String: kCFBooleanTrue!, 21 | kSecMatchLimit as String: kSecMatchLimitOne, 22 | ] 23 | var item: CFTypeRef? 24 | let status = SecItemCopyMatching(query as CFDictionary, &item) 25 | if status == errSecSuccess, let data = item as? Data { 26 | return String(data: data, encoding: .utf8) 27 | } 28 | return nil 29 | } 30 | 31 | func removeFromKeychain(key: String) -> Bool { 32 | let query: [String: Any] = [ 33 | kSecClass as String: kSecClassGenericPassword, 34 | kSecAttrAccount as String: key, 35 | ] 36 | let status = SecItemDelete(query as CFDictionary) 37 | return status == errSecSuccess 38 | } 39 | -------------------------------------------------------------------------------- /Sources/XyoClient/Model.xcdatamodeld/Model.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Sources/XyoClient/Module/AbstractModule.swift: -------------------------------------------------------------------------------- 1 | open class AbstractModule: Module { 2 | 3 | private let _account: AccountInstance 4 | 5 | public var account: AccountInstance { 6 | _account 7 | } 8 | 9 | public var address: Address? { 10 | _account.address 11 | } 12 | 13 | public var previousHash: Hash? { 14 | _account.previousHash 15 | } 16 | 17 | public init(account: AccountInstance? = nil) { 18 | self._account = account ?? Account.random() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/XyoClient/Module/Module.swift: -------------------------------------------------------------------------------- 1 | public protocol Module { 2 | var address: Address? { get } 3 | var account: AccountInstance { get } 4 | var previousHash: Hash? { get } 5 | } 6 | -------------------------------------------------------------------------------- /Sources/XyoClient/Module/ModuleQueryResult.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public class ModuleQueryResult: Codable { 4 | public let bw: EncodableBoundWitnessWithMeta 5 | public let payloads: [EncodablePayloadInstance] 6 | public let errors: [EncodablePayloadInstance] 7 | init( 8 | bw: EncodableBoundWitnessWithMeta, 9 | payloads: [EncodablePayloadInstance] = [], 10 | errors: [EncodablePayloadInstance] = [] 11 | ) { 12 | self.bw = bw 13 | self.payloads = payloads 14 | self.errors = errors 15 | } 16 | public func encode(to encoder: Encoder) throws { 17 | // Create an unkeyed container for array encoding 18 | var container = encoder.unkeyedContainer() 19 | // Encode `bw` as the first element 20 | try container.encode(bw) 21 | // Encode `payloads` as the second element 22 | try container.encode(payloads) 23 | // Encode `errors` as the third element 24 | try container.encode(errors) 25 | } 26 | public required init(from decoder: Decoder) throws { 27 | var container = try decoder.unkeyedContainer() 28 | // Decode elements in the expected order from the array 29 | bw = try container.decode(BoundWitnessWithMeta.self) 30 | // TODO: Decodable Payloads 31 | // payloads = try container.decode([XyoPayload].self) 32 | // errors = try container.decode([XyoPayload].self) 33 | payloads = [] 34 | errors = [] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/XyoClient/Panel/Panel.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum XyoPanelError: Error { 4 | case failedToBuildBoundWitness 5 | } 6 | 7 | public class XyoPanel { 8 | 9 | public typealias XyoPanelReportCallback = (([String]) -> Void) 10 | 11 | private let _account: AccountInstance 12 | private let _archivists: [XyoArchivistApiClient] 13 | private let _witnesses: [WitnessModule] 14 | 15 | public var account: AccountInstance { 16 | _account 17 | } 18 | 19 | public init( 20 | account: AccountInstance, 21 | witnesses: [WitnessModule], 22 | archivists: [XyoArchivistApiClient] 23 | ) { 24 | self._archivists = archivists 25 | self._witnesses = witnesses 26 | self._account = account 27 | } 28 | 29 | public convenience init( 30 | account: AccountInstance? = nil, 31 | witnesses: [WitnessModule] = [], 32 | apiDomain: String = XyoArchivistApiClient.DefaultApiDomain, 33 | apiModule: String = XyoArchivistApiClient.DefaultArchivist 34 | ) { 35 | let panelAccount = account ?? AccountServices.getNamedAccount(name: "DefaultPanelAccount") 36 | let apiConfig = XyoArchivistApiConfig(apiModule, apiDomain) 37 | let archivist = XyoArchivistApiClient.get(apiConfig) 38 | self.init(account: panelAccount, witnesses: witnesses, archivists: [archivist]) 39 | } 40 | 41 | @available(iOS 15, *) 42 | private func witnessAll() async -> [EncodablePayloadInstance] { 43 | var payloads: [EncodablePayloadInstance] = [] 44 | // Collect payloads from both synchronous and asynchronous witnesses 45 | for witness in _witnesses { 46 | if let syncWitness = witness as? WitnessSync { 47 | // For synchronous witnesses, call the sync `observe` method directly 48 | payloads.append(contentsOf: syncWitness.observe()) 49 | } else if let asyncWitness = witness as? WitnessAsync { 50 | // For asynchronous witnesses, call the async `observe` method using `await` 51 | do { 52 | let asyncPayloads = try await asyncWitness.observe() 53 | payloads.append(contentsOf: asyncPayloads) 54 | } catch { 55 | print("Error observing async witness: \(error)") 56 | // Handle error as needed, possibly continue or throw 57 | } 58 | } 59 | } 60 | return payloads 61 | } 62 | 63 | @available(iOS 15, *) 64 | public func storeWitnessedResults(payloads: [EncodablePayloadInstance]) async { 65 | // Insert witnessed results into archivists 66 | await withTaskGroup(of: [EncodablePayload]?.self) { group in 67 | for instance in _archivists { 68 | group.addTask { 69 | do { 70 | return try await instance.insert(payloads: payloads) 71 | } catch { 72 | print("Error in insert for instance \(instance): \(error)") 73 | return nil 74 | } 75 | } 76 | } 77 | } 78 | return 79 | } 80 | 81 | @available(iOS 15, *) 82 | public func report() async -> [EncodablePayload] { 83 | // Report 84 | let results = await witnessAll() 85 | // Insert results into Archivists 86 | await storeWitnessedResults(payloads: results) 87 | // Return results 88 | return results 89 | } 90 | 91 | @available(iOS 15, *) 92 | public func reportQuery() async throws -> ModuleQueryResult { 93 | do { 94 | // Report 95 | let results = await witnessAll() 96 | 97 | // Sign the results 98 | let (bw, payloads) = try BoundWitnessBuilder() 99 | .payloads(results) 100 | .signers([self._account]) 101 | .build() 102 | 103 | // Insert signed results into Archivists 104 | let signedResults: [EncodablePayloadInstance] = [bw.typedPayload] + payloads 105 | await storeWitnessedResults(payloads: signedResults) 106 | 107 | // Return signed results 108 | return ModuleQueryResult(bw: bw, payloads: payloads, errors: []) 109 | } catch { 110 | print("Error in reportQuery: \(error)") 111 | // Return an empty ModuleQueryResult in case of an error 112 | do { 113 | let (bw, payloads) = try BoundWitnessBuilder().build() 114 | return ModuleQueryResult(bw: bw, payloads: payloads, errors: []) 115 | } catch { 116 | 117 | } 118 | } 119 | throw XyoPanelError.failedToBuildBoundWitness 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Sources/XyoClient/Payload/Core/Id.swift: -------------------------------------------------------------------------------- 1 | open class IdPayload: EncodablePayloadInstance { 2 | 3 | public static let schema: String = "network.xyo.id" 4 | 5 | var salt: String 6 | 7 | public override init(_ salt: String) { 8 | self.salt = salt 9 | super.init(IdPayload.schema) 10 | } 11 | 12 | public convenience init(_ salt: UInt) { 13 | let s = "\(salt)" 14 | self.init(s) 15 | } 16 | 17 | enum CodingKeys: String, CodingKey { 18 | case salt 19 | case schema 20 | } 21 | 22 | override open func encode(to encoder: Encoder) throws { 23 | var container = encoder.container(keyedBy: CodingKeys.self) 24 | try container.encode(self.schema, forKey: .schema) 25 | try container.encode(self.salt, forKey: .salt) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/XyoClient/Payload/Payload.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol PayloadFields: Encodable { 4 | var schema: String { get } 5 | } 6 | 7 | public protocol EncodablePayload: PayloadFields, Encodable {} 8 | 9 | public protocol Payload: PayloadFields, EncodablePayload, Decodable {} 10 | 11 | open class EncodablePayloadInstance: EncodablePayload { 12 | public init(_ schema: String) { 13 | self.schema = schema.lowercased() 14 | } 15 | 16 | enum CodingKeys: String, CodingKey { 17 | case schema 18 | } 19 | 20 | public var schema: String 21 | 22 | public func toJson() throws -> String { 23 | return try PayloadBuilder.toJson(from: self) 24 | } 25 | } 26 | 27 | open class PayloadInstance: EncodablePayloadInstance, Payload { 28 | 29 | override public init(_ schema: String) { 30 | super.init(schema) 31 | } 32 | 33 | public required init(from decoder: Decoder) throws { 34 | let values = try decoder.container(keyedBy: CodingKeys.self) 35 | let schema = try values.decode(String.self, forKey: .schema) 36 | super.init(schema) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/XyoClient/Payload/PayloadBuilder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum PayloadBuilderError: Error { 4 | case encodingError 5 | } 6 | 7 | /// Merges multiple `Encodable` objects into a single JSON object. 8 | /// 9 | /// This method accepts a variable number of `Encodable` objects, encodes each into JSON, converts them into dictionaries, and merges the dictionaries into one. In case of key conflicts, the value from the latter object will overwrite the earlier value. The final merged dictionary is then serialized back into JSON data. 10 | /// 11 | /// - Parameters: 12 | /// - encodables: A variadic list of objects conforming to the `Encodable` protocol. 13 | /// 14 | /// - Returns: A `Data` object representing the merged JSON of all provided `Encodable` objects. 15 | /// 16 | /// - Throws: 17 | /// - An error if any of the `Encodable` objects fail to encode. 18 | /// - An error if the JSON data cannot be converted to a dictionary or serialized. 19 | func mergeToJsonObject(_ encodables: (any Encodable)...) throws -> Data { 20 | let encoder = JSONEncoder() 21 | encoder.outputFormatting = .sortedKeys 22 | 23 | // Initialize an empty dictionary to store merged data 24 | var mergedDict: [String: Any] = [:] 25 | 26 | for encodable in encodables { 27 | // Encode the current object into JSON data 28 | let data = try encoder.encode(encodable) 29 | 30 | // Decode the JSON into a dictionary 31 | if let dict = try JSONSerialization.jsonObject(with: data) as? [String: Any] { 32 | // Merge the current dictionary with the existing merged dictionary 33 | mergedDict.merge(dict) { (_, new) in new } 34 | } 35 | } 36 | 37 | // Serialize the merged dictionary back into JSON 38 | return try JSONSerialization.data(withJSONObject: mergedDict, options: .sortedKeys) 39 | } 40 | 41 | public protocol EncodableWithMeta: Encodable { 42 | var payload: EncodablePayload { get } 43 | var meta: Encodable? { get } 44 | func toJson() throws -> String 45 | } 46 | 47 | public struct AnyEncodableWithMeta: EncodableWithMeta { 48 | private let _item: T 49 | 50 | public init(_ from: T) { 51 | self._item = from 52 | } 53 | 54 | public var payload: EncodablePayload { 55 | return self._item.payload 56 | } 57 | 58 | public var meta: Encodable? { 59 | return self._item.meta 60 | } 61 | 62 | public func toJson() throws -> String { 63 | return try self._item.toJson() 64 | } 65 | 66 | public func encode(to: Encoder) throws { 67 | return try self._item.encode(to: to) 68 | } 69 | } 70 | 71 | public class EncodableEmptyMeta: Encodable {} 72 | 73 | public class EmptyMeta: Codable {} 74 | 75 | public class EncodableWithMetaInstance: EncodableWithCustomMetaInstance< 76 | T, EncodableEmptyMeta 77 | > 78 | { 79 | public init(from: T) { 80 | super.init(from: from, meta: nil) 81 | } 82 | } 83 | 84 | public class EncodableWithCustomMetaInstance: EncodableWithMeta { 85 | var _meta: M? = nil 86 | var _payload: T 87 | 88 | public var payload: EncodablePayload { 89 | return self._payload 90 | } 91 | 92 | public var typedPayload: T { 93 | return self._payload 94 | } 95 | 96 | public var meta: Encodable? { 97 | return self._meta 98 | } 99 | 100 | public var typedMeta: M? { 101 | return self._meta 102 | } 103 | 104 | public var schema: String { 105 | return _payload.schema 106 | } 107 | 108 | public init(from: T, meta: M?) { 109 | _payload = from 110 | _meta = meta 111 | } 112 | 113 | public func encode(to encoder: Encoder) throws { 114 | if let meta = _meta { 115 | try meta.encode(to: encoder) 116 | } 117 | try self._payload.encode(to: encoder) 118 | } 119 | 120 | public func toJson() throws -> String { 121 | return try PayloadBuilder.toJson(from: self) 122 | } 123 | } 124 | 125 | public class WithCustomMetaInstance: 126 | EncodableWithCustomMetaInstance, Decodable 127 | { 128 | 129 | override public init(from: T, meta: M?) { 130 | super.init(from: from, meta: meta) 131 | } 132 | 133 | public required init(from decoder: Decoder) throws { 134 | super.init(from: try T(from: decoder), meta: try M(from: decoder)) 135 | } 136 | } 137 | 138 | public class WithMetaInstance: WithCustomMetaInstance { 139 | public init(from: T) { 140 | super.init(from: from, meta: nil) 141 | } 142 | 143 | public required init(from decoder: Decoder) throws { 144 | super.init(from: try T(from: decoder), meta: nil) 145 | } 146 | } 147 | 148 | public class PayloadBuilder { 149 | private static func isHashableField(_ key: String) -> Bool { 150 | // Remove keys starting with "_" 151 | return !key.hasPrefix("_") 152 | } 153 | 154 | private static func isDataHashableField(_ key: String) -> Bool { 155 | // Remove keys starting with "_" 156 | return isHashableField(key) 157 | // Remove keys starting with "$" 158 | && !key.hasPrefix("$") 159 | } 160 | 161 | private static func dataHashableFields(_ jsonObject: Any) -> Any { 162 | if let dictionary = jsonObject as? [String: Any] { 163 | // Process dictionaries: filter keys, sort, and recurse 164 | let filteredDictionary = 165 | dictionary 166 | .filter { isDataHashableField($0.key) } // Filter meta fields 167 | .sorted { $0.key < $1.key } // Sort keys lexicographically 168 | .reduce(into: [String: Any]()) { result, pair in 169 | result[pair.key] = dataHashableFields(pair.value) // Recurse on values 170 | } 171 | return filteredDictionary 172 | } else if let array = jsonObject as? [Any] { 173 | // Process arrays: recursively process each element 174 | return array.map { dataHashableFields($0) } 175 | } else { 176 | // Return primitives (String, Number, etc.) 177 | return jsonObject 178 | } 179 | } 180 | 181 | private static func hashableFields(_ jsonObject: Any) -> Any { 182 | if let dictionary = jsonObject as? [String: Any] { 183 | // Process dictionaries: filter keys, sort, and recurse 184 | let filteredDictionary = 185 | dictionary 186 | .sorted { $0.key < $1.key } // Sort keys lexicographically 187 | .reduce(into: [String: Any]()) { result, pair in 188 | result[pair.key] = hashableFields(pair.value) // Recurse on values 189 | } 190 | return filteredDictionary 191 | } else if let array = jsonObject as? [Any] { 192 | // Process arrays: recursively process each element 193 | return array.map { dataHashableFields($0) } 194 | } else { 195 | // Return primitives (String, Number, etc.) 196 | return jsonObject 197 | } 198 | } 199 | 200 | static public func dataHash(from: T) throws -> Hash { 201 | let jsonString = try PayloadBuilder.toJson(from: from) 202 | return try jsonString.sha256() 203 | } 204 | 205 | static public func hash(fromWithMeta: T) throws -> Hash { 206 | let jsonString = try fromWithMeta.toJson() 207 | return try jsonString.sha256() 208 | } 209 | 210 | static public func hash(from: T, meta: M?) 211 | throws -> Hash 212 | { 213 | let withMeta = EncodableWithCustomMetaInstance(from: from, meta: meta) 214 | let jsonString = try withMeta.toJson() 215 | return try jsonString.sha256() 216 | } 217 | 218 | static public func hash(from: T) throws -> Hash { 219 | let withMeta = EncodableWithMetaInstance(from: from) 220 | let jsonString = try withMeta.toJson() 221 | return try jsonString.sha256() 222 | } 223 | 224 | static public func toJson(from: T) throws -> String { 225 | let encoder = JSONEncoder() 226 | encoder.outputFormatting = .sortedKeys 227 | let data = try encoder.encode(from) 228 | guard let result = String(data: data, encoding: .utf8) else { 229 | throw PayloadBuilderError.encodingError 230 | } 231 | return result 232 | } 233 | 234 | static public func toJsonWithMeta(from: T, meta: (any Encodable)?) 235 | throws -> String 236 | { 237 | guard let m = meta else { 238 | return try PayloadBuilder.toJson(from: from) 239 | } 240 | let data = try mergeToJsonObject(from, m) 241 | guard let result = String(data: data, encoding: .utf8) else { 242 | throw PayloadBuilderError.encodingError 243 | } 244 | return result 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /Sources/XyoClient/Payload/PayloadValidator.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | open class PayloadValidator { 4 | public var payload: Payload 5 | private var schemaValidator: SchemaValidator 6 | 7 | public init(_ payload: Payload) { 8 | self.payload = payload 9 | self.schemaValidator = SchemaValidator(payload.schema) 10 | } 11 | 12 | public func allDynamic(closure: (_ errors: [String]) -> Void) { 13 | var errors: [String] = [] 14 | self.schemaValidator.allDynamic { schemaErrors in 15 | errors.append(contentsOf: schemaErrors) 16 | closure(errors) 17 | } 18 | } 19 | 20 | public func all() -> [String] { 21 | var errors: [String] = [] 22 | errors.append(contentsOf: self.schemaValidator.all()) 23 | return errors 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/XyoClient/Payload/PayloadWrapper.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | open class PayloadWrapper { 4 | 5 | public var payload: Payload 6 | private var validator: PayloadValidator 7 | 8 | public init(_ payload: Payload) { 9 | self.payload = payload 10 | self.validator = PayloadValidator(payload) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/XyoClient/PersistentContainerProvider/DefaultPersistentContainerProvider.swift: -------------------------------------------------------------------------------- 1 | import CoreData 2 | 3 | public class DefaultPersistentContainerProvider: PersistentContainerProvider { 4 | public let persistentContainer: NSPersistentContainer 5 | 6 | public init() { 7 | // Locate the bundled Core Data model in the package's resources 8 | guard let modelURL = Bundle.module.url(forResource: "Model", withExtension: "momd"), 9 | let model = NSManagedObjectModel(contentsOf: modelURL) 10 | else { 11 | fatalError("Failed to load Core Data model from the package.") 12 | } 13 | 14 | persistentContainer = NSPersistentContainer(name: "DefaultModel", managedObjectModel: model) 15 | 16 | persistentContainer.loadPersistentStores { _, error in 17 | if let error = error { 18 | fatalError("Failed to load Core Data persistent stores: \(error)") 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/XyoClient/PersistentContainerProvider/PersistentContainerProvider.swift: -------------------------------------------------------------------------------- 1 | import CoreData 2 | 3 | public protocol PersistentContainerProvider { 4 | var persistentContainer: NSPersistentContainer { get } 5 | } 6 | -------------------------------------------------------------------------------- /Sources/XyoClient/PreviousHashStore/CoreData/CoreDataPreviousHashStore.swift: -------------------------------------------------------------------------------- 1 | import CoreData 2 | 3 | public class CoreDataPreviousHashStore: PreviousHashStore { 4 | private let context: NSManagedObjectContext 5 | 6 | public init(provider: PersistentContainerProvider = DefaultPersistentContainerProvider()) { 7 | self.context = provider.persistentContainer.viewContext 8 | } 9 | 10 | public func getItem(address: Address) -> Hash? { 11 | let fetchRequest: NSFetchRequest = HashStore.fetchRequest() 12 | fetchRequest.predicate = NSPredicate(format: "address == %@", address.toHex()) 13 | fetchRequest.fetchLimit = 1 14 | 15 | do { 16 | let results = try context.fetch(fetchRequest) 17 | guard let previousHash = results.first?.previousHash else { return nil } 18 | return Hash(previousHash) 19 | } catch { 20 | print("Failed to fetch item for address \(address): \(error)") 21 | return nil 22 | } 23 | } 24 | 25 | public func removeItem(address: Address) { 26 | let fetchRequest: NSFetchRequest = HashStore.fetchRequest() 27 | fetchRequest.predicate = NSPredicate(format: "address == %@", address.toHex()) 28 | 29 | do { 30 | let results = try context.fetch(fetchRequest) 31 | for object in results { 32 | context.delete(object) 33 | } 34 | try context.save() 35 | } catch { 36 | print("Failed to remove item for address \(address): \(error)") 37 | } 38 | } 39 | 40 | public func setItem(address: Address, previousHash: Hash) { 41 | let fetchRequest: NSFetchRequest = HashStore.fetchRequest() 42 | fetchRequest.predicate = NSPredicate(format: "address == %@", address.toHex()) 43 | fetchRequest.fetchLimit = 1 44 | 45 | do { 46 | let results = try context.fetch(fetchRequest) 47 | 48 | if let existingEntry = results.first { 49 | existingEntry.previousHash = previousHash.toHex() 50 | } else { 51 | let newEntry = HashStore(context: context) 52 | newEntry.address = address.toHex() 53 | newEntry.previousHash = previousHash.toHex() 54 | } 55 | 56 | try context.save() 57 | } catch { 58 | print("Failed to set item for address \(address): \(error)") 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/XyoClient/PreviousHashStore/CoreData/TestPersistentContainerProvider.swift: -------------------------------------------------------------------------------- 1 | import CoreData 2 | 3 | public class TestPersistentContainerProvider: PersistentContainerProvider { 4 | public let persistentContainer: NSPersistentContainer 5 | 6 | init() { 7 | // Load the Core Data model 8 | guard let modelURL = Bundle.module.url(forResource: "Model", withExtension: "momd"), 9 | let model = NSManagedObjectModel(contentsOf: modelURL) 10 | else { 11 | fatalError("Failed to load Core Data model.") 12 | } 13 | 14 | persistentContainer = NSPersistentContainer(name: "TestModel", managedObjectModel: model) 15 | 16 | // Use an in-memory store for testing 17 | let description = NSPersistentStoreDescription() 18 | description.type = NSInMemoryStoreType 19 | persistentContainer.persistentStoreDescriptions = [description] 20 | 21 | persistentContainer.loadPersistentStores { description, error in 22 | if let error = error { 23 | fatalError("Error setting up in-memory store: \(error)") 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/XyoClient/PreviousHashStore/Memory/MemoryPreviousHashStore.swift: -------------------------------------------------------------------------------- 1 | public class MemoryPreviousHashStore: PreviousHashStore { 2 | private var store: [Address: Hash] = [:] 3 | 4 | public func getItem(address: Address) -> Hash? { 5 | return store[address] 6 | } 7 | 8 | public func removeItem(address: Address) { 9 | store.removeValue(forKey: address) 10 | } 11 | 12 | public func setItem(address: Address, previousHash: Hash) { 13 | store[address] = previousHash 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/XyoClient/PreviousHashStore/PreviousHashStore.swift: -------------------------------------------------------------------------------- 1 | public protocol PreviousHashStore { 2 | func getItem(address: Address) -> Hash? 3 | func removeItem(address: Address) 4 | func setItem(address: Address, previousHash: Hash) 5 | } 6 | -------------------------------------------------------------------------------- /Sources/XyoClient/Schema/SchemaValidator.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | open class SchemaValidator { 4 | public var schema: String 5 | public var parts: [Substring] 6 | 7 | public init(_ schema: String) { 8 | self.schema = schema 9 | self.parts = schema.split(separator: ".") 10 | } 11 | 12 | public var levels: Int { 13 | return self.parts.count 14 | } 15 | 16 | var isLowercase: Bool { 17 | return self.schema == self.schema.lowercased() 18 | } 19 | 20 | private func domainLevel(_ level: Int) -> String { 21 | return self.parts[0..<(level + 1)].reversed().joined(separator: ".") 22 | } 23 | 24 | var rootDomain: String { 25 | return self.domainLevel(1) 26 | } 27 | 28 | public func rootDomainExists(_ closure: (_ exists: Bool) -> Void) { 29 | // domainExists(this.rootDomain, closure) 30 | closure(true) 31 | } 32 | 33 | public func allDynamic(closure: (_ errors: [String]) -> Void) { 34 | var errors: [String] = [] 35 | if self.schema.isEmpty { 36 | errors.append("schema missing") 37 | closure(errors) 38 | } else { 39 | self.rootDomainExists { exists in 40 | if !exists { 41 | errors.append("schema root domain must exist [\(self.rootDomain)]") 42 | } 43 | closure(errors) 44 | } 45 | } 46 | } 47 | 48 | public func all() -> [String] { 49 | var errors: [String] = [] 50 | if self.schema.isEmpty { 51 | errors.append("schema missing") 52 | } else if self.levels < 3 { 53 | errors.append("schema levels < 3 [\(self.levels), \(self.schema)]") 54 | } else if !self.isLowercase { 55 | errors.append("schema not lowercase [\(self.schema)]") 56 | } 57 | return errors 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/XyoClient/Wallet/BIP-39/Bip39.swift: -------------------------------------------------------------------------------- 1 | import BigInt 2 | import CryptoKit 3 | import CryptoSwift 4 | import Foundation 5 | 6 | public enum Bip39Error: Error { 7 | case invalidSeedLength 8 | case invalidSeedChecksum 9 | case invalidMnemonicLength 10 | case invalidMnemonicChecksum 11 | case invalidMnemonicWordCount 12 | case invalidMnemonicWordIndex 13 | case invalidMnemonicWord 14 | case failedToGenerateSeed 15 | case invalidByteCode 16 | case failedToEncode 17 | case invalidEntropyLength 18 | case invalidPrivateKeyGenerated 19 | case failedToComputeHmac 20 | case invalidCurveOrder 21 | case invalidKey 22 | } 23 | 24 | let BITCOIN_SEED = "Bitcoin seed".data(using: .utf8)! 25 | let PRIVATE_KEY_SIZE = 32 26 | let CHAINCODE_SIZE = 32 27 | 28 | public class Bip39 { 29 | static let wordList: [String] = Bip39Words 30 | 31 | static func mnemonicToSeed(phrase: String) throws -> Data { 32 | let entropy = try mnemonicToEntropy(phrase: phrase) 33 | return try entropyToSeed(entropy: entropy) 34 | } 35 | 36 | static func generateEntropy(bits: Int) -> Data { 37 | precondition(bits % 32 == 0, "Entropy must be a multiple of 32") 38 | let byteCount = bits / 8 39 | var entropy = Data(count: byteCount) 40 | _ = entropy.withUnsafeMutableBytes { bytes in 41 | SecRandomCopyBytes(kSecRandomDefault, byteCount, bytes.baseAddress!) 42 | } 43 | return entropy 44 | } 45 | 46 | static func mnemonicToEntropy(phrase: String) throws -> Data { 47 | let words = phrase.lowercased().split(separator: " ").map(String.init) 48 | 49 | // Step 1: Validate word count 50 | guard [12, 15, 18, 21, 24].contains(words.count) else { 51 | throw Bip39Error.invalidMnemonicWordCount 52 | } 53 | 54 | // Step 2: Map words to indices 55 | let indices = words.compactMap { wordList.firstIndex(of: $0) } 56 | guard indices.count == words.count else { 57 | throw Bip39Error.invalidMnemonicWordCount 58 | } 59 | 60 | // Step 3: Reconstruct combined bits from indices 61 | let combinedBits = 62 | indices 63 | .map { String($0, radix: 2).leftPad(toLength: 11, with: "0") } 64 | .joined() 65 | 66 | // Step 4: Separate entropy and checksum 67 | let checksumBits = words.count / 3 68 | let entropyBits = combinedBits.count - checksumBits 69 | 70 | let entropyBinary = String(combinedBits.prefix(entropyBits)) 71 | let checksumBinary = String(combinedBits.suffix(checksumBits)) 72 | 73 | // Step 5: Convert binary entropy to Data 74 | let entropy = try entropyBinary.binaryToData() 75 | 76 | // Step 6: Verify checksum 77 | let calculatedChecksum = calculateChecksum(entropy: entropy, bits: checksumBits) 78 | guard checksumBinary == calculatedChecksum else { 79 | throw Bip39Error.invalidMnemonicChecksum 80 | } 81 | 82 | return entropy 83 | } 84 | 85 | static func entropyToMnemonic(entropy: Data) throws -> String { 86 | // Step 1: Validate entropy length 87 | guard [16, 20, 24, 28, 32].contains(entropy.count) else { 88 | throw Bip39Error.invalidEntropyLength 89 | } 90 | 91 | // Step 2: Calculate checksum 92 | let checksumBits = entropy.count * 8 / 32 93 | let checksum = calculateChecksum(entropy: entropy, bits: checksumBits) 94 | 95 | // Step 3: Combine entropy and checksum into a binary string 96 | let entropyBits = entropy.map { String($0, radix: 2).leftPad(toLength: 8, with: "0") } 97 | .joined() 98 | let combinedBits = entropyBits + checksum 99 | 100 | // Step 4: Split combined bits into 11-bit chunks 101 | let chunks = stride(from: 0, to: combinedBits.count, by: 11).map { startIndex in 102 | let start = combinedBits.index(combinedBits.startIndex, offsetBy: startIndex) 103 | let end = combinedBits.index(start, offsetBy: 11) 104 | return String(combinedBits[start.. Data { 124 | // Step 1: Convert entropy to mnemonic 125 | let mnemonic = try entropyToMnemonic(entropy: entropy) 126 | // Step 2: Apply PBKDF2 to derive the seed 127 | let salt = "mnemonic" + passphrase 128 | guard let mnemonicData = mnemonic.data(using: .utf8), 129 | let saltData = salt.data(using: .utf8) 130 | else { 131 | throw Bip39Error.failedToEncode 132 | } 133 | 134 | do { 135 | let seed = try PKCS5.PBKDF2( 136 | password: Array(mnemonicData), 137 | salt: Array(saltData), 138 | iterations: 2048, 139 | keyLength: 64, 140 | variant: .sha2(.sha512) 141 | ).calculate() 142 | 143 | return Data(seed) 144 | } catch { 145 | throw Bip39Error.failedToGenerateSeed 146 | } 147 | } 148 | 149 | static func rootPrivateKeyFromSeed(seed: Data) throws -> Key { 150 | 151 | let hmac = Hmac.hmacSha512(key: BITCOIN_SEED, data: seed) 152 | let privateKey = hmac.prefix(PRIVATE_KEY_SIZE) 153 | let chainCode = hmac.suffix(from: PRIVATE_KEY_SIZE) 154 | 155 | let ib = privateKey.toBigInt() 156 | if ib == 0 || ib >= Secp256k1CurveConstants.n { 157 | throw NSError(domain: "Invalid key", code: 0, userInfo: nil) 158 | } 159 | 160 | return Key(privateKey: privateKey, chainCode: chainCode) 161 | } 162 | 163 | private static func calculateChecksum(entropy: Data, bits: Int) -> String { 164 | let hash = Data(SHA256.hash(data: entropy)) 165 | let hashBits = hash.toBinaryString() 166 | return String(hashBits.prefix(bits)) 167 | } 168 | 169 | private static func isValidPrivateKey(privateKey: Data) throws -> Bool { 170 | // Ensure private key is exactly 32 bytes 171 | guard privateKey.count == 32 else { 172 | print("Invalid private key length: must be 32 bytes.") 173 | return false 174 | } 175 | 176 | // Convert private key to BigUInt 177 | let privateKeyValue = BigUInt(privateKey) 178 | 179 | // Check if private key is within the valid range (0 < key < curve order) 180 | if privateKeyValue > 0 && privateKeyValue < Secp256k1CurveConstants.n { 181 | return true 182 | } else { 183 | print("Private key is out of valid range.") 184 | return false 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /Sources/XyoClient/Wallet/BIP-39/Bip39Words.swift: -------------------------------------------------------------------------------- 1 | public let Bip39Words: [String] = [ 2 | "abandon", "ability", "able", "about", "above", "absent", "absorb", "abstract", "absurd", 3 | "abuse", 4 | "access", "accident", "account", "accuse", "achieve", "acid", "acoustic", "acquire", "across", 5 | "act", 6 | "action", "actor", "actress", "actual", "adapt", "add", "addict", "address", "adjust", "admit", 7 | "adult", "advance", "advice", "aerobic", "affair", "afford", "afraid", "again", "age", "agent", 8 | "agree", "ahead", "aim", "air", "airport", "aisle", "alarm", "album", "alcohol", "alert", 9 | "alien", "all", "alley", "allow", "almost", "alone", "alpha", "already", "also", "alter", 10 | "always", "amateur", "amazing", "among", "amount", "amused", "analyst", "anchor", "ancient", 11 | "anger", 12 | "angle", "angry", "animal", "ankle", "announce", "annual", "another", "answer", "antenna", 13 | "antique", 14 | "anxiety", "any", "apart", "apology", "appear", "apple", "approve", "april", "arch", "arctic", 15 | "area", "arena", "argue", "arm", "armed", "armor", "army", "around", "arrange", "arrest", 16 | "arrive", "arrow", "art", "artefact", "artist", "artwork", "ask", "aspect", "assault", "asset", 17 | "assist", "assume", "asthma", "athlete", "atom", "attack", "attend", "attitude", "attract", 18 | "auction", 19 | "audit", "august", "aunt", "author", "auto", "autumn", "average", "avocado", "avoid", "awake", 20 | "aware", "away", "awesome", "awful", "awkward", "axis", "baby", "bachelor", "bacon", "badge", 21 | "bag", "balance", "balcony", "ball", "bamboo", "banana", "banner", "bar", "barely", "bargain", 22 | "barrel", "base", "basic", "basket", "battle", "beach", "bean", "beauty", "because", "become", 23 | "beef", "before", "begin", "behave", "behind", "believe", "below", "belt", "bench", "benefit", 24 | "best", "betray", "better", "between", "beyond", "bicycle", "bid", "bike", "bind", "biology", 25 | "bird", "birth", "bitter", "black", "blade", "blame", "blanket", "blast", "bleak", "bless", 26 | "blind", "blood", "blossom", "blouse", "blue", "blur", "blush", "board", "boat", "body", 27 | "boil", "bomb", "bone", "bonus", "book", "boost", "border", "boring", "borrow", "boss", 28 | "bottom", "bounce", "box", "boy", "bracket", "brain", "brand", "brass", "brave", "bread", 29 | "breeze", "brick", "bridge", "brief", "bright", "bring", "brisk", "broccoli", "broken", 30 | "bronze", 31 | "broom", "brother", "brown", "brush", "bubble", "buddy", "budget", "buffalo", "build", "bulb", 32 | "bulk", "bullet", "bundle", "bunker", "burden", "burger", "burst", "bus", "business", "busy", 33 | "butter", "buyer", "buzz", "cabbage", "cabin", "cable", "cactus", "cage", "cake", "call", 34 | "calm", "camera", "camp", "can", "canal", "cancel", "candy", "cannon", "canoe", "canvas", 35 | "canyon", "capable", "capital", "captain", "car", "carbon", "card", "cargo", "carpet", "carry", 36 | "cart", "case", "cash", "casino", "castle", "casual", "cat", "catalog", "catch", "category", 37 | "cattle", "caught", "cause", "caution", "cave", "ceiling", "celery", "cement", "census", 38 | "century", 39 | "cereal", "certain", "chair", "chalk", "champion", "change", "chaos", "chapter", "charge", 40 | "chase", 41 | "chat", "cheap", "check", "cheese", "chef", "cherry", "chest", "chicken", "chief", "child", 42 | "chimney", "choice", "choose", "chronic", "chuckle", "chunk", "churn", "cigar", "cinnamon", 43 | "circle", 44 | "citizen", "city", "civil", "claim", "clap", "clarify", "claw", "clay", "clean", "clerk", 45 | "clever", "click", "client", "cliff", "climb", "clinic", "clip", "clock", "clog", "close", 46 | "cloth", "cloud", "clown", "club", "clump", "cluster", "clutch", "coach", "coast", "coconut", 47 | "code", "coffee", "coil", "coin", "collect", "color", "column", "combine", "come", "comfort", 48 | "comic", "common", "company", "concert", "conduct", "confirm", "congress", "connect", 49 | "consider", "control", 50 | "convince", "cook", "cool", "copper", "copy", "coral", "core", "corn", "correct", "cost", 51 | "cotton", "couch", "country", "couple", "course", "cousin", "cover", "coyote", "crack", 52 | "cradle", 53 | "craft", "cram", "crane", "crash", "crater", "crawl", "crazy", "cream", "credit", "creek", 54 | "crew", "cricket", "crime", "crisp", "critic", "crop", "cross", "crouch", "crowd", "crucial", 55 | "cruel", "cruise", "crumble", "crunch", "crush", "cry", "crystal", "cube", "culture", "cup", 56 | "cupboard", "curious", "current", "curtain", "curve", "cushion", "custom", "cute", "cycle", 57 | "dad", 58 | "damage", "damp", "dance", "danger", "daring", "dash", "daughter", "dawn", "day", "deal", 59 | "debate", "debris", "decade", "december", "decide", "decline", "decorate", "decrease", "deer", 60 | "defense", 61 | "define", "defy", "degree", "delay", "deliver", "demand", "demise", "denial", "dentist", "deny", 62 | "depart", "depend", "deposit", "depth", "deputy", "derive", "describe", "desert", "design", 63 | "desk", 64 | "despair", "destroy", "detail", "detect", "develop", "device", "devote", "diagram", "dial", 65 | "diamond", 66 | "diary", "dice", "diesel", "diet", "differ", "digital", "dignity", "dilemma", "dinner", 67 | "dinosaur", 68 | "direct", "dirt", "disagree", "discover", "disease", "dish", "dismiss", "disorder", "display", 69 | "distance", 70 | "divert", "divide", "divorce", "dizzy", "doctor", "document", "dog", "doll", "dolphin", 71 | "domain", 72 | "donate", "donkey", "donor", "door", "dose", "double", "dove", "draft", "dragon", "drama", 73 | "drastic", "draw", "dream", "dress", "drift", "drill", "drink", "drip", "drive", "drop", 74 | "drum", "dry", "duck", "dumb", "dune", "during", "dust", "dutch", "duty", "dwarf", 75 | "dynamic", "eager", "eagle", "early", "earn", "earth", "easily", "east", "easy", "echo", 76 | "ecology", "economy", "edge", "edit", "educate", "effort", "egg", "eight", "either", "elbow", 77 | "elder", "electric", "elegant", "element", "elephant", "elevator", "elite", "else", "embark", 78 | "embody", 79 | "embrace", "emerge", "emotion", "employ", "empower", "empty", "enable", "enact", "end", 80 | "endless", 81 | "endorse", "enemy", "energy", "enforce", "engage", "engine", "enhance", "enjoy", "enlist", 82 | "enough", 83 | "enrich", "enroll", "ensure", "enter", "entire", "entry", "envelope", "episode", "equal", 84 | "equip", 85 | "era", "erase", "erode", "erosion", "error", "erupt", "escape", "essay", "essence", "estate", 86 | "eternal", "ethics", "evidence", "evil", "evoke", "evolve", "exact", "example", "excess", 87 | "exchange", 88 | "excite", "exclude", "excuse", "execute", "exercise", "exhaust", "exhibit", "exile", "exist", 89 | "exit", 90 | "exotic", "expand", "expect", "expire", "explain", "expose", "express", "extend", "extra", 91 | "eye", 92 | "eyebrow", "fabric", "face", "faculty", "fade", "faint", "faith", "fall", "false", "fame", 93 | "family", "famous", "fan", "fancy", "fantasy", "farm", "fashion", "fat", "fatal", "father", 94 | "fatigue", "fault", "favorite", "feature", "february", "federal", "fee", "feed", "feel", 95 | "female", 96 | "fence", "festival", "fetch", "fever", "few", "fiber", "fiction", "field", "figure", "file", 97 | "film", "filter", "final", "find", "fine", "finger", "finish", "fire", "firm", "first", 98 | "fiscal", "fish", "fit", "fitness", "fix", "flag", "flame", "flash", "flat", "flavor", 99 | "flee", "flight", "flip", "float", "flock", "floor", "flower", "fluid", "flush", "fly", 100 | "foam", "focus", "fog", "foil", "fold", "follow", "food", "foot", "force", "forest", 101 | "forget", "fork", "fortune", "forum", "forward", "fossil", "foster", "found", "fox", "fragile", 102 | "frame", "frequent", "fresh", "friend", "fringe", "frog", "front", "frost", "frown", "frozen", 103 | "fruit", "fuel", "fun", "funny", "furnace", "fury", "future", "gadget", "gain", "galaxy", 104 | "gallery", "game", "gap", "garage", "garbage", "garden", "garlic", "garment", "gas", "gasp", 105 | "gate", "gather", "gauge", "gaze", "general", "genius", "genre", "gentle", "genuine", "gesture", 106 | "ghost", "giant", "gift", "giggle", "ginger", "giraffe", "girl", "give", "glad", "glance", 107 | "glare", "glass", "glide", "glimpse", "globe", "gloom", "glory", "glove", "glow", "glue", 108 | "goat", "goddess", "gold", "good", "goose", "gorilla", "gospel", "gossip", "govern", "gown", 109 | "grab", "grace", "grain", "grant", "grape", "grass", "gravity", "great", "green", "grid", 110 | "grief", "grit", "grocery", "group", "grow", "grunt", "guard", "guess", "guide", "guilt", 111 | "guitar", "gun", "gym", "habit", "hair", "half", "hammer", "hamster", "hand", "happy", 112 | "harbor", "hard", "harsh", "harvest", "hat", "have", "hawk", "hazard", "head", "health", 113 | "heart", "heavy", "hedgehog", "height", "hello", "helmet", "help", "hen", "hero", "hidden", 114 | "high", "hill", "hint", "hip", "hire", "history", "hobby", "hockey", "hold", "hole", 115 | "holiday", "hollow", "home", "honey", "hood", "hope", "horn", "horror", "horse", "hospital", 116 | "host", "hotel", "hour", "hover", "hub", "huge", "human", "humble", "humor", "hundred", 117 | "hungry", "hunt", "hurdle", "hurry", "hurt", "husband", "hybrid", "ice", "icon", "idea", 118 | "identify", "idle", "ignore", "ill", "illegal", "illness", "image", "imitate", "immense", 119 | "immune", 120 | "impact", "impose", "improve", "impulse", "inch", "include", "income", "increase", "index", 121 | "indicate", 122 | "indoor", "industry", "infant", "inflict", "inform", "inhale", "inherit", "initial", "inject", 123 | "injury", 124 | "inmate", "inner", "innocent", "input", "inquiry", "insane", "insect", "inside", "inspire", 125 | "install", 126 | "intact", "interest", "into", "invest", "invite", "involve", "iron", "island", "isolate", 127 | "issue", 128 | "item", "ivory", "jacket", "jaguar", "jar", "jazz", "jealous", "jeans", "jelly", "jewel", 129 | "job", "join", "joke", "journey", "joy", "judge", "juice", "jump", "jungle", "junior", 130 | "junk", "just", "kangaroo", "keen", "keep", "ketchup", "key", "kick", "kid", "kidney", 131 | "kind", "kingdom", "kiss", "kit", "kitchen", "kite", "kitten", "kiwi", "knee", "knife", 132 | "knock", "know", "lab", "label", "labor", "ladder", "lady", "lake", "lamp", "language", 133 | "laptop", "large", "later", "latin", "laugh", "laundry", "lava", "law", "lawn", "lawsuit", 134 | "layer", "lazy", "leader", "leaf", "learn", "leave", "lecture", "left", "leg", "legal", 135 | "legend", "leisure", "lemon", "lend", "length", "lens", "leopard", "lesson", "letter", "level", 136 | "liar", "liberty", "library", "license", "life", "lift", "light", "like", "limb", "limit", 137 | "link", "lion", "liquid", "list", "little", "live", "lizard", "load", "loan", "lobster", 138 | "local", "lock", "logic", "lonely", "long", "loop", "lottery", "loud", "lounge", "love", 139 | "loyal", "lucky", "luggage", "lumber", "lunar", "lunch", "luxury", "lyrics", "machine", "mad", 140 | "magic", "magnet", "maid", "mail", "main", "major", "make", "mammal", "man", "manage", 141 | "mandate", "mango", "mansion", "manual", "maple", "marble", "march", "margin", "marine", 142 | "market", 143 | "marriage", "mask", "mass", "master", "match", "material", "math", "matrix", "matter", 144 | "maximum", 145 | "maze", "meadow", "mean", "measure", "meat", "mechanic", "medal", "media", "melody", "melt", 146 | "member", "memory", "mention", "menu", "mercy", "merge", "merit", "merry", "mesh", "message", 147 | "metal", "method", "middle", "midnight", "milk", "million", "mimic", "mind", "minimum", "minor", 148 | "minute", "miracle", "mirror", "misery", "miss", "mistake", "mix", "mixed", "mixture", "mobile", 149 | "model", "modify", "mom", "moment", "monitor", "monkey", "monster", "month", "moon", "moral", 150 | "more", "morning", "mosquito", "mother", "motion", "motor", "mountain", "mouse", "move", 151 | "movie", 152 | "much", "muffin", "mule", "multiply", "muscle", "museum", "mushroom", "music", "must", "mutual", 153 | "myself", "mystery", "myth", "naive", "name", "napkin", "narrow", "nasty", "nation", "nature", 154 | "near", "neck", "need", "negative", "neglect", "neither", "nephew", "nerve", "nest", "net", 155 | "network", "neutral", "never", "news", "next", "nice", "night", "noble", "noise", "nominee", 156 | "noodle", "normal", "north", "nose", "notable", "note", "nothing", "notice", "novel", "now", 157 | "nuclear", "number", "nurse", "nut", "oak", "obey", "object", "oblige", "obscure", "observe", 158 | "obtain", "obvious", "occur", "ocean", "october", "odor", "off", "offer", "office", "often", 159 | "oil", "okay", "old", "olive", "olympic", "omit", "once", "one", "onion", "online", 160 | "only", "open", "opera", "opinion", "oppose", "option", "orange", "orbit", "orchard", "order", 161 | "ordinary", "organ", "orient", "original", "orphan", "ostrich", "other", "outdoor", "outer", 162 | "output", 163 | "outside", "oval", "oven", "over", "own", "owner", "oxygen", "oyster", "ozone", "pact", 164 | "paddle", "page", "pair", "palace", "palm", "panda", "panel", "panic", "panther", "paper", 165 | "parade", "parent", "park", "parrot", "party", "pass", "patch", "path", "patient", "patrol", 166 | "pattern", "pause", "pave", "payment", "peace", "peanut", "pear", "peasant", "pelican", "pen", 167 | "penalty", "pencil", "people", "pepper", "perfect", "permit", "person", "pet", "phone", "photo", 168 | "phrase", "physical", "piano", "picnic", "picture", "piece", "pig", "pigeon", "pill", "pilot", 169 | "pink", "pioneer", "pipe", "pistol", "pitch", "pizza", "place", "planet", "plastic", "plate", 170 | "play", "please", "pledge", "pluck", "plug", "plunge", "poem", "poet", "point", "polar", 171 | "pole", "police", "pond", "pony", "pool", "popular", "portion", "position", "possible", "post", 172 | "potato", "pottery", "poverty", "powder", "power", "practice", "praise", "predict", "prefer", 173 | "prepare", 174 | "present", "pretty", "prevent", "price", "pride", "primary", "print", "priority", "prison", 175 | "private", 176 | "prize", "problem", "process", "produce", "profit", "program", "project", "promote", "proof", 177 | "property", 178 | "prosper", "protect", "proud", "provide", "public", "pudding", "pull", "pulp", "pulse", 179 | "pumpkin", 180 | "punch", "pupil", "puppy", "purchase", "purity", "purpose", "purse", "push", "put", "puzzle", 181 | "pyramid", "quality", "quantum", "quarter", "question", "quick", "quit", "quiz", "quote", 182 | "rabbit", 183 | "raccoon", "race", "rack", "radar", "radio", "rail", "rain", "raise", "rally", "ramp", 184 | "ranch", "random", "range", "rapid", "rare", "rate", "rather", "raven", "raw", "razor", 185 | "ready", "real", "reason", "rebel", "rebuild", "recall", "receive", "recipe", "record", 186 | "recycle", 187 | "reduce", "reflect", "reform", "refuse", "region", "regret", "regular", "reject", "relax", 188 | "release", 189 | "relief", "rely", "remain", "remember", "remind", "remove", "render", "renew", "rent", "reopen", 190 | "repair", "repeat", "replace", "report", "require", "rescue", "resemble", "resist", "resource", 191 | "response", 192 | "result", "retire", "retreat", "return", "reunion", "reveal", "review", "reward", "rhythm", 193 | "rib", 194 | "ribbon", "rice", "rich", "ride", "ridge", "rifle", "right", "rigid", "ring", "riot", 195 | "ripple", "risk", "ritual", "rival", "river", "road", "roast", "robot", "robust", "rocket", 196 | "romance", "roof", "rookie", "room", "rose", "rotate", "rough", "round", "route", "royal", 197 | "rubber", "rude", "rug", "rule", "run", "runway", "rural", "sad", "saddle", "sadness", 198 | "safe", "sail", "salad", "salmon", "salon", "salt", "salute", "same", "sample", "sand", 199 | "satisfy", "satoshi", "sauce", "sausage", "save", "say", "scale", "scan", "scare", "scatter", 200 | "scene", "scheme", "school", "science", "scissors", "scorpion", "scout", "scrap", "screen", 201 | "script", 202 | "scrub", "sea", "search", "season", "seat", "second", "secret", "section", "security", "seed", 203 | "seek", "segment", "select", "sell", "seminar", "senior", "sense", "sentence", "series", 204 | "service", 205 | "session", "settle", "setup", "seven", "shadow", "shaft", "shallow", "share", "shed", "shell", 206 | "sheriff", "shield", "shift", "shine", "ship", "shiver", "shock", "shoe", "shoot", "shop", 207 | "short", "shoulder", "shove", "shrimp", "shrug", "shuffle", "shy", "sibling", "sick", "side", 208 | "siege", "sight", "sign", "silent", "silk", "silly", "silver", "similar", "simple", "since", 209 | "sing", "siren", "sister", "situate", "six", "size", "skate", "sketch", "ski", "skill", 210 | "skin", "skirt", "skull", "slab", "slam", "sleep", "slender", "slice", "slide", "slight", 211 | "slim", "slogan", "slot", "slow", "slush", "small", "smart", "smile", "smoke", "smooth", 212 | "snack", "snake", "snap", "sniff", "snow", "soap", "soccer", "social", "sock", "soda", 213 | "soft", "solar", "soldier", "solid", "solution", "solve", "someone", "song", "soon", "sorry", 214 | "sort", "soul", "sound", "soup", "source", "south", "space", "spare", "spatial", "spawn", 215 | "speak", "special", "speed", "spell", "spend", "sphere", "spice", "spider", "spike", "spin", 216 | "spirit", "split", "spoil", "sponsor", "spoon", "sport", "spot", "spray", "spread", "spring", 217 | "spy", "square", "squeeze", "squirrel", "stable", "stadium", "staff", "stage", "stairs", 218 | "stamp", 219 | "stand", "start", "state", "stay", "steak", "steel", "stem", "step", "stereo", "stick", 220 | "still", "sting", "stock", "stomach", "stone", "stool", "story", "stove", "strategy", "street", 221 | "strike", "strong", "struggle", "student", "stuff", "stumble", "style", "subject", "submit", 222 | "subway", 223 | "success", "such", "sudden", "suffer", "sugar", "suggest", "suit", "summer", "sun", "sunny", 224 | "sunset", "super", "supply", "supreme", "sure", "surface", "surge", "surprise", "surround", 225 | "survey", 226 | "suspect", "sustain", "swallow", "swamp", "swap", "swarm", "swear", "sweet", "swift", "swim", 227 | "swing", "switch", "sword", "symbol", "symptom", "syrup", "system", "table", "tackle", "tag", 228 | "tail", "talent", "talk", "tank", "tape", "target", "task", "taste", "tattoo", "taxi", 229 | "teach", "team", "tell", "ten", "tenant", "tennis", "tent", "term", "test", "text", 230 | "thank", "that", "theme", "then", "theory", "there", "they", "thing", "this", "thought", 231 | "three", "thrive", "throw", "thumb", "thunder", "ticket", "tide", "tiger", "tilt", "timber", 232 | "time", "tiny", "tip", "tired", "tissue", "title", "toast", "tobacco", "today", "toddler", 233 | "toe", "together", "toilet", "token", "tomato", "tomorrow", "tone", "tongue", "tonight", "tool", 234 | "tooth", "top", "topic", "topple", "torch", "tornado", "tortoise", "toss", "total", "tourist", 235 | "toward", "tower", "town", "toy", "track", "trade", "traffic", "tragic", "train", "transfer", 236 | "trap", "trash", "travel", "tray", "treat", "tree", "trend", "trial", "tribe", "trick", 237 | "trigger", "trim", "trip", "trophy", "trouble", "truck", "true", "truly", "trumpet", "trust", 238 | "truth", "try", "tube", "tuition", "tumble", "tuna", "tunnel", "turkey", "turn", "turtle", 239 | "twelve", "twenty", "twice", "twin", "twist", "two", "type", "typical", "ugly", "umbrella", 240 | "unable", "unaware", "uncle", "uncover", "under", "undo", "unfair", "unfold", "unhappy", 241 | "uniform", 242 | "unique", "unit", "universe", "unknown", "unlock", "until", "unusual", "unveil", "update", 243 | "upgrade", 244 | "uphold", "upon", "upper", "upset", "urban", "urge", "usage", "use", "used", "useful", 245 | "useless", "usual", "utility", "vacant", "vacuum", "vague", "valid", "valley", "valve", "van", 246 | "vanish", "vapor", "various", "vast", "vault", "vehicle", "velvet", "vendor", "venture", 247 | "venue", 248 | "verb", "verify", "version", "very", "vessel", "veteran", "viable", "vibrant", "vicious", 249 | "victory", 250 | "video", "view", "village", "vintage", "violin", "virtual", "virus", "visa", "visit", "visual", 251 | "vital", "vivid", "vocal", "voice", "void", "volcano", "volume", "vote", "voyage", "wage", 252 | "wagon", "wait", "walk", "wall", "walnut", "want", "warfare", "warm", "warrior", "wash", 253 | "wasp", "waste", "water", "wave", "way", "wealth", "weapon", "wear", "weasel", "weather", 254 | "web", "wedding", "weekend", "weird", "welcome", "west", "wet", "whale", "what", "wheat", 255 | "wheel", "when", "where", "whip", "whisper", "wide", "width", "wife", "wild", "will", 256 | "win", "window", "wine", "wing", "wink", "winner", "winter", "wire", "wisdom", "wise", 257 | "wish", "witness", "wolf", "woman", "wonder", "wood", "wool", "word", "work", "world", 258 | "worry", "worth", "wrap", "wreck", "wrestle", "wrist", "write", "wrong", "yard", "year", 259 | "yellow", 260 | "you", "young", "youth", "zebra", "zero", "zone", "zoo", 261 | ] 262 | -------------------------------------------------------------------------------- /Sources/XyoClient/Wallet/Extensions.swift: -------------------------------------------------------------------------------- 1 | import BigInt 2 | import Foundation 3 | 4 | extension Array { 5 | subscript(safe index: Int) -> Element? { 6 | return indices.contains(index) ? self[index] : nil 7 | } 8 | } 9 | 10 | extension String { 11 | func leftPad(toLength: Int, with padCharacter: Character) -> String { 12 | let paddingCount = toLength - count 13 | guard paddingCount > 0 else { return self } 14 | return String(repeating: padCharacter, count: paddingCount) + self 15 | } 16 | func binaryToData() throws -> Data { 17 | var data = Data() 18 | for i in stride(from: 0, to: self.count, by: 8) { 19 | let startIndex = self.index(self.startIndex, offsetBy: i) 20 | let endIndex = self.index(startIndex, offsetBy: min(8, self.count - i)) 21 | let byteString = self[startIndex.. BigInt { 34 | return BigInt(self) 35 | } 36 | func toBigUInt() -> BigUInt { 37 | return BigUInt(self) 38 | } 39 | func toBinaryString() -> String { 40 | return self.map { String($0, radix: 2).leftPad(toLength: 8, with: "0") }.joined() 41 | } 42 | } 43 | 44 | extension BigInt { 45 | /// Converts the BigInt to a Data representation. 46 | /// 47 | /// - Parameter length: The desired byte length of the output. Pads with leading zeros if necessary. 48 | /// If `nil`, the result is the minimal byte representation. 49 | /// - Returns: A `Data` object representing the BigInt. 50 | func toData(length: Int? = nil) -> Data { 51 | var magnitudeBytes = self.magnitude.serialize() 52 | 53 | // Adjust for desired length, if specified 54 | if let length = length { 55 | if magnitudeBytes.count < length { 56 | // Pad with leading zeros 57 | let padding = [UInt8](repeating: 0, count: length - magnitudeBytes.count) 58 | magnitudeBytes = padding + magnitudeBytes 59 | } else if magnitudeBytes.count > length { 60 | // Truncate to the specified length 61 | magnitudeBytes = magnitudeBytes.suffix(length) 62 | } 63 | } 64 | 65 | return Data(magnitudeBytes) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Sources/XyoClient/Wallet/Hmac.swift: -------------------------------------------------------------------------------- 1 | import CryptoKit 2 | import Foundation 3 | 4 | struct Hmac { 5 | static func hmacSha512(key: Data, data: Data) -> Data { 6 | let symmetricKey = SymmetricKey(data: key) 7 | let hmac = HMAC.authenticationCode(for: data, using: symmetricKey) 8 | return Data(hmac) // Convert the result to Data 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Sources/XyoClient/Wallet/Secp256k1.swift: -------------------------------------------------------------------------------- 1 | import BigInt 2 | 3 | struct Point { 4 | let x: BigInt 5 | let y: BigInt 6 | } 7 | 8 | struct Secp256k1CurveConstants { 9 | static let p = BigInt( 10 | "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F", radix: 16)! 11 | static let a = BigInt(0) 12 | static let b = BigInt(7) 13 | static let n = BigInt( 14 | "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", radix: 16)! 15 | static let g = Point( 16 | x: BigInt("79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798", radix: 16)!, 17 | y: BigInt("483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8", radix: 16)! 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /Sources/XyoClient/Wallet/Wallet.swift: -------------------------------------------------------------------------------- 1 | import BigInt 2 | import CryptoKit 3 | import CryptoSwift 4 | import Foundation 5 | import secp256k1 6 | 7 | public struct Key { 8 | let privateKey: Data 9 | let chainCode: Data 10 | } 11 | 12 | public enum WalletError: Error { 13 | case failedToGetPublicKey 14 | case failedToDerivePath 15 | case invalidSeed 16 | case invalidPath 17 | case invalidPathComponent 18 | case invalidSeedLength 19 | case failedToGenerateHmac 20 | case invalidPrivateKeyLength 21 | case invalidChildKey 22 | case missingPrivateKey 23 | case missingPublicKey 24 | } 25 | 26 | public class Wallet: Account, WalletInstance, WalletStatic { 27 | 28 | static let defaultPath = "m/44'/60'/0'/0/0" 29 | 30 | private var _key: Key 31 | 32 | init(key: Key) throws { 33 | self._key = key 34 | super.init(key.privateKey) 35 | } 36 | 37 | convenience init(phrase: String, path: String = defaultPath) throws { 38 | let seed = try Bip39.mnemonicToSeed(phrase: phrase) 39 | try self.init(seed: seed, path: path) 40 | } 41 | 42 | convenience init(seed: Data, path: String = defaultPath) throws { 43 | let rootKey = try Bip39.rootPrivateKeyFromSeed(seed: seed) 44 | let derivedKey = try Wallet.deriveKey(from: rootKey, path: path) 45 | try self.init(key: derivedKey) 46 | } 47 | 48 | public static func fromMnemonic(mnemonic: String, path: String?) throws -> any WalletInstance { 49 | return try Wallet(phrase: mnemonic, path: path ?? Wallet.defaultPath) 50 | } 51 | 52 | static func deriveKey(from parentKey: Key, path: String) throws -> Key { 53 | let components = path.split(separator: "/") 54 | 55 | guard components.first == "m" else { 56 | throw WalletError.invalidPath 57 | } 58 | 59 | var currentKey = parentKey 60 | for component in components.dropFirst() { 61 | let hardened = component.last == "'" 62 | let indexString = hardened ? component.dropLast() : component 63 | guard let index = UInt32(indexString) else { 64 | throw WalletError.invalidPathComponent 65 | } 66 | 67 | let derivedIndex = hardened ? index | 0x8000_0000 : index 68 | currentKey = try deriveChildKey(parentKey: currentKey, index: derivedIndex) 69 | } 70 | 71 | return currentKey 72 | } 73 | 74 | /// Derives a child key given a parent key and an index 75 | private static func deriveChildKey(parentKey: Key, index: UInt32) throws -> Key { 76 | var data = Data() 77 | 78 | if index >= 0x8000_0000 { 79 | // Hardened key: prepend 0x00 and parent private key 80 | guard parentKey.privateKey.count == 32 else { 81 | throw WalletError.invalidPrivateKeyLength 82 | } 83 | data.append(0x00) 84 | data.append(parentKey.privateKey) 85 | } else { 86 | // Append the compressed public key 87 | guard 88 | let publicKey = try? Wallet.getCompressedPublicKeyFrom( 89 | privateKey: parentKey.privateKey) 90 | else { 91 | throw WalletError.failedToGetPublicKey 92 | } 93 | data.append(publicKey) 94 | } 95 | 96 | // Append the index 97 | data.append(contentsOf: withUnsafeBytes(of: index.bigEndian, Array.init)) 98 | 99 | // Perform HMAC-SHA512 100 | guard data.count == 37 else { 101 | throw WalletError.failedToGenerateHmac 102 | } 103 | let hmac = Hmac.hmacSha512(key: parentKey.chainCode, data: data) 104 | 105 | // Convert L to an integer 106 | let L = BigInt(hmac.prefix(32).toHex(), radix: 16)! 107 | // Validate L 108 | guard L < Secp256k1CurveConstants.n else { 109 | throw WalletError.invalidChildKey 110 | } 111 | let R = hmac.suffix(32) // Right 32 bytes (R) 112 | 113 | // Compute the child private key: (L + parentPrivateKey) % curveOrder 114 | let parentPrivateKeyInt = BigInt(parentKey.privateKey.toHex(), radix: 16)! 115 | let childPrivateKeyInt = (L + parentPrivateKeyInt) % Secp256k1CurveConstants.n 116 | 117 | // Ensure the child private key is valid 118 | guard childPrivateKeyInt != 0 else { 119 | throw WalletError.invalidChildKey 120 | } 121 | 122 | // Convert the child private key back to Data 123 | var childPrivateKey = childPrivateKeyInt.toData() 124 | guard childPrivateKey.count <= 32 else { 125 | throw WalletError.invalidChildKey 126 | } 127 | 128 | if childPrivateKey.count < 32 { 129 | // Pad with leading zeros to make it 32 bytes 130 | let padding = Data(repeating: 0, count: 32 - data.count) 131 | childPrivateKey = padding + data 132 | } 133 | 134 | // Return the new child key 135 | return Key(privateKey: childPrivateKey, chainCode: Data(R)) 136 | } 137 | 138 | public func derivePath(path: String) throws -> any WalletInstance { 139 | let key = try Wallet.deriveKey(from: self._key, path: path) 140 | return try Wallet(key: key) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /Sources/XyoClient/Wallet/WalletInstance.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol WalletInstance: AccountInstance { 4 | func derivePath(path: String) throws -> WalletInstance 5 | } 6 | -------------------------------------------------------------------------------- /Sources/XyoClient/Wallet/WalletStatic.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol WalletStatic { 4 | 5 | // static func fromExtendedKey(key: String) throws -> WalletInstance 6 | static func fromMnemonic(mnemonic: String, path: String?) throws -> WalletInstance 7 | // static func random() -> WalletInstance 8 | } 9 | -------------------------------------------------------------------------------- /Sources/XyoClient/Witness/Basic/BasicWitness.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | open class BasicWitness: WitnessModuleSync { 4 | 5 | public typealias TPayloadOut = Payload 6 | 7 | public init(observer: @escaping ObserverClosure) { 8 | _observer = observer 9 | super.init() 10 | } 11 | 12 | public init(account: AccountInstance, observer: @escaping ObserverClosure) { 13 | _observer = observer 14 | super.init(account: account) 15 | } 16 | 17 | public typealias ObserverClosure = (() -> EncodablePayloadInstance?) 18 | 19 | private let _observer: ObserverClosure 20 | 21 | override public func observe() -> [EncodablePayloadInstance] { 22 | if let payload = _observer() { 23 | return [payload] 24 | } else { 25 | return [] 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/XyoClient/Witness/Event/EventPayload.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | open class XyoEventPayload: EncodablePayloadInstance { 4 | 5 | let time_stamp = Date() 6 | let event: String 7 | 8 | override init(_ event: String) { 9 | self.event = event 10 | super.init("network.xyo.event") 11 | } 12 | 13 | enum CodingKeys: String, CodingKey { 14 | case time_stamp 15 | case event 16 | } 17 | override open func encode(to encoder: Encoder) throws { 18 | var container = encoder.container(keyedBy: CodingKeys.self) 19 | try container.encode(Int(time_stamp.timeIntervalSince1970 * 1000), forKey: .time_stamp) 20 | try container.encode(event, forKey: .event) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/XyoClient/Witness/Event/EventWitness.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | open class XyoEventWitness: WitnessModuleSync { 4 | 5 | public init(_ observer: @escaping ObserverClosure) { 6 | _observer = observer 7 | super.init() 8 | } 9 | 10 | public init(_ account: AccountInstance, _ observer: @escaping ObserverClosure) { 11 | _observer = observer 12 | super.init(account: account) 13 | } 14 | 15 | public typealias ObserverClosure = (() -> XyoEventPayload?) 16 | 17 | private let _observer: ObserverClosure 18 | 19 | public override func observe() -> [EncodablePayloadInstance] { 20 | if let payload = _observer() { 21 | return [payload] 22 | } else { 23 | return [] 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/XyoClient/Witness/Location/Generic/Coordinates.swift: -------------------------------------------------------------------------------- 1 | import CoreLocation 2 | import Foundation 3 | 4 | struct CoordinatesStruct: Encodable { 5 | 6 | var accuracy: Double? 7 | var altitude: Double? 8 | var altitudeAccuracy: Double? 9 | var heading: Double? 10 | var latitude: Double 11 | var longitude: Double 12 | var speed: Double? 13 | 14 | init( 15 | accuracy: Double?, 16 | altitude: Double?, 17 | altitudeAccuracy: Double?, 18 | heading: Double?, 19 | latitude: Double, 20 | longitude: Double, 21 | speed: Double? 22 | ) { 23 | self.accuracy = accuracy 24 | self.altitude = altitude 25 | self.altitudeAccuracy = altitudeAccuracy 26 | self.heading = heading 27 | self.latitude = latitude 28 | self.longitude = longitude 29 | self.speed = speed 30 | } 31 | 32 | enum CodingKeys: String, CodingKey { 33 | case accuracy 34 | case altitude 35 | case altitudeAccuracy 36 | case heading 37 | case latitude 38 | case longitude 39 | case speed 40 | } 41 | 42 | func encode(to encoder: Encoder) throws { 43 | var container = encoder.container(keyedBy: CodingKeys.self) 44 | 45 | try container.encodeIfValidNumeric(self.accuracy, forKey: .accuracy) 46 | try container.encodeIfValidNumeric(self.altitude, forKey: .altitude) 47 | try container.encodeIfValidNumeric(self.altitudeAccuracy, forKey: .altitudeAccuracy) 48 | try container.encodeIfValidNumeric(self.heading, forKey: .heading) 49 | try container.encode(self.latitude, forKey: .latitude) // Always encode latitude 50 | try container.encode(self.longitude, forKey: .longitude) // Always encode longitude 51 | try container.encodeIfValidNumeric(self.speed, forKey: .speed) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/XyoClient/Witness/Location/Generic/CurrentLocation.swift: -------------------------------------------------------------------------------- 1 | import CoreLocation 2 | import Foundation 3 | 4 | struct CurrentLocationStruct: Encodable { 5 | 6 | var coords: CoordinatesStruct 7 | var timestamp: Date 8 | 9 | init( 10 | coords: CoordinatesStruct, 11 | timestamp: Date 12 | ) { 13 | self.coords = coords 14 | self.timestamp = timestamp 15 | } 16 | 17 | enum CodingKeys: String, CodingKey { 18 | case coords 19 | case timestamp 20 | } 21 | 22 | func encode(to encoder: Encoder) throws { 23 | var container = encoder.container(keyedBy: CodingKeys.self) 24 | 25 | try container.encode(self.coords, forKey: .coords) 26 | try container.encode(Int(self.timestamp.timeIntervalSince1970 * 1000), forKey: .timestamp) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/XyoClient/Witness/Location/Generic/LocationPayload.swift: -------------------------------------------------------------------------------- 1 | import CoreLocation 2 | 3 | open class LocationPayload: EncodablePayloadInstance { 4 | 5 | public static let schema: String = "network.xyo.location.current" 6 | 7 | var location: CLLocation 8 | 9 | public init(_ location: CLLocation) { 10 | self.location = location 11 | super.init(LocationPayload.schema) 12 | } 13 | 14 | enum CodingKeys: String, CodingKey { 15 | case currentLocation 16 | case schema 17 | } 18 | 19 | override open func encode(to encoder: Encoder) throws { 20 | var container = encoder.container(keyedBy: CodingKeys.self) 21 | try container.encode(self.schema, forKey: .schema) 22 | 23 | let coords: CoordinatesStruct = CoordinatesStruct( 24 | accuracy: self.location.horizontalAccuracy, 25 | altitude: self.location.altitude, 26 | altitudeAccuracy: self.location.altitude, 27 | heading: self.location.course, 28 | latitude: self.location.coordinate.latitude, 29 | longitude: self.location.coordinate.longitude, 30 | speed: self.location.speed 31 | ) 32 | let timestamp = self.location.timestamp 33 | let currentLocation: CurrentLocationStruct = CurrentLocationStruct( 34 | coords: coords, timestamp: timestamp) 35 | try container.encode(currentLocation, forKey: .currentLocation) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/XyoClient/Witness/Location/LocationService.swift: -------------------------------------------------------------------------------- 1 | import CoreLocation 2 | import Foundation 3 | 4 | public class LocationService: NSObject, CLLocationManagerDelegate { 5 | private let locationManager = CLLocationManager() 6 | private var locationCompletion: ((Result) -> Void)? 7 | 8 | public override init() { 9 | super.init() 10 | locationManager.delegate = self 11 | locationManager.desiredAccuracy = kCLLocationAccuracyBest 12 | } 13 | 14 | // Method to request location authorization 15 | public func requestAuthorization() { 16 | locationManager.requestWhenInUseAuthorization() 17 | } 18 | 19 | // Method to request the current location once 20 | public func requestLocation(completion: @escaping (Result) -> Void) { 21 | locationCompletion = completion 22 | locationManager.requestLocation() 23 | } 24 | 25 | // CLLocationManagerDelegate methods 26 | public func locationManager( 27 | _ manager: CLLocationManager, didUpdateLocations locations: [CLLocation] 28 | ) { 29 | if let location = locations.last { 30 | locationCompletion?(.success(location)) 31 | } 32 | } 33 | 34 | public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { 35 | locationCompletion?(.failure(error)) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/XyoClient/Witness/Location/LocationServiceProtocol.swift: -------------------------------------------------------------------------------- 1 | import CoreLocation 2 | import Foundation 3 | 4 | public protocol LocationServiceProtocol { 5 | func requestAuthorization() 6 | func requestLocation(completion: @escaping (Result) -> Void) 7 | } 8 | -------------------------------------------------------------------------------- /Sources/XyoClient/Witness/Location/LocationWitness.swift: -------------------------------------------------------------------------------- 1 | import CoreLocation 2 | import Foundation 3 | 4 | open class LocationWitness: WitnessModuleAsync { 5 | 6 | private var locationService: LocationService = LocationService() 7 | 8 | override open func observe(completion: @escaping ([EncodablePayloadInstance]?, Error?) -> Void) 9 | { 10 | locationService.requestAuthorization() 11 | locationService.requestLocation { result in 12 | DispatchQueue.main.async { 13 | switch result { 14 | case .success(let location): 15 | let iosLocationPayload = IosLocationPayload(location) 16 | let locationPayload = LocationPayload(location) 17 | completion([iosLocationPayload, locationPayload], nil) 18 | case .failure(let error): 19 | completion(nil, error) 20 | } 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/XyoClient/Witness/Location/iOS/IosLocationCoordinatePayloadStruct.swift: -------------------------------------------------------------------------------- 1 | import CoreLocation 2 | import Foundation 3 | 4 | struct IosLocationCoordinatePayloadStruct: Encodable { 5 | 6 | var coordinate: CLLocationCoordinate2D 7 | 8 | init(_ coordinate: CLLocationCoordinate2D) { 9 | self.coordinate = coordinate 10 | } 11 | 12 | enum CodingKeys: String, CodingKey { 13 | case latitude 14 | case longitude 15 | } 16 | 17 | func encode(to encoder: Encoder) throws { 18 | var container = encoder.container(keyedBy: CodingKeys.self) 19 | 20 | try container.encode(self.coordinate.latitude, forKey: .latitude) 21 | try container.encode(self.coordinate.longitude, forKey: .longitude) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/XyoClient/Witness/Location/iOS/IosLocationFloorPayloadStruct.swift: -------------------------------------------------------------------------------- 1 | import CoreLocation 2 | import Foundation 3 | 4 | struct IosLocationFloorPayloadStruct: Encodable { 5 | 6 | var floor: CLFloor 7 | 8 | init(_ floor: CLFloor) { 9 | self.floor = floor 10 | } 11 | 12 | enum CodingKeys: String, CodingKey { 13 | case level 14 | } 15 | 16 | func encode(to encoder: Encoder) throws { 17 | var container = encoder.container(keyedBy: CodingKeys.self) 18 | 19 | try container.encode(self.floor.level, forKey: .level) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/XyoClient/Witness/Location/iOS/IosLocationPayload.swift: -------------------------------------------------------------------------------- 1 | import CoreLocation 2 | 3 | open class IosLocationPayload: EncodablePayloadInstance { 4 | 5 | public static let schema: String = "network.xyo.location.ios" 6 | 7 | var location: CLLocation 8 | 9 | public init(_ location: CLLocation) { 10 | self.location = location 11 | super.init(IosLocationPayload.schema) 12 | } 13 | 14 | enum CodingKeys: String, CodingKey { 15 | case altitude 16 | case coordinate 17 | case course 18 | case courseAccuracy 19 | case ellipsoidalAltitude 20 | case floor 21 | case horizontalAccuracy 22 | case schema 23 | case sourceInformation 24 | case speed 25 | case speedAccuracy 26 | case timestamp 27 | case verticalAccuracy 28 | } 29 | 30 | override open func encode(to encoder: Encoder) throws { 31 | var container = encoder.container(keyedBy: CodingKeys.self) 32 | try container.encode(self.schema, forKey: .schema) 33 | 34 | try container.encodeIfValidNumeric(self.location.altitude, forKey: .altitude) 35 | try container.encode( 36 | IosLocationCoordinatePayloadStruct(self.location.coordinate), forKey: .coordinate) 37 | try container.encode(self.location.course, forKey: .course) 38 | if #available(iOS 13.4, *) { 39 | try container.encodeIfValidNumeric( 40 | self.location.courseAccuracy, forKey: .courseAccuracy) 41 | } 42 | if #available(iOS 15, *) { 43 | try container.encodeIfValidNumeric( 44 | self.location.ellipsoidalAltitude, forKey: .ellipsoidalAltitude) 45 | } 46 | if let floor = self.location.floor { 47 | try container.encode( 48 | IosLocationFloorPayloadStruct(floor), forKey: .floor) 49 | } 50 | try container.encodeIfValidNumeric( 51 | self.location.horizontalAccuracy, forKey: .horizontalAccuracy) 52 | if #available(iOS 15.0, *) { 53 | if let sourceInformation = self.location.sourceInformation { 54 | try container.encode( 55 | IosLocationSourceInformationPayloadStruct(sourceInformation), 56 | forKey: .sourceInformation) 57 | } 58 | } 59 | try container.encodeIfValidNumeric(self.location.speed, forKey: .speed) 60 | try container.encodeIfValidNumeric(self.location.speedAccuracy, forKey: .speedAccuracy) 61 | try container.encode( 62 | Int(self.location.timestamp.timeIntervalSince1970 * 1000), forKey: .timestamp) 63 | try container.encodeIfValidNumeric( 64 | self.location.verticalAccuracy, forKey: .verticalAccuracy) 65 | 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Sources/XyoClient/Witness/Location/iOS/IosLocationSourceInformationPayloadStruct.swift: -------------------------------------------------------------------------------- 1 | import CoreLocation 2 | import Foundation 3 | 4 | @available(iOS 15.0, *) 5 | struct IosLocationSourceInformationPayloadStruct: Encodable { 6 | 7 | var sourceInformation: CLLocationSourceInformation 8 | 9 | init(_ sourceInformation: CLLocationSourceInformation) { 10 | self.sourceInformation = sourceInformation 11 | } 12 | 13 | enum CodingKeys: String, CodingKey { 14 | case isProducedByAccessory 15 | case isSimulatedBySoftware 16 | } 17 | 18 | func encode(to encoder: Encoder) throws { 19 | var container = encoder.container(keyedBy: CodingKeys.self) 20 | 21 | try container.encode( 22 | self.sourceInformation.isProducedByAccessory, forKey: .isProducedByAccessory) 23 | try container.encode( 24 | self.sourceInformation.isSimulatedBySoftware, forKey: .isSimulatedBySoftware) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/XyoClient/Witness/SystemInfo/OsName.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | func osName() -> String { 4 | #if os(iOS) 5 | return "iOS" 6 | #elseif os(macOS) 7 | return "macOS" 8 | #elseif os(watchOS) 9 | return "watchOS" 10 | #elseif os(tvOS) 11 | return "tvOS" 12 | #else 13 | return "unknown" 14 | #endif 15 | } 16 | -------------------------------------------------------------------------------- /Sources/XyoClient/Witness/SystemInfo/PathMonitorManager.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Network 3 | 4 | #if os(iOS) 5 | import UIKit 6 | #endif 7 | 8 | public class PathMonitorManager { 9 | let monitor = NWPathMonitor() 10 | let queue = DispatchQueue(label: "Monitor") 11 | // we create a group to prevent deinit while still processing on another thread 12 | let group = DispatchGroup() 13 | var connected: Bool? 14 | var name: String? 15 | var ip: String? 16 | var isWifi: Bool? 17 | var isCellular: Bool? 18 | var isWired: Bool? 19 | var ready = false 20 | var shuttingDown = false 21 | 22 | public init(_ start: Bool = true) { 23 | #if os(iOS) 24 | NotificationCenter.default.addObserver( 25 | self, 26 | selector: #selector(applicationWillEnterForeground(notification:)), 27 | name: UIApplication.willEnterForegroundNotification, 28 | object: nil) 29 | NotificationCenter.default.addObserver( 30 | self, 31 | selector: #selector(applicationWillResignActive(notification:)), 32 | name: UIApplication.willResignActiveNotification, 33 | object: nil) 34 | NotificationCenter.default.addObserver( 35 | self, 36 | selector: #selector(applicationWillResignActive(notification:)), 37 | name: UIApplication.willTerminateNotification, 38 | object: nil) 39 | #endif 40 | if start { 41 | self.start() 42 | } 43 | } 44 | 45 | deinit { 46 | stop() 47 | } 48 | 49 | @objc func applicationWillEnterForeground(notification: Notification) { 50 | self.start() 51 | } 52 | 53 | @objc func applicationWillResignActive(notification: Notification) { 54 | self.stop() 55 | } 56 | 57 | @objc func applicationWillTerminate(notification: Notification) { 58 | self.stop() 59 | } 60 | 61 | func start() { 62 | monitor.start(queue: queue) 63 | monitor.pathUpdateHandler = { path in 64 | // bail if shutting down 65 | if self.shuttingDown { 66 | return 67 | } 68 | self.group.enter() 69 | self.ready = true 70 | self.name = path.availableInterfaces.first?.name 71 | print("Name: \(self.name!)") 72 | self.connected = path.status == .satisfied 73 | print("Connected: \(self.connected!)") 74 | self.isWifi = path.usesInterfaceType(.wifi) 75 | print("Wifi: \(self.isWifi!)") 76 | self.isCellular = path.usesInterfaceType(.cellular) 77 | print("Cellular: \(self.isCellular!)") 78 | self.isWired = path.usesInterfaceType(.wiredEthernet) 79 | print("Wired: \(self.isWired!)") 80 | 81 | if #available(iOS 13, *) { 82 | if let endpoint = path.gateways.first { 83 | switch endpoint { 84 | case .hostPort(let host, _): 85 | self.ip = host.debugDescription 86 | default: 87 | break 88 | } 89 | } else { 90 | self.ip = nil 91 | } 92 | } else { 93 | self.ip = nil 94 | } 95 | 96 | self.group.leave() 97 | } 98 | } 99 | 100 | func stop() { 101 | // stop processing new dispatches 102 | shuttingDown = true 103 | 104 | // stop generating new dispatches 105 | monitor.cancel() 106 | 107 | // wait for any last dispatch, if any, to finish 108 | group.wait() 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Sources/XyoClient/Witness/SystemInfo/SystemInfoCellularProviderPayloadStruct.swift: -------------------------------------------------------------------------------- 1 | import CoreTelephony 2 | import Foundation 3 | 4 | struct SystemInfoCellularProviderPayloadStruct: Encodable { 5 | var allowVoip: Bool? 6 | var icc: String? 7 | var name: String? 8 | var mcc: String? 9 | var mnc: String? 10 | init() { 11 | #if os(iOS) 12 | let networkInfo = CTTelephonyNetworkInfo() 13 | let subscriberCellularProvider = networkInfo.serviceSubscriberCellularProviders?.first? 14 | .value 15 | name = subscriberCellularProvider?.carrierName 16 | mcc = subscriberCellularProvider?.mobileCountryCode 17 | mnc = subscriberCellularProvider?.mobileNetworkCode 18 | icc = subscriberCellularProvider?.isoCountryCode 19 | allowVoip = subscriberCellularProvider?.allowsVOIP 20 | #endif 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/XyoClient/Witness/SystemInfo/SystemInfoDevicePayloadStruct.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class SystemInfoDevicePayloadStruct: Encodable, Decodable { 4 | var model: String? 5 | var nodename: String? 6 | var release: String? 7 | var sysname: String? 8 | var version: String? 9 | 10 | init() { 11 | 12 | } 13 | 14 | public required init(from decoder: Decoder) throws { 15 | let values = try decoder.container(keyedBy: CodingKeys.self) 16 | self.model = try values.decode(String.self, forKey: .model) 17 | self.nodename = try values.decode(String.self, forKey: .nodename) 18 | self.release = try values.decode(String.self, forKey: .release) 19 | self.sysname = try values.decode(String.self, forKey: .sysname) 20 | self.version = try values.decode(String.self, forKey: .version) 21 | } 22 | 23 | enum CodingKeys: String, CodingKey { 24 | case model 25 | case nodename 26 | case release 27 | case sysname 28 | case version 29 | } 30 | 31 | static func load() -> SystemInfoDevicePayloadStruct { 32 | var result = SystemInfoDevicePayloadStruct() 33 | var systemInfo = utsname() 34 | uname(&systemInfo) 35 | result.model = withUnsafePointer(to: &systemInfo.machine) { 36 | $0.withMemoryRebound(to: CChar.self, capacity: 1) { 37 | ptr in String(validatingUTF8: ptr) 38 | } 39 | }?.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) 40 | 41 | result.sysname = withUnsafePointer(to: &systemInfo.sysname) { 42 | $0.withMemoryRebound(to: CChar.self, capacity: 1) { 43 | ptr in String(validatingUTF8: ptr) 44 | } 45 | }?.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) 46 | 47 | result.nodename = withUnsafePointer(to: &systemInfo.nodename) { 48 | $0.withMemoryRebound(to: CChar.self, capacity: 1) { 49 | ptr in String(validatingUTF8: ptr) 50 | } 51 | }?.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) 52 | 53 | result.release = withUnsafePointer(to: &systemInfo.release) { 54 | $0.withMemoryRebound(to: CChar.self, capacity: 1) { 55 | ptr in String(validatingUTF8: ptr) 56 | } 57 | }?.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) 58 | 59 | result.version = withUnsafePointer(to: &systemInfo.version) { 60 | $0.withMemoryRebound(to: CChar.self, capacity: 1) { 61 | ptr in String(validatingUTF8: ptr) 62 | } 63 | }?.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) 64 | return result 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/XyoClient/Witness/SystemInfo/SystemInfoNetworkCellularPayloadStruct.swift: -------------------------------------------------------------------------------- 1 | import CoreTelephony 2 | import Foundation 3 | 4 | struct SystemInfoNetworkCellularPayloadStruct: Encodable { 5 | var ip: String? 6 | var provider = SystemInfoCellularProviderPayloadStruct() 7 | var radio: String? 8 | init(_ wifiInfo: WifiInformation?) { 9 | #if os(iOS) 10 | let networkInfo = CTTelephonyNetworkInfo() 11 | radio = networkInfo.serviceCurrentRadioAccessTechnology?.first?.value 12 | #endif 13 | ip = wifiInfo?.pathMonitor?.ip 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/XyoClient/Witness/SystemInfo/SystemInfoNetworkPayloadStruct.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct SystemInfoNetworkPayloadStruct: Encodable { 4 | var cellular: SystemInfoNetworkCellularPayloadStruct? 5 | var wifi: SystemInfoNetworkWifiPayloadStruct? 6 | var wired: SystemInfoNetworkWiredPayloadStruct? 7 | 8 | init(_ wifiInfo: WifiInformation) { 9 | cellular = wifiInfo.isCellular() ? SystemInfoNetworkCellularPayloadStruct(wifiInfo) : nil 10 | wifi = wifiInfo.isWifi() ? SystemInfoNetworkWifiPayloadStruct(wifiInfo) : nil 11 | wired = wifiInfo.isWired() ? SystemInfoNetworkWiredPayloadStruct(wifiInfo) : nil 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/XyoClient/Witness/SystemInfo/SystemInfoNetworkWifiPayloadStruct.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct SystemInfoNetworkWifiPayloadStruct: Encodable { 4 | var ip: String? 5 | var mac: String? 6 | var rssi: Int? 7 | var security: String? 8 | var ssid: String? 9 | var txPower: Int? 10 | init(_ wifiInfo: WifiInformation?) { 11 | ssid = wifiInfo?.ssid() 12 | mac = wifiInfo?.mac() 13 | rssi = wifiInfo?.rssi() 14 | txPower = wifiInfo?.txPower() 15 | security = wifiInfo?.security() 16 | ip = wifiInfo?.pathMonitor?.ip 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/XyoClient/Witness/SystemInfo/SystemInfoNetworkWiredPayloadStruct.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct SystemInfoNetworkWiredPayloadStruct: Encodable { 4 | var ip: String? 5 | init(_ wifiInfo: WifiInformation?) { 6 | ip = wifiInfo?.pathMonitor?.ip 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Sources/XyoClient/Witness/SystemInfo/SystemInfoOsPayloadStruct.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct SystemInfoOsPayloadStruct: Encodable { 4 | var name: String 5 | var version = SystemInfoOsVersionPayloadStruct() 6 | init() { 7 | name = osName() 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Sources/XyoClient/Witness/SystemInfo/SystemInfoOsVersionPayloadStruct.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct SystemInfoOsVersionPayloadStruct: Encodable { 4 | var major: Int 5 | var minor: Int 6 | var patch: Int 7 | init() { 8 | let osVersion = ProcessInfo().operatingSystemVersion 9 | major = osVersion.majorVersion 10 | minor = osVersion.minorVersion 11 | patch = osVersion.patchVersion 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/XyoClient/Witness/SystemInfo/SystemInfoPayload.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | open class SystemInfoPayload: EncodablePayloadInstance { 4 | 5 | var wifiInfo: WifiInformation 6 | 7 | public init(_ wifiInfo: WifiInformation) { 8 | self.wifiInfo = wifiInfo 9 | super.init("network.xyo.system.info") 10 | } 11 | 12 | enum CodingKeys: String, CodingKey { 13 | case device 14 | case network 15 | case os 16 | case previousHash 17 | case schema 18 | } 19 | override open func encode(to encoder: Encoder) throws { 20 | var container = encoder.container(keyedBy: CodingKeys.self) 21 | try container.encode(SystemInfoDevicePayloadStruct(), forKey: .device) 22 | try container.encode(SystemInfoNetworkPayloadStruct(wifiInfo), forKey: .network) 23 | try container.encode(SystemInfoOsPayloadStruct(), forKey: .os) 24 | try container.encode(self.schema, forKey: .schema) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/XyoClient/Witness/SystemInfo/SystemInfoWitness.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | open class SystemInfoWitness: WitnessModuleSync { 4 | 5 | var allowPathMonitor: Bool 6 | 7 | public init(allowPathMonitor: Bool = false) { 8 | self.allowPathMonitor = allowPathMonitor 9 | } 10 | 11 | public override func observe() -> [EncodablePayloadInstance] { 12 | let payload = SystemInfoPayload(WifiInformation(allowPathMonitor)) 13 | return [payload] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/XyoClient/Witness/SystemInfo/WifiInformation.swift: -------------------------------------------------------------------------------- 1 | import CoreTelephony 2 | import Foundation 3 | 4 | #if os(iOS) 5 | import SystemConfiguration.CaptiveNetwork 6 | #endif 7 | 8 | #if os(macOS) 9 | import CoreWLAN 10 | #endif 11 | 12 | public class WifiInformation { 13 | 14 | var pathMonitor: PathMonitorManager? 15 | 16 | public init(_ allowPathMonitor: Bool = false) { 17 | self.pathMonitor = allowPathMonitor ? PathMonitorManager(true) : nil 18 | } 19 | 20 | #if os(iOS) 21 | func ssid() -> String? { 22 | guard let interfaceNames = CNCopySupportedInterfaces() as? [String] else { 23 | return nil 24 | } 25 | let ssids: [String] = interfaceNames.compactMap { name in 26 | guard let info = CNCopyCurrentNetworkInfo(name as CFString) as? [String: AnyObject] 27 | else { 28 | return nil 29 | } 30 | guard let ssid = info[kCNNetworkInfoKeySSID as String] as? String else { 31 | return nil 32 | } 33 | return ssid 34 | } 35 | return ssids.first 36 | } 37 | #elseif os(macOS) 38 | func ssid() -> String? { 39 | let client = CWWiFiClient.shared() 40 | let interface = client.interface(withName: nil) 41 | return interface?.ssid() 42 | } 43 | #else 44 | func ssid() -> String? { 45 | var ssid: String? 46 | if let interfaces = CNCopySupportedInterfaces() as NSArray? { 47 | for interface in interfaces { 48 | if let interfaceInfo = CNCopyCurrentNetworkInfo(interface as! CFString) 49 | as NSDictionary? 50 | { 51 | ssid = interfaceInfo[kCNNetworkInfoKeySSID as String] as? String 52 | break 53 | } 54 | } 55 | } 56 | return ssid 57 | } 58 | #endif 59 | 60 | #if os(macOS) 61 | func mac() -> String? { 62 | let client = CWWiFiClient.shared() 63 | let interface = client.interface(withName: nil) 64 | return interface?.hardwareAddress() 65 | } 66 | #else 67 | func mac() -> String? { 68 | return nil 69 | } 70 | #endif 71 | 72 | #if os(macOS) 73 | func security() -> String? { 74 | let client = CWWiFiClient.shared() 75 | let interface = client.interface(withName: nil) 76 | guard let security = interface?.security() else { return nil } 77 | switch security { 78 | case .WEP: 79 | return "wep" 80 | case .dynamicWEP: 81 | return "dynamicWEP" 82 | case .enterprise: 83 | return "enterprise" 84 | case .none: 85 | return "none" 86 | case .personal: 87 | return "personal" 88 | case .unknown: 89 | return "unknown" 90 | case .wpa2Enterprise: 91 | return "wpa2Enterprise" 92 | case .wpa2Personal: 93 | return "wpa2Personal" 94 | case .wpa3Enterprise: 95 | return "wpa3Enterprise" 96 | case .wpa3Personal: 97 | return "wpa3Personal" 98 | case .wpa3Transition: 99 | return "wpa3Transition" 100 | case .wpaEnterprise: 101 | return "wpaEnterprise" 102 | case .wpaEnterpriseMixed: 103 | return "wpaEnterpriseMixed" 104 | case .wpaPersonal: 105 | return "wpaPersonal" 106 | case .wpaPersonalMixed: 107 | return "wpaPersonalMixed" 108 | default: 109 | return nil 110 | } 111 | } 112 | #else 113 | func security() -> String? { 114 | return nil 115 | } 116 | #endif 117 | 118 | func isWifi() -> Bool { 119 | return pathMonitor?.isWifi ?? false 120 | } 121 | 122 | func isWired() -> Bool { 123 | return pathMonitor?.isWired ?? false 124 | } 125 | 126 | func isCellular() -> Bool { 127 | return pathMonitor?.isCellular ?? false 128 | } 129 | 130 | #if os(macOS) 131 | func rssi() -> Int? { 132 | let client = CWWiFiClient.shared() 133 | let interface = client.interface(withName: nil) 134 | return interface?.rssiValue() 135 | } 136 | #else 137 | func rssi() -> Int? { 138 | return nil 139 | } 140 | #endif 141 | 142 | #if os(macOS) 143 | func txPower() -> Int? { 144 | let client = CWWiFiClient.shared() 145 | let interface = client.interface(withName: nil) 146 | return interface?.transmitPower() 147 | } 148 | #else 149 | func txPower() -> Int? { 150 | return nil 151 | } 152 | #endif 153 | } 154 | -------------------------------------------------------------------------------- /Sources/XyoClient/Witness/Witness.swift: -------------------------------------------------------------------------------- 1 | public protocol WitnessSync { 2 | func observe() -> [EncodablePayloadInstance] 3 | } 4 | 5 | public protocol WitnessAsync { 6 | func observe(completion: @escaping ([EncodablePayloadInstance]?, Error?) -> Void) 7 | 8 | @available(iOS 15, *) 9 | func observe() async throws -> [EncodablePayloadInstance] 10 | } 11 | -------------------------------------------------------------------------------- /Sources/XyoClient/Witness/WitnessModule.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol WitnessModule: Module {} 4 | 5 | open class WitnessModuleSync: AbstractModule, WitnessSync, WitnessModule { 6 | open func observe() -> [EncodablePayloadInstance] { 7 | preconditionFailure("This method must be overridden") 8 | } 9 | } 10 | 11 | open class WitnessModuleAsync: AbstractModule, WitnessAsync, WitnessModule { 12 | open func observe(completion: @escaping ([EncodablePayloadInstance]?, Error?) -> Void) { 13 | preconditionFailure("This method must be overridden") 14 | } 15 | 16 | @available(iOS 15, *) 17 | open func observe() async throws -> [EncodablePayloadInstance] { 18 | try await withCheckedThrowingContinuation { continuation in 19 | observe { payloads, error in 20 | if let error = error { 21 | continuation.resume(throwing: error) 22 | } else if let payloads = payloads { 23 | continuation.resume(returning: payloads) 24 | } else { 25 | continuation.resume(returning: []) 26 | } 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/XyoClient/extensions/Data.swift: -------------------------------------------------------------------------------- 1 | import CommonCrypto 2 | import Foundation 3 | import keccak 4 | 5 | extension Data { 6 | var pointer: UnsafePointer! { 7 | return withUnsafeBytes { (bytes: UnsafeRawBufferPointer) -> UnsafePointer? in 8 | bytes.baseAddress?.assumingMemoryBound(to: UInt8.self) 9 | } 10 | } 11 | 12 | mutating func mutablePointer() -> UnsafeMutablePointer! { 13 | return withUnsafeMutableBytes { 14 | (bytes: UnsafeMutableRawBufferPointer) -> UnsafeMutablePointer? in 15 | bytes.baseAddress?.assumingMemoryBound(to: UInt8.self) 16 | } 17 | } 18 | 19 | func sha256() -> Data { 20 | return digest(input: self as NSData) as Data 21 | } 22 | 23 | func digest(input: NSData) -> NSData { 24 | let digestLength = Int(CC_SHA256_DIGEST_LENGTH) 25 | var hash = [UInt8](repeating: 0, count: digestLength) 26 | CC_SHA256(input.bytes, UInt32(input.length), &hash) 27 | return NSData(bytes: hash, length: digestLength) 28 | } 29 | 30 | func toHex(_ expectedLength: Int? = nil) -> String { 31 | return Data.hexStringFromData(input: self as NSData).padding( 32 | toLength: expectedLength ?? self.count * 2, withPad: "0", startingAt: 0 33 | ).lowercased() 34 | } 35 | 36 | static func hexStringFromData(input: NSData) -> String { 37 | var bytes = [UInt8](repeating: 0, count: input.length) 38 | input.getBytes(&bytes, length: input.length) 39 | 40 | var hexString = "" 41 | for byte in bytes { 42 | hexString += String(format: "%02x", UInt8(byte)) 43 | } 44 | 45 | return hexString 46 | } 47 | 48 | static func dataFrom(hexString: String) -> Data? { 49 | var data = Data() 50 | let hexString = hexString.trimmingCharacters(in: .whitespacesAndNewlines) 51 | 52 | // Ensure the hex string has an even length 53 | guard hexString.count % 2 == 0 else { 54 | print("Invalid hex string length.") 55 | return nil 56 | } 57 | 58 | var index = hexString.startIndex 59 | while index < hexString.endIndex { 60 | let nextIndex = hexString.index(index, offsetBy: 2) 61 | let byteString = hexString[index.. Data { 75 | var data = Data(count: 32) 76 | keccak_256(data.mutablePointer(), 32, pointer, count) 77 | return data 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Sources/XyoClient/extensions/KeyedEncodingContainer.swift: -------------------------------------------------------------------------------- 1 | extension KeyedEncodingContainer { 2 | mutating func encodeIfValidNumeric(_ value: T?, forKey key: KeyedEncodingContainer.Key) 3 | throws 4 | where T: BinaryFloatingPoint & Encodable { 5 | if let value = value, !value.isNaN { 6 | try encodeIfPresent(value, forKey: key) 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Sources/XyoClient/extensions/String.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum ExtendedStringError: Error { 4 | case sha256HashFailure 5 | } 6 | 7 | extension String { 8 | enum ExtendedEncoding { 9 | case hexadecimal 10 | } 11 | 12 | func sha256() throws -> Data { 13 | if let stringData = data(using: String.Encoding.utf8) { 14 | return stringData.sha256() as Data 15 | } 16 | throw ExtendedStringError.sha256HashFailure 17 | } 18 | 19 | func keccak256() throws -> Data { 20 | if let stringData = data(using: String.Encoding.utf8) { 21 | return stringData.keccak256() 22 | } 23 | throw ExtendedStringError.sha256HashFailure 24 | } 25 | 26 | func data(using encoding: ExtendedEncoding) -> Data? { 27 | let hexStr = self.dropFirst(self.hasPrefix("0x") ? 2 : 0) 28 | 29 | guard hexStr.count % 2 == 0 else { return nil } 30 | 31 | var newData = Data(capacity: hexStr.count / 2) 32 | 33 | var indexIsEven = true 34 | for i in hexStr.indices { 35 | if indexIsEven { 36 | let byteRange = i...hexStr.index(after: i) 37 | guard let byte = UInt8(hexStr[byteRange], radix: 16) else { return nil } 38 | newData.append(byte) 39 | } 40 | indexIsEven.toggle() 41 | } 42 | return newData 43 | } 44 | 45 | func hexToData() -> Data? { 46 | var data = Data(capacity: self.count / 2) 47 | 48 | let regex = try! NSRegularExpression(pattern: "[0-9a-f]{1,2}", options: .caseInsensitive) 49 | regex.enumerateMatches( 50 | in: self, options: [], range: NSRange(location: 0, length: self.count) 51 | ) { 52 | match, _, _ in 53 | let byteString = (self as NSString).substring(with: match!.range) 54 | var num = UInt8(byteString, radix: 16)! 55 | data.append(&num, count: 1) 56 | } 57 | 58 | guard !data.isEmpty else { 59 | return nil 60 | } 61 | 62 | return data 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/keccak/include/keccak.h: -------------------------------------------------------------------------------- 1 | #ifndef KECCAK_FIPS202_H 2 | #define KECCAK_FIPS202_H 3 | #define __STDC_WANT_LIB_EXT1__ 1 4 | #include 5 | #include 6 | 7 | #define decshake(bits) \ 8 | int shake##bits(uint8_t*, size_t, const uint8_t*, size_t); 9 | 10 | #define decsha3(bits) \ 11 | int sha3_##bits(uint8_t*, size_t, const uint8_t*, size_t); 12 | 13 | #define deckeccak(bits) \ 14 | int keccak_##bits(uint8_t*, size_t, const uint8_t*, size_t); 15 | 16 | 17 | decshake(128) 18 | decshake(256) 19 | decsha3(224) 20 | decsha3(256) 21 | decsha3(384) 22 | decsha3(512) 23 | deckeccak(256) 24 | deckeccak(512) 25 | #endif 26 | -------------------------------------------------------------------------------- /Sources/keccak/keccak.c: -------------------------------------------------------------------------------- 1 | /** libkeccak-tiny 2 | * 3 | * A single-file implementation of SHA-3 and SHAKE. 4 | * 5 | * Implementor: David Leon Gil 6 | * License: CC0, attribution kindly requested. Blame taken too, 7 | * but not liability. 8 | */ 9 | #include "keccak.h" 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | /******** The Keccak-f[1600] permutation ********/ 17 | 18 | /*** Constants. ***/ 19 | static const uint8_t rho[24] = \ 20 | { 1, 3, 6, 10, 15, 21, 21 | 28, 36, 45, 55, 2, 14, 22 | 27, 41, 56, 8, 25, 43, 23 | 62, 18, 39, 61, 20, 44}; 24 | static const uint8_t pi[24] = \ 25 | {10, 7, 11, 17, 18, 3, 26 | 5, 16, 8, 21, 24, 4, 27 | 15, 23, 19, 13, 12, 2, 28 | 20, 14, 22, 9, 6, 1}; 29 | static const uint64_t RC[24] = \ 30 | {1ULL, 0x8082ULL, 0x800000000000808aULL, 0x8000000080008000ULL, 31 | 0x808bULL, 0x80000001ULL, 0x8000000080008081ULL, 0x8000000000008009ULL, 32 | 0x8aULL, 0x88ULL, 0x80008009ULL, 0x8000000aULL, 33 | 0x8000808bULL, 0x800000000000008bULL, 0x8000000000008089ULL, 0x8000000000008003ULL, 34 | 0x8000000000008002ULL, 0x8000000000000080ULL, 0x800aULL, 0x800000008000000aULL, 35 | 0x8000000080008081ULL, 0x8000000000008080ULL, 0x80000001ULL, 0x8000000080008008ULL}; 36 | 37 | /*** Helper macros to unroll the permutation. ***/ 38 | #define rol(x, s) (((x) << s) | ((x) >> (64 - s))) 39 | #define REPEAT6(e) e e e e e e 40 | #define REPEAT24(e) REPEAT6(e e e e) 41 | #define REPEAT5(e) e e e e e 42 | #define FOR5(v, s, e) \ 43 | v = 0; \ 44 | REPEAT5(e; v += s;) 45 | 46 | /*** Keccak-f[1600] ***/ 47 | static inline void keccakf(void* state) { 48 | uint64_t* a = (uint64_t*)state; 49 | uint64_t b[5] = {0}; 50 | uint64_t t = 0; 51 | uint8_t x, y; 52 | 53 | for (int i = 0; i < 24; i++) { 54 | // Theta 55 | FOR5(x, 1, 56 | b[x] = 0; 57 | FOR5(y, 5, 58 | b[x] ^= a[x + y]; )) 59 | FOR5(x, 1, 60 | FOR5(y, 5, 61 | a[y + x] ^= b[(x + 4) % 5] ^ rol(b[(x + 1) % 5], 1); )) 62 | // Rho and pi 63 | t = a[1]; 64 | x = 0; 65 | REPEAT24(b[0] = a[pi[x]]; 66 | a[pi[x]] = rol(t, rho[x]); 67 | t = b[0]; 68 | x++; ) 69 | // Chi 70 | FOR5(y, 71 | 5, 72 | FOR5(x, 1, 73 | b[x] = a[y + x];) 74 | FOR5(x, 1, 75 | a[y + x] = b[x] ^ ((~b[(x + 1) % 5]) & b[(x + 2) % 5]); )) 76 | // Iota 77 | a[0] ^= RC[i]; 78 | } 79 | } 80 | 81 | /******** The FIPS202-defined functions. ********/ 82 | 83 | /*** Some helper macros. ***/ 84 | 85 | #define _(S) do { S } while (0) 86 | #define FOR(i, ST, L, S) \ 87 | _(for (size_t i = 0; i < L; i += ST) { S; }) 88 | #define mkapply_ds(NAME, S) \ 89 | static inline void NAME(uint8_t* dst, \ 90 | const uint8_t* src, \ 91 | size_t len) { \ 92 | FOR(i, 1, len, S); \ 93 | } 94 | #define mkapply_sd(NAME, S) \ 95 | static inline void NAME(const uint8_t* src, \ 96 | uint8_t* dst, \ 97 | size_t len) { \ 98 | FOR(i, 1, len, S); \ 99 | } 100 | 101 | mkapply_ds(xorin, dst[i] ^= src[i]) // xorin 102 | mkapply_sd(setout, dst[i] = src[i]) // setout 103 | 104 | #define P keccakf 105 | #define Plen 200 106 | 107 | // Fold P*F over the full blocks of an input. 108 | #define foldP(I, L, F) \ 109 | while (L >= rate) { \ 110 | F(a, I, rate); \ 111 | P(a); \ 112 | I += rate; \ 113 | L -= rate; \ 114 | } 115 | 116 | /** The sponge-based hash construction. **/ 117 | static inline int hash(uint8_t* out, size_t outlen, 118 | const uint8_t* in, size_t inlen, 119 | size_t rate, uint8_t delim) { 120 | if ((out == NULL) || ((in == NULL) && inlen != 0) || (rate >= Plen)) { 121 | return -1; 122 | } 123 | uint8_t a[Plen] = {0}; 124 | // Absorb input. 125 | foldP(in, inlen, xorin); 126 | // Xor in the DS and pad frame. 127 | a[inlen] ^= delim; 128 | a[rate - 1] ^= 0x80; 129 | // Xor in the last block. 130 | xorin(a, in, inlen); 131 | // Apply P 132 | P(a); 133 | // Squeeze output. 134 | foldP(out, outlen, setout); 135 | setout(a, out, outlen); 136 | memset_s(a, 200, 0, 200); 137 | return 0; 138 | } 139 | 140 | /*** Helper macros to define SHA3 and SHAKE instances. ***/ 141 | #define defshake(bits) \ 142 | int shake##bits(uint8_t* out, size_t outlen, \ 143 | const uint8_t* in, size_t inlen) { \ 144 | return hash(out, outlen, in, inlen, 200 - (bits / 4), 0x1f); \ 145 | } 146 | #define defsha3(bits) \ 147 | int sha3_##bits(uint8_t* out, size_t outlen, \ 148 | const uint8_t* in, size_t inlen) { \ 149 | if (outlen > (bits/8)) { \ 150 | return -1; \ 151 | } \ 152 | return hash(out, outlen, in, inlen, 200 - (bits / 4), 0x06); \ 153 | } 154 | 155 | #define defkeccak(bits) \ 156 | int keccak_##bits(uint8_t* out, size_t outlen, \ 157 | const uint8_t* in, size_t inlen) { \ 158 | if (outlen > (bits/8)) { \ 159 | return -1; \ 160 | } \ 161 | return hash(out, outlen, in, inlen, 200 - (bits / 4), 0x01); \ 162 | } 163 | 164 | /*** FIPS202 SHAKE VOFs ***/ 165 | defshake(128) 166 | defshake(256) 167 | 168 | /*** FIPS202 SHA3 FOFs ***/ 169 | defsha3(224) 170 | defsha3(256) 171 | defsha3(384) 172 | defsha3(512) 173 | 174 | /*** pre-FIPS202 Keccak standard ***/ 175 | defkeccak(256) 176 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import sdk_xyo_client_swiftTests 3 | 4 | var tests = [XCTestCaseEntry]() 5 | tests += sdk_xyo_client_swiftTests.allTests() 6 | XCTMain(tests) 7 | -------------------------------------------------------------------------------- /Tests/XyoClientTests/Account/Account.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | @testable import XyoClient 4 | 5 | class AccountTests: XCTestCase { 6 | 7 | static var allTests = [ 8 | ( 9 | "testAccount_PreviousHash_IsNil_WhenNoPreviousSignings", 10 | testAccount_PreviousHash_IsNil_WhenNoPreviousSignings 11 | ), 12 | ( 13 | "testAccount_PreviousHash_IsPreviousHash_WhenPreviouslySigned1", 14 | testAccount_PreviousHash_IsPreviousHash_WhenPreviouslySigned1 15 | ), 16 | ( 17 | "testAccount_PreviousHash_IsPreviousHash_WhenPreviouslySigned2", 18 | testAccount_PreviousHash_IsPreviousHash_WhenPreviouslySigned2 19 | ), 20 | ] 21 | 22 | override func setUp() { 23 | super.setUp() 24 | Account.previousHashStore = MemoryPreviousHashStore() 25 | } 26 | 27 | func testAccount_PreviousHash_IsNil_WhenNoPreviousSignings() { 28 | // Arrange 29 | let account = Account.random() 30 | 31 | // Assert 32 | XCTAssertNil(account.previousHash) 33 | } 34 | 35 | func testAccount_PreviousHash_IsPreviousHash_WhenPreviouslySigned1() throws { 36 | // Arrange 37 | let account = Account.random() 38 | let _ = try account.sign(testPayload1Hash) 39 | 40 | // Assert 41 | XCTAssertEqual(account.previousHash, testPayload1Hash) 42 | } 43 | 44 | func testAccount_PreviousHash_IsPreviousHash_WhenPreviouslySigned2() throws { 45 | // Arrange 46 | let account = XyoAddress() 47 | let hexString = account.privateKeyHex! 48 | let key = Data.dataFrom(hexString: hexString)! 49 | 50 | // Act 51 | // Create an account from the private key 52 | let account1 = Account.fromPrivateKey(key) 53 | // Sign something so the previousHash is stored 54 | let _ = try account1.sign(testPayload2Hash) 55 | // Next account creation should hydrate previousHash from store 56 | let account2 = Account.fromPrivateKey(key) 57 | 58 | // Assert 59 | XCTAssertEqual(account1.previousHash, account2.previousHash) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Tests/XyoClientTests/Account/AccountServices.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | @testable import XyoClient 4 | 5 | class AccountServicesTests: XCTestCase { 6 | 7 | static var allTests = [ 8 | ( 9 | "testGetNamedAccount_CreatesAndReturnsNewAccount_WhenNoExistingAccount", 10 | testGetNamedAccount_CreatesAndReturnsNewAccount_WhenNoExistingAccount 11 | ), 12 | ( 13 | "testGetNamedAccount_ReturnsExistingAccount_WhenAccountExists", 14 | testGetNamedAccount_ReturnsExistingAccount_WhenAccountExists 15 | ), 16 | ] 17 | 18 | func testGetNamedAccount_CreatesAndReturnsNewAccount_WhenNoExistingAccount() { 19 | // Act 20 | let account = AccountServices.getNamedAccount(name: "testAccount1") 21 | 22 | // Assert 23 | XCTAssertNotNil(account) 24 | XCTAssertNotNil(account.address) 25 | } 26 | 27 | func testGetNamedAccount_ReturnsExistingAccount_WhenAccountExists() { 28 | // Act 29 | // Initial attempt create account 30 | let accountA = AccountServices.getNamedAccount(name: "testAccount2") 31 | // Subsequent ones retrieve account 32 | let accountB = AccountServices.getNamedAccount(name: "testAccount2") 33 | 34 | // Asserts 35 | let addressA = accountA.address 36 | let addressB = accountB.address 37 | XCTAssertNotNil(addressA) 38 | XCTAssertNotNil(addressB) 39 | XCTAssertEqual(addressA, addressB) 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /Tests/XyoClientTests/Account/Address.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | @testable import XyoClient 4 | 5 | let testVectorPrivateKey = "7f71bc5644f8f521f7e9b73f7a391e82c05432f8a9d36c44d6b1edbf1d8db62f" 6 | let testVectorPublicKey = 7 | "ed6f3b86542f45aab88ec48ab1366b462bd993fec83e234054afd8f2311fba774800fdb40c04918463b463a6044b83413a604550bfba8f8911beb65475d6528e" 8 | let testVectorKeccak = "0889fa0b3d5bb98e749c7bf75e7a847447e7fec41011ae7d32d768f86605ba03" 9 | let testVectorAddress = "5e7a847447e7fec41011ae7d32d768f86605ba03" 10 | let testVectorHash = "4b688df40bcedbe641ddb16ff0a1842d9c67ea1c3bf63f3e0471baa664531d1a" 11 | let testVectorSignature = 12 | "b61dad551e910e2793b4f9f880125b5799086510ce102fad0222c1b093c60a6bc755ca10a9068079ac8d9617416a7cd41077093061c1e9bcb2f81812086ae603" 13 | 14 | final class AddressTests: XCTestCase { 15 | static var allTests = [ 16 | ("testGeneratedPrivateKey", testGeneratedPrivateKey), 17 | ("testKnownPrivateKey", testKnownPrivateKey), 18 | ] 19 | 20 | func testGeneratedPrivateKey() { 21 | let address = XyoAddress() 22 | XCTAssertNotNil(address) 23 | 24 | XCTAssertEqual(address.privateKeyBytes?.count, 32) 25 | XCTAssertEqual(address.publicKeyBytes?.count, 64) 26 | XCTAssertEqual(address.keccakBytes?.count, 32) 27 | XCTAssertEqual(address.addressBytes?.count, 20) 28 | 29 | XCTAssertEqual(address.privateKeyHex?.count, 64) 30 | XCTAssertEqual(address.publicKeyHex?.count, 128) 31 | XCTAssertEqual(address.keccakHex?.count, 64) 32 | XCTAssertEqual(address.addressHex?.count, 40) 33 | } 34 | 35 | func testKnownPrivateKey() { 36 | let address = XyoAddress(privateKey: testVectorPrivateKey) 37 | // let signature = try? address.sign(testVectorHash) 38 | XCTAssertNotNil(address) 39 | XCTAssertEqual(address.privateKeyHex, testVectorPrivateKey) 40 | XCTAssertEqual(address.publicKeyHex, testVectorPublicKey) 41 | XCTAssertEqual(address.keccakHex, testVectorKeccak) 42 | XCTAssertEqual(address.addressHex, testVectorAddress) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Tests/XyoClientTests/BoundWitness/BoundWitnessBuilder.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | @testable import XyoClient 4 | 5 | @available(iOS 13.0, *) 6 | final class BoundWitnessBuilderTests: XCTestCase { 7 | static var allTests = [ 8 | ("Build Returns Expected Hash", test_build_returnsExpectedHash) 9 | ] 10 | 11 | override func setUp() { 12 | super.setUp() 13 | // Ensure previousHash = nil for tests addresses 14 | Account.previousHashStore = MemoryPreviousHashStore() 15 | } 16 | 17 | func test_build_returnsExpectedHash() throws { 18 | for testCase in boundWitnessSequenceTestCases { 19 | // Create accounts 20 | var signers: [AccountInstance] = [] 21 | for (i, mnemonic) in testCase.mnemonics.enumerated() { 22 | let path = testCase.paths[i] 23 | if let account = try? Wallet.fromMnemonic(mnemonic: mnemonic, path: path) { 24 | signers.append(account) 25 | } else { 26 | XCTAssertTrue(false, "Error creating account from mnemonic") 27 | } 28 | } 29 | XCTAssertEqual( 30 | testCase.addresses.count, 31 | signers.count, 32 | "Incorrect number of accounts created." 33 | ) 34 | XCTAssertEqual( 35 | testCase.addresses.compactMap { $0.lowercased() }, 36 | signers.compactMap { $0.address?.toHex().lowercased() }, 37 | "Incorrect addresses when creating accounts." 38 | ) 39 | // Ensure correct initial account state 40 | for (i, previousHash) in testCase.previousHashes.enumerated() { 41 | XCTAssertEqual( 42 | testCase.previousHashes[i], 43 | previousHash, 44 | "Incorrect previous hash for account" 45 | ) 46 | } 47 | 48 | // Build the BW 49 | let builder = try BoundWitnessBuilder().signers(signers).payloads(testCase.payloads) 50 | let (bw, payloads) = try builder.build() 51 | let dataHash = try PayloadBuilder.dataHash(from: bw.typedPayload) 52 | // print( 53 | // try PayloadBuilder.toJsonWithMeta( 54 | // from: bw.typedPayload, meta: bw.meta)) 55 | let rootHash = try PayloadBuilder.hash(from: bw.typedPayload, meta: bw.typedMeta) 56 | 57 | // Ensure the BW is correct 58 | XCTAssertEqual(dataHash.toHex(), testCase.dataHash, "Incorrect data hash in BW") 59 | XCTAssertEqual(rootHash.toHex(), testCase.rootHash, "Incorrect root hash in BW") 60 | for (i, expectedPayloadHash) in testCase.payloadHashes.enumerated() { 61 | let actualPayloadHash = bw.typedPayload.payload_hashes[i] 62 | // Ensure payload hash is correct from test data 63 | XCTAssertEqual( 64 | expectedPayloadHash, actualPayloadHash, 65 | "Incorrect payload hash in BW as compared to test data") 66 | // Ensure payload hash is correct as calculated from returned payloads 67 | let dataHash = try PayloadBuilder.dataHash(from: payloads[i]) 68 | XCTAssertEqual( 69 | expectedPayloadHash, dataHash.toHex(), 70 | "Incorrect payload hash in BW as compared to BoundWitnessBuilder returned payloads data hash" 71 | ) 72 | } 73 | for (i, payload) in testCase.payloads.enumerated() { 74 | let actualSchema = bw.typedPayload.payload_schemas[i] 75 | // Ensure payload hash is correct 76 | XCTAssertEqual(payload.schema, actualSchema, "Incorrect payload schema in BW") 77 | } 78 | 79 | // Ensure correct ending account state 80 | for signer in signers { 81 | // Ensure previous hash is correct 82 | XCTAssertEqual(signer.previousHash, dataHash, "Incorrect previous hash for account") 83 | } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Tests/XyoClientTests/Panel/Panel.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | @testable import XyoClient 4 | 5 | @available(iOS 13.0, *) 6 | final class PanelTests: XCTestCase { 7 | static var allTests = [ 8 | ( 9 | "createPanel", testCreatePanel, 10 | "simplePanelReport", testSimplePanelReport, 11 | "singleWitnessPanel", testSingleWitnessPanel, 12 | "multiWitnessPanel", testMultiWitnessPanel 13 | ) 14 | ] 15 | 16 | let basicWitness = BasicWitness(observer: { 17 | return EncodablePayloadInstance("network.xyo.basic") 18 | }) 19 | let systemInfoWitness = SystemInfoWitness() 20 | 21 | func testCreatePanel() throws { 22 | let panel = XyoPanel(witnesses: [basicWitness]) 23 | XCTAssertNotNil(panel) 24 | } 25 | 26 | @available(iOS 15, *) 27 | func testSimplePanelReport() async throws { 28 | let panel = XyoPanel() 29 | let result = try await panel.reportQuery() 30 | XCTAssertEqual(result.bw.typedPayload.addresses.count, 1) 31 | XCTAssertEqual(result.bw.typedPayload.addresses[0], panel.account.address?.toHex()) 32 | XCTAssertTrue(result.payloads.isEmpty, "Expected empty result from panel report") 33 | } 34 | 35 | @available(iOS 15, *) 36 | func testSingleWitnessPanel() async throws { 37 | let witnesses = [basicWitness] 38 | let panel = XyoPanel( 39 | witnesses: [basicWitness] 40 | ) 41 | let result = try await panel.reportQuery() 42 | XCTAssertEqual(result.bw.typedPayload.addresses.count, 1) 43 | XCTAssertEqual(result.bw.typedPayload.addresses[0], panel.account.address?.toHex()) 44 | XCTAssertEqual( 45 | result.payloads.count, witnesses.count, 46 | "Expected \(witnesses.count) payloads in the panel report result") 47 | } 48 | 49 | @available(iOS 15, *) 50 | func testMultiWitnessPanel() async throws { 51 | let witnesses = [basicWitness, systemInfoWitness] 52 | let panel = XyoPanel( 53 | witnesses: witnesses 54 | ) 55 | let result = try await panel.reportQuery() 56 | XCTAssertEqual(result.bw.typedPayload.addresses.count, 1) 57 | XCTAssertEqual(result.bw.typedPayload.addresses[0], panel.account.address?.toHex()) 58 | XCTAssertEqual( 59 | result.payloads.count, witnesses.count, 60 | "Expected \(witnesses.count) payloads in the panel report result") 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Tests/XyoClientTests/PrevioiusHashStore/CoreDataPreviousHashStoreTests.swift: -------------------------------------------------------------------------------- 1 | import CoreData 2 | import XCTest 3 | 4 | @testable import XyoClient 5 | 6 | final class CoreDataPreviousHashStoreTests: XCTestCase { 7 | var provider: TestPersistentContainerProvider! 8 | var store: CoreDataPreviousHashStore! 9 | 10 | override func setUp() { 11 | super.setUp() 12 | // Initialize the test provider and store 13 | provider = TestPersistentContainerProvider() 14 | store = CoreDataPreviousHashStore(provider: provider) 15 | } 16 | 17 | override func tearDown() { 18 | // Clean up after tests 19 | provider = nil 20 | store = nil 21 | super.tearDown() 22 | } 23 | 24 | func testSetItemAndGetItem() { 25 | let address = Data("2d0fb5708b9d68bfaa96c6e426cbc66a341f117d")! 26 | let hash = Data("fb2b2ed349278d35b2cf32ec719227cf2a0f099f3a3305307bce15362eca32b9")! 27 | 28 | // Set an item 29 | store.setItem(address: address, previousHash: hash) 30 | 31 | // Get the item and verify it was stored correctly 32 | let retrievedHash = store.getItem(address: address) 33 | XCTAssertEqual(retrievedHash, hash, "Retrieved hash should match the stored hash.") 34 | } 35 | 36 | func testRemoveItem() { 37 | let address = Data("f90b9ad30ea94d3df17d51c727c416b46faf18b6")! 38 | let hash = Data("8a76ed3fa2507859e43f24ea0e6c03acb1782281429294bb8123b6d9e73f1710")! 39 | 40 | // Set an item 41 | store.setItem(address: address, previousHash: hash) 42 | 43 | // Remove the item 44 | store.removeItem(address: address) 45 | 46 | // Verify the item is removed 47 | let retrievedHash = store.getItem(address: address) 48 | XCTAssertNil(retrievedHash, "Hash should be nil after removal.") 49 | } 50 | 51 | func testUpdateItem() { 52 | let address = Data("85e7a0494c1feb184a80d64aca7bef07d8efd960")! 53 | let initialHash = Data("6c509659288d86d4961906299692239d40e5e3a8834ab89a473d9031e50703e0")! 54 | let updatedHash = Data("c2842590d989afae0bf2970b31d0323f97fe68c71a1c9d13bf275bbed13cf92c")! 55 | 56 | // Set an initial item 57 | store.setItem(address: address, previousHash: initialHash) 58 | 59 | // Update the item with a new hash 60 | store.setItem(address: address, previousHash: updatedHash) 61 | 62 | // Verify the item was updated 63 | let retrievedHash = store.getItem(address: address) 64 | XCTAssertEqual(retrievedHash, updatedHash, "Hash should match the updated value.") 65 | } 66 | 67 | func testGetItemNonExistentAddress() { 68 | let address = Data("f8ede235dbc41c06936d46a26d9038a58ba254a1")! 69 | 70 | // Try to get an item that doesn't exist 71 | let retrievedHash = store.getItem(address: address) 72 | 73 | // Verify the result is nil 74 | XCTAssertNil(retrievedHash, "Hash should be nil for a non-existent address.") 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Tests/XyoClientTests/TestData/TestBoundWitnessSequence.swift: -------------------------------------------------------------------------------- 1 | import XyoClient 2 | 3 | public struct BoundWitnessSequenceTestCase { 4 | public var mnemonics: [String] 5 | public var paths: [String] 6 | public var addresses: [String] 7 | public var payloads: [EncodablePayloadInstance] 8 | public var payloadHashes: [String] 9 | public var previousHashes: [String?] 10 | public var dataHash: String 11 | public var rootHash: String 12 | } 13 | 14 | public struct PayloadsWithHashes { 15 | public var payloads: [EncodablePayloadInstance] 16 | public var payloadHashes: [String] 17 | } 18 | 19 | let payloadSequences: [PayloadsWithHashes] = [ 20 | .init( 21 | payloads: [IdPayload(0)], 22 | payloadHashes: [ 23 | "ada56ff753c0c9b2ce5e1f823eda9ac53501db2843d8883d6cf6869c18ef7f65" 24 | ] 25 | ), 26 | .init( 27 | payloads: [IdPayload(1)], 28 | payloadHashes: [ 29 | "3a3b8deca568ff820b0b7c8714fbdf82b40fb54f4b15aca8745e06b81291558e" 30 | ] 31 | ), 32 | .init( 33 | payloads: [IdPayload(2), IdPayload(3)], 34 | payloadHashes: [ 35 | "1a40207fab71fc184e88557d5bee6196cbbb49f11f73cda85000555a628a8f0a", 36 | "c4bce9b4d3239fcc9a248251d1bef1ba7677e3c0c2c43ce909a6668885b519e6", 37 | ] 38 | ), 39 | .init( 40 | payloads: [IdPayload(4), IdPayload(5)], 41 | payloadHashes: [ 42 | "59c0374dd801ae64ddddba27320ca028d7bd4b3d460f6674c7da1b4aa9c956d6", 43 | "5d9b8e84bc824280fcbb6290904c2edbb401d626ad9789717c0a23d1cab937b0", 44 | ] 45 | ), 46 | ] 47 | 48 | let wallet1Mnemonic = 49 | "report door cry include salad horn recipe luxury access pledge husband maple busy double olive" 50 | let wallet1Path = "m/44'/60'/0'/0/0" 51 | let wallet1Address = "25524Ca99764D76CA27604Bb9727f6e2f27C4533" 52 | 53 | let wallet2Mnemonic = 54 | "turn you orphan sauce act patient village entire lava transfer height sense enroll quit idle" 55 | let wallet2Path = "m/44'/60'/0'/0/0" 56 | let wallet2Address = "FdCeD2c3549289049BeBf743fB721Df211633fBF" 57 | 58 | let boundWitnessSequenceTestCase1: BoundWitnessSequenceTestCase = .init( 59 | mnemonics: [wallet1Mnemonic], 60 | paths: [wallet1Path], 61 | addresses: [wallet1Address], 62 | payloads: payloadSequences[0].payloads, 63 | payloadHashes: payloadSequences[0].payloadHashes, 64 | previousHashes: [nil], 65 | dataHash: "750113b9826ba94b622667b06cd8467f1330837581c28907c16160fec20d0a4b", 66 | rootHash: "d8c29f77505e5da7479de1aa6474b247b348004a90bf7048e60581592deac1e7" 67 | ) 68 | 69 | let boundWitnessSequenceTestCase2: BoundWitnessSequenceTestCase = .init( 70 | mnemonics: [wallet2Mnemonic], 71 | paths: [wallet2Path], 72 | addresses: [wallet2Address], 73 | payloads: payloadSequences[1].payloads, 74 | payloadHashes: payloadSequences[1].payloadHashes, 75 | previousHashes: [nil], 76 | dataHash: "bacd010d79126a154339e59c11c5b46be032c3bef65626f83bcafe968dc6dd1b", 77 | rootHash: "ea1d3dd28daea3df2c7d50ffcecec3be95c8011636a6590598a4aab0ce2b6971" 78 | ) 79 | 80 | let boundWitnessSequenceTestCase3: BoundWitnessSequenceTestCase = .init( 81 | mnemonics: [wallet1Mnemonic, wallet2Mnemonic], 82 | paths: [wallet1Path, wallet2Path], 83 | addresses: [wallet1Address, wallet2Address], 84 | payloads: payloadSequences[2].payloads, 85 | payloadHashes: payloadSequences[2].payloadHashes, 86 | previousHashes: [ 87 | "750113b9826ba94b622667b06cd8467f1330837581c28907c16160fec20d0a4b", 88 | "bacd010d79126a154339e59c11c5b46be032c3bef65626f83bcafe968dc6dd1b", 89 | ], 90 | dataHash: "73245ef73517913f4b57c12d56d81199968ecd8fbefea9ddc474f43dd6cfa8c8", 91 | rootHash: "02caf1f81905ec9311b3b4793309f462567b35516d7dee7ce62d1e4759b7022a" 92 | ) 93 | 94 | let boundWitnessSequenceTestCase4: BoundWitnessSequenceTestCase = .init( 95 | mnemonics: [wallet1Mnemonic, wallet2Mnemonic], 96 | paths: [wallet1Path, wallet2Path], 97 | addresses: [wallet1Address, wallet2Address], 98 | payloads: payloadSequences[3].payloads, 99 | payloadHashes: payloadSequences[3].payloadHashes, 100 | previousHashes: [ 101 | "73245ef73517913f4b57c12d56d81199968ecd8fbefea9ddc474f43dd6cfa8c8", 102 | "73245ef73517913f4b57c12d56d81199968ecd8fbefea9ddc474f43dd6cfa8c8", 103 | ], 104 | dataHash: "210d86ea43d82b85a49b77959a8ee4e6016ff7036254cfa39953befc66073010", 105 | rootHash: "a99467084abb2d7812f4d529a2e84d566716aca9443c4b4800e016572cf91416" 106 | ) 107 | 108 | let boundWitnessSequenceTestCases = [ 109 | boundWitnessSequenceTestCase1, 110 | boundWitnessSequenceTestCase2, 111 | boundWitnessSequenceTestCase3, 112 | boundWitnessSequenceTestCase4, 113 | ] 114 | -------------------------------------------------------------------------------- /Tests/XyoClientTests/TestData/TestPayload1.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XyoClient 3 | 4 | public class TestPayload1SubObject: Encodable { 5 | var number_value = 2 6 | var string_value = "yo" 7 | } 8 | 9 | public class TestPayload1: EncodablePayloadInstance { 10 | var timestamp = 1_618_603_439_107 11 | var number_field = 1 12 | var object_field = TestPayload1SubObject() 13 | var string_field = "there" 14 | 15 | enum CodingKeys: String, CodingKey { 16 | case schema 17 | case timestamp 18 | case number_field 19 | case object_field 20 | case string_field 21 | } 22 | 23 | public override func encode(to encoder: Encoder) throws { 24 | var container = encoder.container(keyedBy: CodingKeys.self) 25 | try container.encode(number_field, forKey: .number_field) 26 | try container.encode(object_field, forKey: .object_field) 27 | try container.encode(schema, forKey: .schema) 28 | try container.encode(string_field, forKey: .string_field) 29 | try container.encode(timestamp, forKey: .timestamp) 30 | } 31 | } 32 | 33 | let testPayload1 = TestPayload1("network.xyo.test") 34 | let testPayload1Hash: Hash = Hash( 35 | "c915c56dd93b5e0db509d1a63ca540cfb211e11f03039b05e19712267bb8b6db")! 36 | -------------------------------------------------------------------------------- /Tests/XyoClientTests/TestData/TestPayload2.swift: -------------------------------------------------------------------------------- 1 | import XyoClient 2 | 3 | public class TestPayload2SubObject: Encodable { 4 | var string_value = "yo" 5 | var number_value = 2 6 | var optional_field: String? = nil 7 | } 8 | 9 | public class TestPayload2: EncodablePayloadInstance { 10 | var string_field = "there" 11 | var object_field = TestPayload2SubObject() 12 | var timestamp = 1_618_603_439_107 13 | var number_field = 1 14 | 15 | enum CodingKeys: String, CodingKey { 16 | case schema 17 | case string_field 18 | case object_field 19 | case timestamp 20 | case number_field 21 | case optional_field 22 | } 23 | 24 | public override func encode(to encoder: Encoder) throws { 25 | var container = encoder.container(keyedBy: CodingKeys.self) 26 | try container.encode(number_field, forKey: .number_field) 27 | try container.encode(object_field, forKey: .object_field) 28 | try container.encode(schema, forKey: .schema) 29 | try container.encode(string_field, forKey: .string_field) 30 | try container.encode(timestamp, forKey: .timestamp) 31 | } 32 | } 33 | 34 | let testPayload2 = TestPayload2("network.xyo.test") 35 | let testPayload2Hash: Hash = Hash( 36 | "c915c56dd93b5e0db509d1a63ca540cfb211e11f03039b05e19712267bb8b6db")! 37 | -------------------------------------------------------------------------------- /Tests/XyoClientTests/Wallet/Wallet.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | @testable import XyoClient 4 | 5 | struct MasterKeyTestVector: Decodable { 6 | let hexEncodedSeed: String 7 | let base58CheckEncodedKey: String 8 | } 9 | 10 | class WalletTests: XCTestCase { 11 | 12 | let publicMasterKeyTestData: [MasterKeyTestVector] = 13 | [ 14 | MasterKeyTestVector( 15 | hexEncodedSeed: "000102030405060708090a0b0c0d0e0f", 16 | base58CheckEncodedKey: 17 | "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi" 18 | ), 19 | MasterKeyTestVector( 20 | hexEncodedSeed: 21 | "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542", 22 | base58CheckEncodedKey: 23 | "xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U" 24 | ), 25 | MasterKeyTestVector( 26 | hexEncodedSeed: 27 | "4b381541583be4423346c643850da4b320e46a87ae3d2a4e6da11eba819cd4acba45d239319ac14f863b8d5ab5a0d0c64d2e8a1e7d1457df2e5a3c51c73235be", 28 | base58CheckEncodedKey: 29 | "xprv9s21ZrQH143K25QhxbucbDDuQ4naNntJRi4KUfWT7xo4EKsHt2QJDu7KXp1A3u7Bi1j8ph3EGsZ9Xvz9dGuVrtHHs7pXeTzjuxBrCmmhgC6" 30 | ), 31 | MasterKeyTestVector( 32 | hexEncodedSeed: "3ddd5602285899a946114506157c7997e5444528f3003f6134712147db19b678", 33 | base58CheckEncodedKey: 34 | "xprv9s21ZrQH143K48vGoLGRPxgo2JNkJ3J3fqkirQC2zVdk5Dgd5w14S7fRDyHH4dWNHUgkvsvNDCkvAwcSHNAQwhwgNMgZhLtQC63zxwhQmRv" 35 | ), 36 | ] 37 | 38 | func test_generateFromMnemonic() { 39 | let phrase = 40 | "later puppy sound rebuild rebuild noise ozone amazing hope broccoli crystal grief" 41 | let rootEntropy = "7d55c33f59ab352ba7a03e6d638cd533" 42 | let paths = [ 43 | "0/4", "44'/0'/0'", "44'/60'/0'/0/0", "44'/60'/0'/0/1", "49'/0'/0'", "84'/0'/0'", 44 | "84'/0'/0'/0", 45 | ] 46 | let pathAddresses = [ 47 | "0/4", "44'/0'/0'", "44'/60'/0'/0/0", "44'/60'/0'/0/1", "49'/0'/0'", "84'/0'/0'", 48 | "84'/0'/0'/0", 49 | ] 50 | 51 | do { 52 | 53 | let entropy = try Bip39.mnemonicToEntropy(phrase: phrase) 54 | let reconstructedMnemonic = try Bip39.entropyToMnemonic(entropy: entropy) 55 | print("Original Mnemonic: \(phrase)") 56 | print("Reconstructed Mnemonic: \(reconstructedMnemonic)") 57 | XCTAssertEqual(phrase, reconstructedMnemonic) 58 | 59 | let sut = try Wallet(phrase: phrase, path: "m/44'/60'/0'/0/0") 60 | let sut2 = try Wallet(phrase: phrase, path: "m/44'/60'/0'/0/1") 61 | XCTAssertNotEqual(sut.address, sut2.address) 62 | let calcedEntropy = try Bip39.mnemonicToEntropy(phrase: phrase) 63 | let calcedPhrase = try Bip39.entropyToMnemonic(entropy: calcedEntropy) 64 | 65 | XCTAssertEqual(phrase, calcedPhrase) 66 | 67 | XCTAssertEqual(rootEntropy, calcedEntropy.toHex()) 68 | 69 | //let sutPath0 = try sut.derivePath(path: paths[0]) 70 | //XCTAssertEqual(sutPath0.address?.toHex(), pathAddresses[0]) 71 | 72 | // Assert 73 | XCTAssertNil(nil) 74 | } catch { 75 | print("\nCaught error: \(error)\n") 76 | XCTAssertTrue(false) 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Tests/XyoClientTests/Wallet/WalletVectors.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | @testable import XyoClient 4 | 5 | struct TestVector: Decodable { 6 | let address: String 7 | let path: String 8 | let phrase: String 9 | let privateKey: String 10 | let publicKey: String 11 | let publicKeyUncompressed: String 12 | } 13 | 14 | let testCases: [TestVector] = [ 15 | .init( 16 | address: "e46c258c74c7c1df33d7caa4c2c664dc0843ab3f", 17 | path: "m/44'/60'/0'/0/0", 18 | phrase: "later puppy sound rebuild rebuild noise ozone amazing hope broccoli crystal grief", 19 | privateKey: "96a7705eedbb701a03ee235911253fd3eb80e48a06106c0bf957d42b72bd8efa", 20 | publicKey: "03a9f10779cb44e73a1983b8225ce9de96ff63cbc8a2900db102fa55a38a14b206", 21 | publicKeyUncompressed: 22 | "04a9f10779cb44e73a1983b8225ce9de96ff63cbc8a2900db102fa55a38a14b206f850a6decf0d0277c8ea237d865a06b6237f07eaf4273217ed6b2ed830161bef" 23 | ) 24 | ] 25 | 26 | class WalletVectorTests: XCTestCase { 27 | 28 | func testCreationFromPrivateKey() { 29 | for vector in testCases { 30 | do { 31 | let entropy = try Bip39.mnemonicToEntropy(phrase: vector.phrase) 32 | let seed = try Bip39.entropyToSeed(entropy: entropy) 33 | let pk = try Bip39.rootPrivateKeyFromSeed(seed: seed) 34 | let pkDerived = try Wallet.deriveKey(from: pk, path: vector.path) 35 | let wallet2 = try Wallet(key: pkDerived) 36 | XCTAssertEqual(wallet2.privateKey?.toHex(), vector.privateKey) 37 | XCTAssertEqual(wallet2.address?.toHex(), vector.address) 38 | XCTAssertEqual(wallet2.publicKey?.toHex(), vector.publicKey) 39 | XCTAssertEqual(wallet2.publicKeyUncompressed?.toHex(), vector.publicKeyUncompressed) 40 | } catch { 41 | print("\nCaught error: \(error)\n") 42 | XCTAssertTrue(false) 43 | } 44 | } 45 | } 46 | 47 | func testCreationFromPhrase() { 48 | for vector in testCases { 49 | do { 50 | let wallet = try Wallet(phrase: vector.phrase, path: vector.path) 51 | XCTAssertEqual(wallet.privateKey?.toHex(), vector.privateKey) 52 | XCTAssertEqual(wallet.address?.toHex(), vector.address) 53 | XCTAssertEqual(wallet.publicKey?.toHex(), vector.publicKey) 54 | XCTAssertEqual(wallet.publicKeyUncompressed?.toHex(), vector.publicKeyUncompressed) 55 | } catch { 56 | print("\nCaught error: \(error)\n") 57 | XCTAssertTrue(false) 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Tests/XyoClientTests/Witness/Location/Generic/LocationPayloadTests.swift: -------------------------------------------------------------------------------- 1 | import CoreLocation 2 | import XCTest 3 | 4 | @testable import XyoClient 5 | 6 | class LocationPayloadTests: XCTestCase { 7 | struct CoordinatesStruct: Encodable { 8 | let accuracy: CLLocationAccuracy 9 | let altitude: CLLocationDistance 10 | let altitudeAccuracy: CLLocationDistance 11 | let heading: CLLocationDirection 12 | let latitude: CLLocationDegrees 13 | let longitude: CLLocationDegrees 14 | let speed: CLLocationSpeed 15 | } 16 | 17 | struct CurrentLocationStruct: Encodable { 18 | let coords: CoordinatesStruct 19 | let timestamp: Date 20 | } 21 | 22 | func testLocationPayloadEncoding() throws { 23 | // Arrange: Create a CLLocation instance and the corresponding LocationPayload 24 | let location = CLLocation( 25 | coordinate: CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194), 26 | altitude: 15.0, 27 | horizontalAccuracy: 5.0, 28 | verticalAccuracy: 3.0, 29 | course: 90.0, 30 | speed: 2.5, 31 | timestamp: Date(timeIntervalSince1970: 1_609_459_200) // Jan 1, 2021 32 | ) 33 | let payload = LocationPayload(location) 34 | 35 | // Act: Encode the LocationPayload instance into JSON 36 | let jsonString = try PayloadBuilder.toJson(from: payload) 37 | 38 | // Assert: Verify the serialized JSON matches expectations 39 | let expectedJSON = """ 40 | { 41 | "currentLocation" : { 42 | "coords" : { 43 | "accuracy" : 5, 44 | "altitude" : 15, 45 | "altitudeAccuracy" : 15, 46 | "heading" : 90, 47 | "latitude" : 37.7749, 48 | "longitude" : -122.4194, 49 | "speed" : 2.5 50 | }, 51 | "timestamp" : 1609459200000 52 | }, 53 | "schema" : "network.xyo.location.current" 54 | } 55 | """.filter { !$0.isWhitespace } 56 | XCTAssertEqual(jsonString, expectedJSON) 57 | let dataHash = try PayloadBuilder.dataHash(from: payload) 58 | XCTAssertEqual( 59 | dataHash, Data("0c1f0c80481b0f391a677eab542a594a192081325b6416acc3dc99db23355ee2")) 60 | let hash = try PayloadBuilder.hash(fromWithMeta: EncodableWithMetaInstance(from: payload)) 61 | XCTAssertEqual( 62 | hash, Data("0c1f0c80481b0f391a677eab542a594a192081325b6416acc3dc99db23355ee2")) 63 | } 64 | 65 | func testLocationPayloadEncodingHandlesNilValues() throws { 66 | // Arrange: Create a CLLocation instance with some properties unset 67 | let location = CLLocation( 68 | coordinate: CLLocationCoordinate2D(latitude: 0.0, longitude: 0.0), 69 | altitude: CLLocationDistance.nan, 70 | horizontalAccuracy: CLLocationAccuracy.nan, 71 | verticalAccuracy: CLLocationAccuracy.nan, 72 | course: CLLocationDirection.nan, 73 | speed: CLLocationSpeed.nan, 74 | timestamp: Date(timeIntervalSince1970: 0) 75 | ) 76 | let payload = LocationPayload(location) 77 | 78 | // Act: Encode the LocationPayload instance into JSON 79 | let jsonString = try PayloadBuilder.toJson(from: payload) 80 | 81 | // Assert: Verify the serialized JSON handles NaN values gracefully (e.g., omitted or replaced) 82 | let expectedJSON = """ 83 | { 84 | "currentLocation" : { 85 | "coords" : { 86 | "latitude" : 0, 87 | "longitude" : 0 88 | }, 89 | "timestamp" : 0 90 | }, 91 | "schema" : "network.xyo.location.current" 92 | } 93 | """.filter { !$0.isWhitespace } 94 | XCTAssertEqual(jsonString, expectedJSON) 95 | let dataHash = try PayloadBuilder.dataHash(from: payload) 96 | XCTAssertEqual( 97 | dataHash, Data("c1bd7396f998a50d20401efd4b5da0cf6670f9418c6f60b42f4c54f3663305c3")) 98 | let hash = try PayloadBuilder.hash(fromWithMeta: EncodableWithMetaInstance(from: payload)) 99 | XCTAssertEqual( 100 | hash, Data("c1bd7396f998a50d20401efd4b5da0cf6670f9418c6f60b42f4c54f3663305c3")) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Tests/XyoClientTests/Witness/Location/LocationWitness.swift: -------------------------------------------------------------------------------- 1 | //import CoreLocation 2 | //import XCTest 3 | // 4 | //@testable import XyoClient 5 | // 6 | //private class MockLocationService: LocationServiceProtocol { 7 | // var didRequestAuthorization = false 8 | // var simulatedResult: Result? 9 | // 10 | // func requestAuthorization() { 11 | // didRequestAuthorization = true 12 | // } 13 | // 14 | // func requestLocation(completion: @escaping (Result) -> Void) { 15 | // if let result = simulatedResult { 16 | // completion(result) 17 | // } 18 | // } 19 | //} 20 | // 21 | //@available(iOS 13.0, *) 22 | //final class LocationWitnessTests: XCTestCase { 23 | // static var allTests = [ 24 | // ( 25 | // "observe:returnsMultipleLocationPayloads", 26 | // testLocationWitness_observe_returnsMultipleLocationPayloads 27 | // ) 28 | // ] 29 | // 30 | // @available(iOS 15, *) 31 | // func testLocationWitness_observe_returnsMultipleLocationPayloads() async throws { 32 | // let locationServiceMock = MockLocationService() 33 | // let latitude: Double = 1 34 | // let longitude: Double = 2 35 | // locationServiceMock.simulatedResult = .success( 36 | // CLLocation(latitude: latitude, longitude: longitude)) 37 | // let sut = LocationWitness(locationService: locationServiceMock) 38 | // let results = try await sut.observe() 39 | // // XCTAssertEqual(results.count, 2) 40 | // // let locationPayload = try XCTUnwrap( 41 | // // results.compactMap { $0 as? LocationPayload }.first, "Missing location payload.") 42 | // // XCTAssertEqual(locationPayload.schema, LocationPayload.schema) 43 | // // XCTAssertEqual(locationPayload.location.coordinate.latitude, lattitiude) 44 | // // XCTAssertEqual(locationPayload.location.coordinate.longitude, longitude) 45 | // let iosLocationPayload = try XCTUnwrap( 46 | // results.compactMap { $0 as? IosLocationPayload }.first, "Missing iOS location payload.") 47 | // XCTAssertEqual(iosLocationPayload.schema, IosLocationPayload.schema) 48 | // XCTAssertEqual(iosLocationPayload.location.coordinate.latitude, latitude) 49 | // XCTAssertEqual(iosLocationPayload.location.coordinate.longitude, longitude) 50 | // 51 | // } 52 | //} 53 | -------------------------------------------------------------------------------- /Tests/XyoClientTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(BoundWitnessTests.allTests), 7 | testCase(AddressTests.allTests), 8 | ] 9 | } 10 | #endif 11 | --------------------------------------------------------------------------------