├── .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 |
--------------------------------------------------------------------------------