├── .gitignore
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── Demo
├── FCLDemo
│ ├── FCLDemo.xcodeproj
│ │ ├── project.pbxproj
│ │ ├── project.xcworkspace
│ │ │ ├── contents.xcworkspacedata
│ │ │ └── xcshareddata
│ │ │ │ ├── IDEWorkspaceChecks.plist
│ │ │ │ └── swiftpm
│ │ │ │ └── Package.resolved
│ │ └── xcshareddata
│ │ │ └── xcschemes
│ │ │ └── FCLDemo.xcscheme
│ └── FCLDemo
│ │ ├── AppDelegate.swift
│ │ ├── Assets.xcassets
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ └── Contents.json
│ │ ├── Contents.json
│ │ ├── error.imageset
│ │ │ ├── Contents.json
│ │ │ └── error-cutout.png
│ │ ├── ic28Copy.imageset
│ │ │ ├── Contents.json
│ │ │ └── ic28Copy.pdf
│ │ ├── ic28Earth.imageset
│ │ │ ├── Contents.json
│ │ │ └── ic28Earth.pdf
│ │ ├── icExamination.imageset
│ │ │ ├── Contents.json
│ │ │ └── sh-process-audit-process-processing-icon-with-png-and-vector-516865-cutout.png
│ │ └── icon20Selected.imageset
│ │ │ ├── Contents.json
│ │ │ └── icon20Selected.pdf
│ │ ├── Base.lproj
│ │ ├── LaunchScreen.storyboard
│ │ └── Main.storyboard
│ │ ├── Info.plist
│ │ ├── SceneDelegate.swift
│ │ └── ViewController.swift
├── FCL_Cocoa_Demo
│ ├── FCL_Cocoa_Demo.xcodeproj
│ │ ├── project.pbxproj
│ │ ├── project.xcworkspace
│ │ │ └── xcshareddata
│ │ │ │ └── IDEWorkspaceChecks.plist
│ │ └── xcshareddata
│ │ │ └── xcschemes
│ │ │ └── FCL_Cocoa_Demo.xcscheme
│ ├── FCL_Cocoa_Demo.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ ├── FCL_Cocoa_Demo
│ │ ├── AppDelegate.swift
│ │ ├── Assets.xcassets
│ │ │ ├── AccentColor.colorset
│ │ │ │ └── Contents.json
│ │ │ ├── AppIcon.appiconset
│ │ │ │ └── Contents.json
│ │ │ ├── Contents.json
│ │ │ ├── error.imageset
│ │ │ │ ├── Contents.json
│ │ │ │ └── error-cutout.png
│ │ │ ├── ic28Copy.imageset
│ │ │ │ ├── Contents.json
│ │ │ │ └── ic28Copy.pdf
│ │ │ ├── ic28Earth.imageset
│ │ │ │ ├── Contents.json
│ │ │ │ └── ic28Earth.pdf
│ │ │ ├── icExamination.imageset
│ │ │ │ ├── Contents.json
│ │ │ │ └── sh-process-audit-process-processing-icon-with-png-and-vector-516865-cutout.png
│ │ │ └── icon20Selected.imageset
│ │ │ │ ├── Contents.json
│ │ │ │ └── icon20Selected.pdf
│ │ ├── Base.lproj
│ │ │ ├── LaunchScreen.storyboard
│ │ │ └── Main.storyboard
│ │ ├── FlowDemoViewController.swift
│ │ ├── Info.plist
│ │ └── SceneDelegate.swift
│ ├── Gemfile
│ ├── Gemfile.lock
│ ├── Podfile
│ └── Podfile.lock
└── FCL_SwiftUI_Demo
│ ├── FCL-SwiftUI-Demo-Info.plist
│ ├── FCL_SwiftUI_Demo.xcodeproj
│ ├── project.pbxproj
│ └── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── swiftpm
│ │ └── Package.resolved
│ └── FCL_SwiftUI_Demo
│ ├── Assets.xcassets
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ ├── AppIcon.appiconset
│ │ └── Contents.json
│ └── Contents.json
│ ├── ContentView.swift
│ ├── ExplorerURLType.swift
│ ├── FCL_SwiftUI_DemoApp.swift
│ ├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
│ └── ViewModel.swift
├── FCL-SDK.podspec
├── LICENSE
├── Package.swift
├── README.md
├── Sources
└── FCL-SDK
│ ├── AppUtilities
│ └── AppUtilities.swift
│ ├── Config
│ ├── AddressReplacement.swift
│ ├── AppDetail.swift
│ └── Config.swift
│ ├── Constants.swift
│ ├── Extensions
│ ├── CadenceArgumentExtension.swift
│ ├── Extensions.swift
│ └── TaskExtension.swift
│ ├── FCL.swift
│ ├── FCLDelegate.swift
│ ├── FCLError.swift
│ ├── Models
│ ├── AccountProofData.swift
│ ├── AuthData.swift
│ ├── ClientInfo.swift
│ ├── DynamicKey.swift
│ ├── FCLCompositeSignature.swift
│ ├── Interaction.swift
│ ├── Pragma.swift
│ ├── PreSignable.swift
│ ├── ProviderInfo.swift
│ ├── ResponseStatus.swift
│ ├── Role.swift
│ ├── RoleType.swift
│ ├── Signable.swift
│ ├── SignableUser.swift
│ ├── Singature.swift
│ ├── TransactionFeePayer.swift
│ └── Voucher.swift
│ ├── Network
│ ├── PollingResponse.swift
│ └── URLSessionExtension.swift
│ ├── Resolve
│ ├── AccountsResolver.swift
│ ├── CadenceResolver.swift
│ ├── RefBlockResolver.swift
│ ├── Resolver.swift
│ ├── SequenceNumberResolver.swift
│ └── SignatureResolver.swift
│ ├── User
│ ├── Service
│ │ ├── Service.swift
│ │ ├── ServiceAccountProof.swift
│ │ ├── ServiceDataType.swift
│ │ ├── ServiceIdentity.swift
│ │ ├── ServiceMethod.swift
│ │ ├── ServiceProvider.swift
│ │ └── ServiceType.swift
│ └── User.swift
│ ├── Utilities
│ └── RequestBuilder.swift
│ ├── WalletProvider
│ ├── BloctoWalletProvider.swift
│ ├── DapperWalletProvider.swift
│ └── WalletProvider.swift
│ ├── WalletProviderSelectionViewController.swift
│ └── WalletUtilities
│ └── WalletUtilities.swift
├── Tests
└── FCLTests
│ └── FCLTests.swift
├── docs-asset
├── FCL-Swift.jpg
├── wallet-discovery.png
└── xcode-build-target.png
└── scripts
└── bump-publish-version.sh
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 | DerivedData/
7 | .swiftpm/config/registries.json
8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
9 | .netrc
10 |
11 | # CocoaPods
12 | Pods/
13 | **/Pods/
14 |
15 | fastlane
16 | #
17 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
18 | # screenshots whenever they are needed.
19 | # For more information about the recommended setup visit:
20 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
21 |
22 | report.xml
23 | Preview.html
24 | screenshots
25 | test_output
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Demo/FCLDemo/FCLDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Demo/FCLDemo/FCLDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Demo/FCLDemo/FCLDemo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "alamofire",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/Alamofire/Alamofire.git",
7 | "state" : {
8 | "revision" : "354dda32d89fc8cd4f5c46487f64957d355f53d8",
9 | "version" : "5.6.1"
10 | }
11 | },
12 | {
13 | "identity" : "bigint",
14 | "kind" : "remoteSourceControl",
15 | "location" : "https://github.com/attaswift/BigInt.git",
16 | "state" : {
17 | "revision" : "0ed110f7555c34ff468e72e1686e59721f2b0da6",
18 | "version" : "5.3.0"
19 | }
20 | },
21 | {
22 | "identity" : "blocto-ios-sdk",
23 | "kind" : "remoteSourceControl",
24 | "location" : "https://github.com/portto/blocto-ios-sdk.git",
25 | "state" : {
26 | "revision" : "1cbecf62ad3655703137aa7141f12664af52142f",
27 | "version" : "0.6.1"
28 | }
29 | },
30 | {
31 | "identity" : "cryptoswift",
32 | "kind" : "remoteSourceControl",
33 | "location" : "https://github.com/krzyzanowskim/CryptoSwift.git",
34 | "state" : {
35 | "revision" : "039f56c5d7960f277087a0be51f5eb04ed0ec073",
36 | "version" : "1.5.1"
37 | }
38 | },
39 | {
40 | "identity" : "flow-swift-sdk",
41 | "kind" : "remoteSourceControl",
42 | "location" : "https://github.com/portto/flow-swift-sdk.git",
43 | "state" : {
44 | "revision" : "52448238d0e887af02fc0bdaf50ca76426317289",
45 | "version" : "0.5.0"
46 | }
47 | },
48 | {
49 | "identity" : "grpc-swift",
50 | "kind" : "remoteSourceControl",
51 | "location" : "https://github.com/grpc/grpc-swift.git",
52 | "state" : {
53 | "revision" : "d114c5ec34015bab663b3b7afaa7d6197656e47e",
54 | "version" : "1.9.0"
55 | }
56 | },
57 | {
58 | "identity" : "runtime",
59 | "kind" : "remoteSourceControl",
60 | "location" : "https://github.com/wickwirew/Runtime.git",
61 | "state" : {
62 | "revision" : "dad03135d7701a4e7b3a4051e75d6b37bd8e178e",
63 | "version" : "2.2.4"
64 | }
65 | },
66 | {
67 | "identity" : "rxswift",
68 | "kind" : "remoteSourceControl",
69 | "location" : "https://github.com/ReactiveX/RxSwift",
70 | "state" : {
71 | "revision" : "b4307ba0b6425c0ba4178e138799946c3da594f8",
72 | "version" : "6.5.0"
73 | }
74 | },
75 | {
76 | "identity" : "secp256k1",
77 | "kind" : "remoteSourceControl",
78 | "location" : "git@github.com:portto/secp256k1.git",
79 | "state" : {
80 | "revision" : "6864a2560066cedede330c4b344689432a7300f7",
81 | "version" : "0.0.5"
82 | }
83 | },
84 | {
85 | "identity" : "secp256k1.swift",
86 | "kind" : "remoteSourceControl",
87 | "location" : "https://github.com/portto/secp256k1.swift",
88 | "state" : {
89 | "revision" : "23aa6bab1f60e513297d0d58a863418f68534e56",
90 | "version" : "0.7.4"
91 | }
92 | },
93 | {
94 | "identity" : "snapkit",
95 | "kind" : "remoteSourceControl",
96 | "location" : "https://github.com/SnapKit/SnapKit",
97 | "state" : {
98 | "revision" : "f222cbdf325885926566172f6f5f06af95473158",
99 | "version" : "5.6.0"
100 | }
101 | },
102 | {
103 | "identity" : "solana-web3.swift",
104 | "kind" : "remoteSourceControl",
105 | "location" : "https://github.com/portto/solana-web3.swift",
106 | "state" : {
107 | "revision" : "9f430df4ce564d900e41da37821d555b2e644b62",
108 | "version" : "0.0.4"
109 | }
110 | },
111 | {
112 | "identity" : "swift-log",
113 | "kind" : "remoteSourceControl",
114 | "location" : "https://github.com/apple/swift-log.git",
115 | "state" : {
116 | "revision" : "5d66f7ba25daf4f94100e7022febf3c75e37a6c7",
117 | "version" : "1.4.2"
118 | }
119 | },
120 | {
121 | "identity" : "swift-nio",
122 | "kind" : "remoteSourceControl",
123 | "location" : "https://github.com/apple/swift-nio.git",
124 | "state" : {
125 | "revision" : "124119f0bb12384cef35aa041d7c3a686108722d",
126 | "version" : "2.40.0"
127 | }
128 | },
129 | {
130 | "identity" : "swift-nio-extras",
131 | "kind" : "remoteSourceControl",
132 | "location" : "https://github.com/apple/swift-nio-extras.git",
133 | "state" : {
134 | "revision" : "a75e92bde3683241c15df3dd905b7a6dcac4d551",
135 | "version" : "1.12.1"
136 | }
137 | },
138 | {
139 | "identity" : "swift-nio-http2",
140 | "kind" : "remoteSourceControl",
141 | "location" : "https://github.com/apple/swift-nio-http2.git",
142 | "state" : {
143 | "revision" : "108ac15087ea9b79abb6f6742699cf31de0e8772",
144 | "version" : "1.22.0"
145 | }
146 | },
147 | {
148 | "identity" : "swift-nio-ssl",
149 | "kind" : "remoteSourceControl",
150 | "location" : "https://github.com/apple/swift-nio-ssl.git",
151 | "state" : {
152 | "revision" : "42436a25ff32c390465567f5c089a9a8ce8d7baf",
153 | "version" : "2.20.0"
154 | }
155 | },
156 | {
157 | "identity" : "swift-nio-transport-services",
158 | "kind" : "remoteSourceControl",
159 | "location" : "https://github.com/apple/swift-nio-transport-services.git",
160 | "state" : {
161 | "revision" : "2cb54f91ddafc90832c5fa247faf5798d0a7c204",
162 | "version" : "1.13.0"
163 | }
164 | },
165 | {
166 | "identity" : "swift-protobuf",
167 | "kind" : "remoteSourceControl",
168 | "location" : "https://github.com/apple/swift-protobuf.git",
169 | "state" : {
170 | "revision" : "e1499bc69b9040b29184f7f2996f7bab467c1639",
171 | "version" : "1.19.0"
172 | }
173 | },
174 | {
175 | "identity" : "swiftyjson",
176 | "kind" : "remoteSourceControl",
177 | "location" : "https://github.com/SwiftyJSON/SwiftyJSON.git",
178 | "state" : {
179 | "revision" : "2b6054efa051565954e1d2b9da831680026cd768",
180 | "version" : "4.3.0"
181 | }
182 | },
183 | {
184 | "identity" : "tweetnacl-swiftwrap",
185 | "kind" : "remoteSourceControl",
186 | "location" : "https://github.com/bitmark-inc/tweetnacl-swiftwrap.git",
187 | "state" : {
188 | "revision" : "f8fd111642bf2336b11ef9ea828510693106e954",
189 | "version" : "1.1.0"
190 | }
191 | }
192 | ],
193 | "version" : 2
194 | }
195 |
--------------------------------------------------------------------------------
/Demo/FCLDemo/FCLDemo.xcodeproj/xcshareddata/xcschemes/FCLDemo.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
54 |
60 |
62 |
68 |
69 |
70 |
71 |
73 |
74 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/Demo/FCLDemo/FCLDemo/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // FCLDemo
4 | //
5 | // Created by Andrew Wang on 2022/6/29.
6 | //
7 |
8 | import UIKit
9 |
10 | @main
11 | class AppDelegate: UIResponder, UIApplicationDelegate {
12 |
13 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
14 | // Override point for customization after application launch.
15 | return true
16 | }
17 |
18 | }
19 |
20 |
--------------------------------------------------------------------------------
/Demo/FCLDemo/FCLDemo/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Demo/FCLDemo/FCLDemo/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "scale" : "1x",
46 | "size" : "20x20"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "scale" : "2x",
51 | "size" : "20x20"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "scale" : "1x",
56 | "size" : "29x29"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "29x29"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "scale" : "1x",
66 | "size" : "40x40"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "scale" : "2x",
71 | "size" : "40x40"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "scale" : "1x",
76 | "size" : "76x76"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "scale" : "2x",
81 | "size" : "76x76"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "scale" : "2x",
86 | "size" : "83.5x83.5"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "scale" : "1x",
91 | "size" : "1024x1024"
92 | }
93 | ],
94 | "info" : {
95 | "author" : "xcode",
96 | "version" : 1
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Demo/FCLDemo/FCLDemo/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Demo/FCLDemo/FCLDemo/Assets.xcassets/error.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "error-cutout.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Demo/FCLDemo/FCLDemo/Assets.xcassets/error.imageset/error-cutout.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocto/fcl-swift/3a90dcd96910ef9ad405e1e96b2447b3ca3759b5/Demo/FCLDemo/FCLDemo/Assets.xcassets/error.imageset/error-cutout.png
--------------------------------------------------------------------------------
/Demo/FCLDemo/FCLDemo/Assets.xcassets/ic28Copy.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "ic28Copy.pdf",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Demo/FCLDemo/FCLDemo/Assets.xcassets/ic28Copy.imageset/ic28Copy.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocto/fcl-swift/3a90dcd96910ef9ad405e1e96b2447b3ca3759b5/Demo/FCLDemo/FCLDemo/Assets.xcassets/ic28Copy.imageset/ic28Copy.pdf
--------------------------------------------------------------------------------
/Demo/FCLDemo/FCLDemo/Assets.xcassets/ic28Earth.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "ic28Earth.pdf",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Demo/FCLDemo/FCLDemo/Assets.xcassets/ic28Earth.imageset/ic28Earth.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocto/fcl-swift/3a90dcd96910ef9ad405e1e96b2447b3ca3759b5/Demo/FCLDemo/FCLDemo/Assets.xcassets/ic28Earth.imageset/ic28Earth.pdf
--------------------------------------------------------------------------------
/Demo/FCLDemo/FCLDemo/Assets.xcassets/icExamination.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "sh-process-audit-process-processing-icon-with-png-and-vector-516865-cutout.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Demo/FCLDemo/FCLDemo/Assets.xcassets/icExamination.imageset/sh-process-audit-process-processing-icon-with-png-and-vector-516865-cutout.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocto/fcl-swift/3a90dcd96910ef9ad405e1e96b2447b3ca3759b5/Demo/FCLDemo/FCLDemo/Assets.xcassets/icExamination.imageset/sh-process-audit-process-processing-icon-with-png-and-vector-516865-cutout.png
--------------------------------------------------------------------------------
/Demo/FCLDemo/FCLDemo/Assets.xcassets/icon20Selected.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "icon20Selected.pdf",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Demo/FCLDemo/FCLDemo/Assets.xcassets/icon20Selected.imageset/icon20Selected.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocto/fcl-swift/3a90dcd96910ef9ad405e1e96b2447b3ca3759b5/Demo/FCLDemo/FCLDemo/Assets.xcassets/icon20Selected.imageset/icon20Selected.pdf
--------------------------------------------------------------------------------
/Demo/FCLDemo/FCLDemo/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/Demo/FCLDemo/FCLDemo/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Demo/FCLDemo/FCLDemo/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleURLTypes
6 |
7 |
8 | CFBundleTypeRole
9 | Editor
10 | CFBundleURLSchemes
11 |
12 | bloctod9fed043-5942-496e-8595-57ffe45b759c
13 |
14 |
15 |
16 | LSApplicationQueriesSchemes
17 |
18 | blocto-dev
19 | blocto
20 |
21 | UIApplicationSceneManifest
22 |
23 | UIApplicationSupportsMultipleScenes
24 |
25 | UISceneConfigurations
26 |
27 | UIWindowSceneSessionRoleApplication
28 |
29 |
30 | UISceneConfigurationName
31 | Default Configuration
32 | UISceneDelegateClassName
33 | $(PRODUCT_MODULE_NAME).SceneDelegate
34 | UISceneStoryboardFile
35 | Main
36 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/Demo/FCLDemo/FCLDemo/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | // FCLDemo
4 | //
5 | // Created by Andrew Wang on 2022/6/29.
6 | //
7 |
8 | import UIKit
9 | import FCL_SDK
10 |
11 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
12 |
13 | var window: UIWindow?
14 |
15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
16 | if let url = connectionOptions.userActivities.first?.webpageURL {
17 | fcl.application(open: url)
18 | }
19 | guard let windowScene = (scene as? UIWindowScene) else { return }
20 | let window = UIWindow(windowScene: windowScene)
21 | window.rootViewController = ViewController()
22 | self.window = window
23 | window.makeKeyAndVisible()
24 | }
25 |
26 | func sceneDidDisconnect(_ scene: UIScene) {
27 | // Called as the scene is being released by the system.
28 | // This occurs shortly after the scene enters the background, or when its session is discarded.
29 | // Release any resources associated with this scene that can be re-created the next time the scene connects.
30 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
31 | }
32 |
33 | func sceneDidBecomeActive(_ scene: UIScene) {
34 | // Called when the scene has moved from an inactive state to an active state.
35 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
36 | }
37 |
38 | func sceneWillResignActive(_ scene: UIScene) {
39 | // Called when the scene will move from an active state to an inactive state.
40 | // This may occur due to temporary interruptions (ex. an incoming phone call).
41 | }
42 |
43 | func sceneWillEnterForeground(_ scene: UIScene) {
44 | // Called as the scene transitions from the background to the foreground.
45 | // Use this method to undo the changes made on entering the background.
46 | }
47 |
48 | func sceneDidEnterBackground(_ scene: UIScene) {
49 | // Called as the scene transitions from the foreground to the background.
50 | // Use this method to save data, release shared resources, and store enough scene-specific state information
51 | // to restore the scene back to its current state.
52 | }
53 |
54 | func scene(_ scene: UIScene, openURLContexts URLContexts: Set) {
55 | for context in URLContexts {
56 | fcl.application(open: context.url)
57 | }
58 | }
59 |
60 | func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
61 | fcl.continueForLinks(userActivity)
62 | }
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/Demo/FCL_Cocoa_Demo/FCL_Cocoa_Demo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Demo/FCL_Cocoa_Demo/FCL_Cocoa_Demo.xcodeproj/xcshareddata/xcschemes/FCL_Cocoa_Demo.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
54 |
60 |
62 |
68 |
69 |
70 |
71 |
73 |
74 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/Demo/FCL_Cocoa_Demo/FCL_Cocoa_Demo.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Demo/FCL_Cocoa_Demo/FCL_Cocoa_Demo.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Demo/FCL_Cocoa_Demo/FCL_Cocoa_Demo/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // FCL_Cocoa_Demo
4 | //
5 | // Created by Andrew Wang on 2022/7/7.
6 | //
7 |
8 | import UIKit
9 | import BloctoSDK
10 |
11 | @main
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 |
14 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
15 | // Override point for customization after application launch.
16 | true
17 | }
18 |
19 | // MARK: UISceneSession Lifecycle
20 |
21 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
22 | // Called when a new scene session is being created.
23 | // Use this method to select a configuration to create the new scene with.
24 | UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
25 | }
26 |
27 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) {
28 | // Called when the user discards a scene session.
29 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
30 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/Demo/FCL_Cocoa_Demo/FCL_Cocoa_Demo/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Demo/FCL_Cocoa_Demo/FCL_Cocoa_Demo/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "scale" : "1x",
46 | "size" : "20x20"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "scale" : "2x",
51 | "size" : "20x20"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "scale" : "1x",
56 | "size" : "29x29"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "29x29"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "scale" : "1x",
66 | "size" : "40x40"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "scale" : "2x",
71 | "size" : "40x40"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "scale" : "1x",
76 | "size" : "76x76"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "scale" : "2x",
81 | "size" : "76x76"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "scale" : "2x",
86 | "size" : "83.5x83.5"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "scale" : "1x",
91 | "size" : "1024x1024"
92 | }
93 | ],
94 | "info" : {
95 | "author" : "xcode",
96 | "version" : 1
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Demo/FCL_Cocoa_Demo/FCL_Cocoa_Demo/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Demo/FCL_Cocoa_Demo/FCL_Cocoa_Demo/Assets.xcassets/error.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "error-cutout.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Demo/FCL_Cocoa_Demo/FCL_Cocoa_Demo/Assets.xcassets/error.imageset/error-cutout.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocto/fcl-swift/3a90dcd96910ef9ad405e1e96b2447b3ca3759b5/Demo/FCL_Cocoa_Demo/FCL_Cocoa_Demo/Assets.xcassets/error.imageset/error-cutout.png
--------------------------------------------------------------------------------
/Demo/FCL_Cocoa_Demo/FCL_Cocoa_Demo/Assets.xcassets/ic28Copy.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "ic28Copy.pdf",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Demo/FCL_Cocoa_Demo/FCL_Cocoa_Demo/Assets.xcassets/ic28Copy.imageset/ic28Copy.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocto/fcl-swift/3a90dcd96910ef9ad405e1e96b2447b3ca3759b5/Demo/FCL_Cocoa_Demo/FCL_Cocoa_Demo/Assets.xcassets/ic28Copy.imageset/ic28Copy.pdf
--------------------------------------------------------------------------------
/Demo/FCL_Cocoa_Demo/FCL_Cocoa_Demo/Assets.xcassets/ic28Earth.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "ic28Earth.pdf",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Demo/FCL_Cocoa_Demo/FCL_Cocoa_Demo/Assets.xcassets/ic28Earth.imageset/ic28Earth.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocto/fcl-swift/3a90dcd96910ef9ad405e1e96b2447b3ca3759b5/Demo/FCL_Cocoa_Demo/FCL_Cocoa_Demo/Assets.xcassets/ic28Earth.imageset/ic28Earth.pdf
--------------------------------------------------------------------------------
/Demo/FCL_Cocoa_Demo/FCL_Cocoa_Demo/Assets.xcassets/icExamination.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "sh-process-audit-process-processing-icon-with-png-and-vector-516865-cutout.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Demo/FCL_Cocoa_Demo/FCL_Cocoa_Demo/Assets.xcassets/icExamination.imageset/sh-process-audit-process-processing-icon-with-png-and-vector-516865-cutout.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocto/fcl-swift/3a90dcd96910ef9ad405e1e96b2447b3ca3759b5/Demo/FCL_Cocoa_Demo/FCL_Cocoa_Demo/Assets.xcassets/icExamination.imageset/sh-process-audit-process-processing-icon-with-png-and-vector-516865-cutout.png
--------------------------------------------------------------------------------
/Demo/FCL_Cocoa_Demo/FCL_Cocoa_Demo/Assets.xcassets/icon20Selected.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "icon20Selected.pdf",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Demo/FCL_Cocoa_Demo/FCL_Cocoa_Demo/Assets.xcassets/icon20Selected.imageset/icon20Selected.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocto/fcl-swift/3a90dcd96910ef9ad405e1e96b2447b3ca3759b5/Demo/FCL_Cocoa_Demo/FCL_Cocoa_Demo/Assets.xcassets/icon20Selected.imageset/icon20Selected.pdf
--------------------------------------------------------------------------------
/Demo/FCL_Cocoa_Demo/FCL_Cocoa_Demo/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/Demo/FCL_Cocoa_Demo/FCL_Cocoa_Demo/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Demo/FCL_Cocoa_Demo/FCL_Cocoa_Demo/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleURLTypes
6 |
7 |
8 | CFBundleTypeRole
9 | Editor
10 | CFBundleURLSchemes
11 |
12 | bloctod9fed043-5942-496e-8595-57ffe45b759c
13 |
14 |
15 |
16 | LSApplicationQueriesSchemes
17 |
18 | blocto-dev
19 | blocto
20 |
21 | UIApplicationSceneManifest
22 |
23 | UIApplicationSupportsMultipleScenes
24 |
25 | UISceneConfigurations
26 |
27 | UIWindowSceneSessionRoleApplication
28 |
29 |
30 | UISceneConfigurationName
31 | Default Configuration
32 | UISceneDelegateClassName
33 | $(PRODUCT_MODULE_NAME).SceneDelegate
34 | UISceneStoryboardFile
35 | Main
36 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/Demo/FCL_Cocoa_Demo/FCL_Cocoa_Demo/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | // FCL_Cocoa_Demo
4 | //
5 | // Created by Andrew Wang on 2022/7/7.
6 | //
7 |
8 | import UIKit
9 | import FCL_SDK
10 |
11 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
12 |
13 | var window: UIWindow?
14 |
15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
16 | if let url = connectionOptions.userActivities.first?.webpageURL {
17 | fcl.application(open: url)
18 | }
19 | guard let windowScene = (scene as? UIWindowScene) else { return }
20 | window = UIWindow(windowScene: windowScene)
21 | window?.rootViewController = FlowDemoViewController()
22 | window?.makeKeyAndVisible()
23 | }
24 |
25 | func scene(_ scene: UIScene, openURLContexts URLContexts: Set) {
26 | for context in URLContexts {
27 | fcl.application(open: context.url)
28 | }
29 | }
30 |
31 | func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
32 | fcl.continueForLinks(userActivity)
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/Demo/FCL_Cocoa_Demo/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source "https://rubygems.org"
4 |
5 | gem 'cocoapods'
6 |
--------------------------------------------------------------------------------
/Demo/FCL_Cocoa_Demo/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | CFPropertyList (3.0.5)
5 | rexml
6 | activesupport (6.1.6.1)
7 | concurrent-ruby (~> 1.0, >= 1.0.2)
8 | i18n (>= 1.6, < 2)
9 | minitest (>= 5.1)
10 | tzinfo (~> 2.0)
11 | zeitwerk (~> 2.3)
12 | addressable (2.8.0)
13 | public_suffix (>= 2.0.2, < 5.0)
14 | algoliasearch (1.27.5)
15 | httpclient (~> 2.8, >= 2.8.3)
16 | json (>= 1.5.1)
17 | atomos (0.1.3)
18 | claide (1.1.0)
19 | cocoapods (1.11.3)
20 | addressable (~> 2.8)
21 | claide (>= 1.0.2, < 2.0)
22 | cocoapods-core (= 1.11.3)
23 | cocoapods-deintegrate (>= 1.0.3, < 2.0)
24 | cocoapods-downloader (>= 1.4.0, < 2.0)
25 | cocoapods-plugins (>= 1.0.0, < 2.0)
26 | cocoapods-search (>= 1.0.0, < 2.0)
27 | cocoapods-trunk (>= 1.4.0, < 2.0)
28 | cocoapods-try (>= 1.1.0, < 2.0)
29 | colored2 (~> 3.1)
30 | escape (~> 0.0.4)
31 | fourflusher (>= 2.3.0, < 3.0)
32 | gh_inspector (~> 1.0)
33 | molinillo (~> 0.8.0)
34 | nap (~> 1.0)
35 | ruby-macho (>= 1.0, < 3.0)
36 | xcodeproj (>= 1.21.0, < 2.0)
37 | cocoapods-core (1.11.3)
38 | activesupport (>= 5.0, < 7)
39 | addressable (~> 2.8)
40 | algoliasearch (~> 1.0)
41 | concurrent-ruby (~> 1.1)
42 | fuzzy_match (~> 2.0.4)
43 | nap (~> 1.0)
44 | netrc (~> 0.11)
45 | public_suffix (~> 4.0)
46 | typhoeus (~> 1.0)
47 | cocoapods-deintegrate (1.0.5)
48 | cocoapods-downloader (1.6.3)
49 | cocoapods-plugins (1.0.0)
50 | nap
51 | cocoapods-search (1.0.1)
52 | cocoapods-trunk (1.6.0)
53 | nap (>= 0.8, < 2.0)
54 | netrc (~> 0.11)
55 | cocoapods-try (1.2.0)
56 | colored2 (3.1.2)
57 | concurrent-ruby (1.1.10)
58 | escape (0.0.4)
59 | ethon (0.15.0)
60 | ffi (>= 1.15.0)
61 | ffi (1.15.5)
62 | fourflusher (2.3.1)
63 | fuzzy_match (2.0.4)
64 | gh_inspector (1.1.3)
65 | httpclient (2.8.3)
66 | i18n (1.12.0)
67 | concurrent-ruby (~> 1.0)
68 | json (2.6.2)
69 | minitest (5.16.2)
70 | molinillo (0.8.0)
71 | nanaimo (0.3.0)
72 | nap (1.1.0)
73 | netrc (0.11.0)
74 | public_suffix (4.0.7)
75 | rexml (3.2.5)
76 | ruby-macho (2.5.1)
77 | typhoeus (1.4.0)
78 | ethon (>= 0.9.0)
79 | tzinfo (2.0.5)
80 | concurrent-ruby (~> 1.0)
81 | xcodeproj (1.22.0)
82 | CFPropertyList (>= 2.3.3, < 4.0)
83 | atomos (~> 0.1.3)
84 | claide (>= 1.0.2, < 2.0)
85 | colored2 (~> 3.1)
86 | nanaimo (~> 0.3.0)
87 | rexml (~> 3.2.4)
88 | zeitwerk (2.6.0)
89 |
90 | PLATFORMS
91 | -darwin-20
92 | arm64-darwin-21
93 |
94 | DEPENDENCIES
95 | cocoapods
96 |
97 | BUNDLED WITH
98 | 2.2.16
99 |
--------------------------------------------------------------------------------
/Demo/FCL_Cocoa_Demo/Podfile:
--------------------------------------------------------------------------------
1 | platform :ios, '13.0'
2 |
3 | target 'FCL_Cocoa_Demo' do
4 | use_frameworks!
5 |
6 | pod 'FCL-SDK', :path => '../../'
7 | pod 'SnapKit'
8 | pod 'RxSwift'
9 | pod 'RxCocoa'
10 |
11 | end
12 |
--------------------------------------------------------------------------------
/Demo/FCL_Cocoa_Demo/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - _NIODataStructures (2.40.0)
3 | - BigInt (5.2.0)
4 | - BloctoSDK/Core (0.6.1)
5 | - BloctoSDK/Flow (0.6.1):
6 | - BloctoSDK/Core (~> 0.6.1)
7 | - FlowSDK (~> 0.5.0)
8 | - Cadence (0.5.0):
9 | - BigInt (~> 5.2.0)
10 | - CryptoSwift (~> 1.5.1)
11 | - CGRPCZlibp (1.8.2)
12 | - CNIOAtomics (2.40.0)
13 | - CNIOBoringSSL (2.19.0)
14 | - CNIOBoringSSLShims (2.19.0):
15 | - CNIOBoringSSL (= 2.19.0)
16 | - CNIODarwin (2.40.0)
17 | - CNIOHTTPParser (2.40.0)
18 | - CNIOLinux (2.40.0)
19 | - CNIOWindows (2.40.0)
20 | - CryptoSwift (1.5.1)
21 | - FCL-SDK (0.4.1):
22 | - BloctoSDK/Flow (~> 0.6.1)
23 | - SwiftyJSON
24 | - FlowSDK (0.5.0):
25 | - FlowSDK/FlowSDK (= 0.5.0)
26 | - FlowSDK/FlowSDK (0.5.0):
27 | - BigInt (~> 5.2.0)
28 | - Cadence (~> 0.5.0)
29 | - CryptoSwift (~> 1.5.1)
30 | - gRPC-Swiftp (~> 1.8.2)
31 | - secp256k1Swift (~> 0.7.4)
32 | - gRPC-Swiftp (1.8.2):
33 | - CGRPCZlibp (= 1.8.2)
34 | - Logging (< 2.0.0, >= 1.4.0)
35 | - SwiftNIO (< 3.0.0, >= 2.32.0)
36 | - SwiftNIOExtras (< 2.0.0, >= 1.4.0)
37 | - SwiftNIOHTTP2 (< 2.0.0, >= 1.18.2)
38 | - SwiftNIOSSL (< 3.0.0, >= 2.14.0)
39 | - SwiftNIOTransportServices (< 2.0.0, >= 1.11.1)
40 | - SwiftProtobuf (< 2.0.0, >= 1.9.0)
41 | - Logging (1.4.0)
42 | - RxCocoa (6.5.0):
43 | - RxRelay (= 6.5.0)
44 | - RxSwift (= 6.5.0)
45 | - RxRelay (6.5.0):
46 | - RxSwift (= 6.5.0)
47 | - RxSwift (6.5.0)
48 | - secp256k1Swift (0.7.4):
49 | - secp256k1Wrapper (~> 0.0.5)
50 | - secp256k1Wrapper (0.0.5)
51 | - SnapKit (5.6.0)
52 | - SwiftNIO (2.40.0):
53 | - _NIODataStructures (= 2.40.0)
54 | - CNIOAtomics (= 2.40.0)
55 | - CNIODarwin (= 2.40.0)
56 | - CNIOLinux (= 2.40.0)
57 | - CNIOWindows (= 2.40.0)
58 | - SwiftNIOConcurrencyHelpers (= 2.40.0)
59 | - SwiftNIOCore (= 2.40.0)
60 | - SwiftNIOEmbedded (= 2.40.0)
61 | - SwiftNIOPosix (= 2.40.0)
62 | - SwiftNIOConcurrencyHelpers (2.40.0):
63 | - CNIOAtomics (= 2.40.0)
64 | - SwiftNIOCore (2.40.0):
65 | - CNIOAtomics (= 2.40.0)
66 | - CNIOLinux (= 2.40.0)
67 | - SwiftNIOConcurrencyHelpers (= 2.40.0)
68 | - SwiftNIOEmbedded (2.40.0):
69 | - _NIODataStructures (= 2.40.0)
70 | - CNIOAtomics (= 2.40.0)
71 | - CNIOLinux (= 2.40.0)
72 | - SwiftNIOConcurrencyHelpers (= 2.40.0)
73 | - SwiftNIOCore (= 2.40.0)
74 | - SwiftNIOExtras (1.11.0):
75 | - _NIODataStructures (< 3, >= 2.32.0)
76 | - CNIOAtomics (< 3, >= 2.32.0)
77 | - CNIODarwin (< 3, >= 2.32.0)
78 | - CNIOLinux (< 3, >= 2.32.0)
79 | - CNIOWindows (< 3, >= 2.32.0)
80 | - SwiftNIO (< 3, >= 2.32.0)
81 | - SwiftNIOConcurrencyHelpers (< 3, >= 2.32.0)
82 | - SwiftNIOCore (< 3, >= 2.32.0)
83 | - SwiftNIOEmbedded (< 3, >= 2.32.0)
84 | - SwiftNIOPosix (< 3, >= 2.32.0)
85 | - SwiftNIOFoundationCompat (2.40.0):
86 | - _NIODataStructures (= 2.40.0)
87 | - CNIOAtomics (= 2.40.0)
88 | - CNIODarwin (= 2.40.0)
89 | - CNIOLinux (= 2.40.0)
90 | - CNIOWindows (= 2.40.0)
91 | - SwiftNIO (= 2.40.0)
92 | - SwiftNIOConcurrencyHelpers (= 2.40.0)
93 | - SwiftNIOCore (= 2.40.0)
94 | - SwiftNIOEmbedded (= 2.40.0)
95 | - SwiftNIOPosix (= 2.40.0)
96 | - SwiftNIOHPACK (1.22.0):
97 | - _NIODataStructures (< 3, >= 2.35.0)
98 | - CNIOAtomics (< 3, >= 2.35.0)
99 | - CNIODarwin (< 3, >= 2.35.0)
100 | - CNIOHTTPParser (< 3, >= 2.35.0)
101 | - CNIOLinux (< 3, >= 2.35.0)
102 | - CNIOWindows (< 3, >= 2.35.0)
103 | - SwiftNIO (< 3, >= 2.35.0)
104 | - SwiftNIOConcurrencyHelpers (< 3, >= 2.35.0)
105 | - SwiftNIOCore (< 3, >= 2.35.0)
106 | - SwiftNIOEmbedded (< 3, >= 2.35.0)
107 | - SwiftNIOHTTP1 (< 3, >= 2.35.0)
108 | - SwiftNIOPosix (< 3, >= 2.35.0)
109 | - SwiftNIOHTTP1 (2.40.0):
110 | - _NIODataStructures (= 2.40.0)
111 | - CNIOAtomics (= 2.40.0)
112 | - CNIODarwin (= 2.40.0)
113 | - CNIOHTTPParser (= 2.40.0)
114 | - CNIOLinux (= 2.40.0)
115 | - CNIOWindows (= 2.40.0)
116 | - SwiftNIO (= 2.40.0)
117 | - SwiftNIOConcurrencyHelpers (= 2.40.0)
118 | - SwiftNIOCore (= 2.40.0)
119 | - SwiftNIOEmbedded (= 2.40.0)
120 | - SwiftNIOPosix (= 2.40.0)
121 | - SwiftNIOHTTP2 (1.22.0):
122 | - _NIODataStructures (< 3, >= 2.35.0)
123 | - CNIOAtomics (< 3, >= 2.35.0)
124 | - CNIODarwin (< 3, >= 2.35.0)
125 | - CNIOHTTPParser (< 3, >= 2.35.0)
126 | - CNIOLinux (< 3, >= 2.35.0)
127 | - CNIOWindows (< 3, >= 2.35.0)
128 | - SwiftNIO (< 3, >= 2.35.0)
129 | - SwiftNIOConcurrencyHelpers (< 3, >= 2.35.0)
130 | - SwiftNIOCore (< 3, >= 2.35.0)
131 | - SwiftNIOEmbedded (< 3, >= 2.35.0)
132 | - SwiftNIOHPACK (= 1.22.0)
133 | - SwiftNIOHTTP1 (< 3, >= 2.35.0)
134 | - SwiftNIOPosix (< 3, >= 2.35.0)
135 | - SwiftNIOTLS (< 3, >= 2.35.0)
136 | - SwiftNIOPosix (2.40.0):
137 | - _NIODataStructures (= 2.40.0)
138 | - CNIOAtomics (= 2.40.0)
139 | - CNIODarwin (= 2.40.0)
140 | - CNIOLinux (= 2.40.0)
141 | - CNIOWindows (= 2.40.0)
142 | - SwiftNIOConcurrencyHelpers (= 2.40.0)
143 | - SwiftNIOCore (= 2.40.0)
144 | - SwiftNIOSSL (2.19.0):
145 | - _NIODataStructures (< 3, >= 2.32.0)
146 | - CNIOAtomics (< 3, >= 2.32.0)
147 | - CNIOBoringSSL (= 2.19.0)
148 | - CNIOBoringSSLShims (= 2.19.0)
149 | - CNIODarwin (< 3, >= 2.32.0)
150 | - CNIOLinux (< 3, >= 2.32.0)
151 | - CNIOWindows (< 3, >= 2.32.0)
152 | - SwiftNIO (< 3, >= 2.32.0)
153 | - SwiftNIOConcurrencyHelpers (< 3, >= 2.32.0)
154 | - SwiftNIOCore (< 3, >= 2.32.0)
155 | - SwiftNIOEmbedded (< 3, >= 2.32.0)
156 | - SwiftNIOPosix (< 3, >= 2.32.0)
157 | - SwiftNIOTLS (< 3, >= 2.32.0)
158 | - SwiftNIOTLS (2.40.0):
159 | - _NIODataStructures (= 2.40.0)
160 | - CNIOAtomics (= 2.40.0)
161 | - CNIODarwin (= 2.40.0)
162 | - CNIOLinux (= 2.40.0)
163 | - CNIOWindows (= 2.40.0)
164 | - SwiftNIO (= 2.40.0)
165 | - SwiftNIOConcurrencyHelpers (= 2.40.0)
166 | - SwiftNIOCore (= 2.40.0)
167 | - SwiftNIOEmbedded (= 2.40.0)
168 | - SwiftNIOPosix (= 2.40.0)
169 | - SwiftNIOTransportServices (1.12.0):
170 | - _NIODataStructures (< 3, >= 2.32.0)
171 | - CNIOAtomics (< 3, >= 2.32.0)
172 | - CNIODarwin (< 3, >= 2.32.0)
173 | - CNIOLinux (< 3, >= 2.32.0)
174 | - CNIOWindows (< 3, >= 2.32.0)
175 | - SwiftNIO (< 3, >= 2.32.0)
176 | - SwiftNIOConcurrencyHelpers (< 3, >= 2.32.0)
177 | - SwiftNIOCore (< 3, >= 2.32.0)
178 | - SwiftNIOEmbedded (< 3, >= 2.32.0)
179 | - SwiftNIOFoundationCompat (< 3, >= 2.32.0)
180 | - SwiftNIOPosix (< 3, >= 2.32.0)
181 | - SwiftNIOTLS (< 3, >= 2.32.0)
182 | - SwiftProtobuf (1.21.0)
183 | - SwiftyJSON (5.0.1)
184 |
185 | DEPENDENCIES:
186 | - FCL-SDK (from `../../`)
187 | - RxCocoa
188 | - RxSwift
189 | - SnapKit
190 |
191 | SPEC REPOS:
192 | trunk:
193 | - _NIODataStructures
194 | - BigInt
195 | - BloctoSDK
196 | - Cadence
197 | - CGRPCZlibp
198 | - CNIOAtomics
199 | - CNIOBoringSSL
200 | - CNIOBoringSSLShims
201 | - CNIODarwin
202 | - CNIOHTTPParser
203 | - CNIOLinux
204 | - CNIOWindows
205 | - CryptoSwift
206 | - FlowSDK
207 | - gRPC-Swiftp
208 | - Logging
209 | - RxCocoa
210 | - RxRelay
211 | - RxSwift
212 | - secp256k1Swift
213 | - secp256k1Wrapper
214 | - SnapKit
215 | - SwiftNIO
216 | - SwiftNIOConcurrencyHelpers
217 | - SwiftNIOCore
218 | - SwiftNIOEmbedded
219 | - SwiftNIOExtras
220 | - SwiftNIOFoundationCompat
221 | - SwiftNIOHPACK
222 | - SwiftNIOHTTP1
223 | - SwiftNIOHTTP2
224 | - SwiftNIOPosix
225 | - SwiftNIOSSL
226 | - SwiftNIOTLS
227 | - SwiftNIOTransportServices
228 | - SwiftProtobuf
229 | - SwiftyJSON
230 |
231 | EXTERNAL SOURCES:
232 | FCL-SDK:
233 | :path: "../../"
234 |
235 | SPEC CHECKSUMS:
236 | _NIODataStructures: 3d45d8e70a1d17a15b1dc59d102c63dbc0525ffd
237 | BigInt: f668a80089607f521586bbe29513d708491ef2f7
238 | BloctoSDK: e56a1fb8d45c5a5909e843cf180c5626077d28fb
239 | Cadence: f354a678487ab17716acd61ddbb637130e9642b8
240 | CGRPCZlibp: 2f3e1e7a6d6cb481d4d1a26d3ec09aefacf09cbb
241 | CNIOAtomics: 8edf08644e5e6fa0f021c239be9e8beb1cd9ef18
242 | CNIOBoringSSL: 2c9c96c2e95f15e83fb8d26b9738d939cc39ae33
243 | CNIOBoringSSLShims: c5c9346e7bbd1040f4f8793a35441dda7487539a
244 | CNIODarwin: 93850990d29f2626b05306c6c9309f9be0d74c2f
245 | CNIOHTTPParser: 8ce395236fa1d09ac3b4f4bcfba79b849b2ac684
246 | CNIOLinux: 62e3505f50de558c393dc2f273dde71dcce518da
247 | CNIOWindows: 3047f2d8165848a3936a0a755fee27c6b5ee479b
248 | CryptoSwift: c4f2debceb38bf44c80659afe009f71e23e4a082
249 | FCL-SDK: 109f99f6d7d37a87a32f623872ff3eb92102b04c
250 | FlowSDK: 08c8b36cdc7b1c0d7017504592e8d13042e71bd0
251 | gRPC-Swiftp: 1f5a05ce5b544bff3dce93223e72829daac26113
252 | Logging: beeb016c9c80cf77042d62e83495816847ef108b
253 | RxCocoa: 94f817b71c07517321eb4f9ad299112ca8af743b
254 | RxRelay: 1de1523e604c72b6c68feadedd1af3b1b4d0ecbd
255 | RxSwift: 5710a9e6b17f3c3d6e40d6e559b9fa1e813b2ef8
256 | secp256k1Swift: ea49d2b06724444a03cf7938a2d3fc7acc4c0f08
257 | secp256k1Wrapper: 0378417cd06d51187bbc9e178ec318e7902e2120
258 | SnapKit: e01d52ebb8ddbc333eefe2132acf85c8227d9c25
259 | SwiftNIO: 829958aab300642625091f82fc2f49cb7cf4ef24
260 | SwiftNIOConcurrencyHelpers: 697370136789b1074e4535eaae75cbd7f900370e
261 | SwiftNIOCore: 473fdfe746534d7aa25766916459eeaf6f92ef49
262 | SwiftNIOEmbedded: ffcb5147db67d9686c8366b7f8427b36132f2c8a
263 | SwiftNIOExtras: 481f74d6bf0b0ef699905ed66439cb019c4975c9
264 | SwiftNIOFoundationCompat: b9cdbea4806e4a12e9f66d9696fa3b98c4c3232b
265 | SwiftNIOHPACK: e7d3ff5bd671528adfb11cd4e0c84ddfdc3c4453
266 | SwiftNIOHTTP1: ef56706550a1dc135ea69d65215b9941e643c23b
267 | SwiftNIOHTTP2: cc81d7a6ba70d2ddc5376f471904b27ef5d2b7b8
268 | SwiftNIOPosix: b49af4bdbecaadfadd5c93dfe28594d6722b75e4
269 | SwiftNIOSSL: d153c5a6fc5b2301b0519b4c4d037a9414212da6
270 | SwiftNIOTLS: 598af547490133e9aac52aed0c23c4a90c31dcfc
271 | SwiftNIOTransportServices: 0b2b407819d82eb63af558c5396e33c945759503
272 | SwiftProtobuf: afced68785854575756db965e9da52bbf3dc45e7
273 | SwiftyJSON: 2f33a42c6fbc52764d96f13368585094bfd8aa5e
274 |
275 | PODFILE CHECKSUM: 84609972c6a1b04f5ffd7c04e38ae26066a44f09
276 |
277 | COCOAPODS: 1.11.3
278 |
--------------------------------------------------------------------------------
/Demo/FCL_SwiftUI_Demo/FCL-SwiftUI-Demo-Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleURLTypes
6 |
7 |
8 | CFBundleTypeRole
9 | Editor
10 | CFBundleURLSchemes
11 |
12 | bloctod9fed043-5942-496e-8595-57ffe45b759c
13 |
14 |
15 |
16 | LSApplicationQueriesSchemes
17 |
18 | blocto-dev
19 | blocto
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Demo/FCL_SwiftUI_Demo/FCL_SwiftUI_Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Demo/FCL_SwiftUI_Demo/FCL_SwiftUI_Demo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Demo/FCL_SwiftUI_Demo/FCL_SwiftUI_Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "alamofire",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/Alamofire/Alamofire.git",
7 | "state" : {
8 | "revision" : "8dd85aee02e39dd280c75eef88ffdb86eed4b07b",
9 | "version" : "5.6.2"
10 | }
11 | },
12 | {
13 | "identity" : "bigint",
14 | "kind" : "remoteSourceControl",
15 | "location" : "https://github.com/attaswift/BigInt.git",
16 | "state" : {
17 | "revision" : "0ed110f7555c34ff468e72e1686e59721f2b0da6",
18 | "version" : "5.3.0"
19 | }
20 | },
21 | {
22 | "identity" : "blocto-ios-sdk",
23 | "kind" : "remoteSourceControl",
24 | "location" : "https://github.com/portto/blocto-ios-sdk.git",
25 | "state" : {
26 | "revision" : "1cbecf62ad3655703137aa7141f12664af52142f",
27 | "version" : "0.6.1"
28 | }
29 | },
30 | {
31 | "identity" : "cryptoswift",
32 | "kind" : "remoteSourceControl",
33 | "location" : "https://github.com/krzyzanowskim/CryptoSwift.git",
34 | "state" : {
35 | "revision" : "039f56c5d7960f277087a0be51f5eb04ed0ec073",
36 | "version" : "1.5.1"
37 | }
38 | },
39 | {
40 | "identity" : "flow-swift-sdk",
41 | "kind" : "remoteSourceControl",
42 | "location" : "https://github.com/portto/flow-swift-sdk.git",
43 | "state" : {
44 | "revision" : "52448238d0e887af02fc0bdaf50ca76426317289",
45 | "version" : "0.5.0"
46 | }
47 | },
48 | {
49 | "identity" : "grpc-swift",
50 | "kind" : "remoteSourceControl",
51 | "location" : "https://github.com/grpc/grpc-swift.git",
52 | "state" : {
53 | "revision" : "d114c5ec34015bab663b3b7afaa7d6197656e47e",
54 | "version" : "1.9.0"
55 | }
56 | },
57 | {
58 | "identity" : "runtime",
59 | "kind" : "remoteSourceControl",
60 | "location" : "https://github.com/wickwirew/Runtime.git",
61 | "state" : {
62 | "revision" : "dad03135d7701a4e7b3a4051e75d6b37bd8e178e",
63 | "version" : "2.2.4"
64 | }
65 | },
66 | {
67 | "identity" : "secp256k1",
68 | "kind" : "remoteSourceControl",
69 | "location" : "https://github.com/portto/secp256k1.git",
70 | "state" : {
71 | "revision" : "6864a2560066cedede330c4b344689432a7300f7",
72 | "version" : "0.0.5"
73 | }
74 | },
75 | {
76 | "identity" : "secp256k1.swift",
77 | "kind" : "remoteSourceControl",
78 | "location" : "https://github.com/portto/secp256k1.swift",
79 | "state" : {
80 | "revision" : "23aa6bab1f60e513297d0d58a863418f68534e56",
81 | "version" : "0.7.4"
82 | }
83 | },
84 | {
85 | "identity" : "solana-web3.swift",
86 | "kind" : "remoteSourceControl",
87 | "location" : "https://github.com/portto/solana-web3.swift",
88 | "state" : {
89 | "revision" : "9f430df4ce564d900e41da37821d555b2e644b62",
90 | "version" : "0.0.4"
91 | }
92 | },
93 | {
94 | "identity" : "swift-atomics",
95 | "kind" : "remoteSourceControl",
96 | "location" : "https://github.com/apple/swift-atomics.git",
97 | "state" : {
98 | "revision" : "919eb1d83e02121cdb434c7bfc1f0c66ef17febe",
99 | "version" : "1.0.2"
100 | }
101 | },
102 | {
103 | "identity" : "swift-log",
104 | "kind" : "remoteSourceControl",
105 | "location" : "https://github.com/apple/swift-log.git",
106 | "state" : {
107 | "revision" : "6fe203dc33195667ce1759bf0182975e4653ba1c",
108 | "version" : "1.4.4"
109 | }
110 | },
111 | {
112 | "identity" : "swift-nio",
113 | "kind" : "remoteSourceControl",
114 | "location" : "https://github.com/apple/swift-nio.git",
115 | "state" : {
116 | "revision" : "b4e0a274f7f34210e97e2f2c50ab02a10b549250",
117 | "version" : "2.41.1"
118 | }
119 | },
120 | {
121 | "identity" : "swift-nio-extras",
122 | "kind" : "remoteSourceControl",
123 | "location" : "https://github.com/apple/swift-nio-extras.git",
124 | "state" : {
125 | "revision" : "5334d949febb396a4e2e5235e9fbcd9c3c014bb3",
126 | "version" : "1.13.0"
127 | }
128 | },
129 | {
130 | "identity" : "swift-nio-http2",
131 | "kind" : "remoteSourceControl",
132 | "location" : "https://github.com/apple/swift-nio-http2.git",
133 | "state" : {
134 | "revision" : "f9ab1c94c80d568efd762d2a638f25162691d766",
135 | "version" : "1.22.1"
136 | }
137 | },
138 | {
139 | "identity" : "swift-nio-ssl",
140 | "kind" : "remoteSourceControl",
141 | "location" : "https://github.com/apple/swift-nio-ssl.git",
142 | "state" : {
143 | "revision" : "a6f9a034e5903024c6bac4ce2c56b157688d844f",
144 | "version" : "2.22.0"
145 | }
146 | },
147 | {
148 | "identity" : "swift-nio-transport-services",
149 | "kind" : "remoteSourceControl",
150 | "location" : "https://github.com/apple/swift-nio-transport-services.git",
151 | "state" : {
152 | "revision" : "4e02d9cf35cabfb538c96613272fb027dd0c8692",
153 | "version" : "1.13.1"
154 | }
155 | },
156 | {
157 | "identity" : "swift-protobuf",
158 | "kind" : "remoteSourceControl",
159 | "location" : "https://github.com/apple/swift-protobuf.git",
160 | "state" : {
161 | "revision" : "b8230909dedc640294d7324d37f4c91ad3dcf177",
162 | "version" : "1.20.1"
163 | }
164 | },
165 | {
166 | "identity" : "swiftyjson",
167 | "kind" : "remoteSourceControl",
168 | "location" : "https://github.com/SwiftyJSON/SwiftyJSON.git",
169 | "state" : {
170 | "revision" : "2b6054efa051565954e1d2b9da831680026cd768",
171 | "version" : "4.3.0"
172 | }
173 | },
174 | {
175 | "identity" : "tweetnacl-swiftwrap",
176 | "kind" : "remoteSourceControl",
177 | "location" : "https://github.com/bitmark-inc/tweetnacl-swiftwrap.git",
178 | "state" : {
179 | "revision" : "f8fd111642bf2336b11ef9ea828510693106e954",
180 | "version" : "1.1.0"
181 | }
182 | }
183 | ],
184 | "version" : 2
185 | }
186 |
--------------------------------------------------------------------------------
/Demo/FCL_SwiftUI_Demo/FCL_SwiftUI_Demo/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Demo/FCL_SwiftUI_Demo/FCL_SwiftUI_Demo/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "scale" : "1x",
46 | "size" : "20x20"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "scale" : "2x",
51 | "size" : "20x20"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "scale" : "1x",
56 | "size" : "29x29"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "29x29"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "scale" : "1x",
66 | "size" : "40x40"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "scale" : "2x",
71 | "size" : "40x40"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "scale" : "1x",
76 | "size" : "76x76"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "scale" : "2x",
81 | "size" : "76x76"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "scale" : "2x",
86 | "size" : "83.5x83.5"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "scale" : "1x",
91 | "size" : "1024x1024"
92 | }
93 | ],
94 | "info" : {
95 | "author" : "xcode",
96 | "version" : 1
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Demo/FCL_SwiftUI_Demo/FCL_SwiftUI_Demo/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Demo/FCL_SwiftUI_Demo/FCL_SwiftUI_Demo/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // FCL_SwiftUI_Demo
4 | //
5 | // Created by Andrew Wang on 2022/9/6.
6 | //
7 |
8 | import SwiftUI
9 | import FlowSDK
10 | import SafariServices
11 |
12 | struct ContentView: View {
13 |
14 | @ObservedObject var viewModel = ViewModel()
15 |
16 | @State var showSafari = false
17 |
18 | var body: some View {
19 | NavigationView {
20 | Form {
21 | // Network selection
22 | if let errorMessage = viewModel.errorMessage {
23 | Section {
24 | Text(errorMessage).foregroundColor(.red)
25 | }
26 | }
27 |
28 | Section {
29 | VStack {
30 | Picker("network", selection: $viewModel.network) {
31 | Text("testnet").tag(Network.testnet)
32 | Text("mainnet-beta").tag(Network.mainnet)
33 | }.onChange(of: viewModel.network) { newValue in
34 | debugPrint(newValue)
35 | viewModel.updateNetwork()
36 | }
37 | .pickerStyle(SegmentedPickerStyle())
38 | }
39 | } header: {
40 | Label("network", systemImage: "network")
41 | }
42 |
43 | // Request account
44 | Section {
45 | Toggle("using account proof", isOn: $viewModel.usingAccountProof)
46 |
47 | Button {
48 | viewModel.authn(usingAccountProof: viewModel.usingAccountProof)
49 | } label: {
50 | Label("Request account", systemImage: "person.crop.circle")
51 | }
52 | viewModel.loginErrorMessage == nil
53 | ? Text(viewModel.address?.hexStringWithPrefix ?? "").foregroundColor(.black)
54 | : Text(viewModel.loginErrorMessage ?? "").foregroundColor(.red)
55 |
56 | // explorer
57 | if let address = viewModel.address {
58 | if let url = ExplorerURLType.address(address.hexStringWithPrefix).url(network: viewModel.network) {
59 | Button {
60 | showSafari = true
61 | } label: {
62 | Label("Look up with flowscan", systemImage: "magnifyingglass")
63 | }.sheet(isPresented: $showSafari) {
64 | SafariView(url: url)
65 | }
66 | } else {
67 | Text("url not found").foregroundColor(.red)
68 | }
69 | }
70 |
71 | if viewModel.usingAccountProof,
72 | viewModel.accountProof != nil {
73 | HStack(alignment: .center) {
74 | Button {
75 | viewModel.verifyAccountProof()
76 | } label: {
77 | Label("Verify account proof", systemImage: "person.fill.checkmark")
78 | }
79 |
80 | if let valid = viewModel.accountProofValid {
81 | valid
82 | ? Image(systemName: "checkmark.circle.fill")
83 | .renderingMode(.template)
84 | .foregroundColor(.blue)
85 | : Image(systemName: "xmark.circle.fill")
86 | .renderingMode(.template)
87 | .foregroundColor(.red)
88 | }
89 | }
90 | if let errorMessage = viewModel.verifyAccountProofErrorMessage {
91 | Text(errorMessage)
92 | }
93 | }
94 | } header: {
95 | Label("Account", systemImage: "person.crop.circle.badge.questionmark")
96 | }
97 |
98 | // Sign message
99 | Section {
100 | ZStack(alignment: .leading) {
101 | if viewModel.signingMessage.isEmpty {
102 | Text("input any message you want to sign.")
103 | .foregroundColor(Color.gray)
104 | .font(.system(.body))
105 | .padding(.all)
106 | }
107 | TextEditor(text: $viewModel.signingMessage)
108 | .foregroundColor(Color.gray)
109 | .font(.system(.body))
110 | .frame(height: 35)
111 | .cornerRadius(10)
112 | .overlay(textFieldBorder)
113 | .padding([.top, .bottom], 10)
114 | }
115 | Button {
116 | viewModel.signMessage(message: viewModel.signingMessage)
117 | } label: {
118 | Label("Sign message", systemImage: "pencil")
119 | }
120 | viewModel.signingErrorMessage == nil
121 | ? Text(viewModel.userSignatures.map(\.signature).joined(separator: "\n\n"))
122 | : Text(viewModel.signingErrorMessage ?? "").foregroundColor(.red)
123 | if !viewModel.userSignatures.isEmpty {
124 | HStack(alignment: .center) {
125 | Button {
126 | viewModel.verifySignature()
127 | } label: {
128 | Label("Verify signatures", systemImage: "mail.and.text.magnifyingglass")
129 | }
130 | if let valid = viewModel.signatureValid {
131 | valid
132 | ? Image(systemName: "checkmark.circle.fill")
133 | .renderingMode(.template)
134 | .foregroundColor(.blue)
135 | : Image(systemName: "xmark.circle.fill")
136 | .renderingMode(.template)
137 | .foregroundColor(.red)
138 | }
139 | }
140 | if let errorMessage = viewModel.verifySigningErrorMessage {
141 | Text(errorMessage).foregroundColor(.red)
142 | }
143 | }
144 | } header: {
145 | Label("Signing", systemImage: "pencil.and.outline")
146 | }
147 |
148 | // Blockchain interactions
149 | Section {
150 | Button {
151 | viewModel.getValue()
152 | } label: {
153 | Label("Get value", systemImage: "book")
154 | }
155 |
156 | viewModel.getValueErrorMessage == nil
157 | ? Text(viewModel.onChainValue?.description ?? "")
158 | : Text(viewModel.getValueErrorMessage ?? "").foregroundColor(.red)
159 |
160 | ZStack(alignment: .leading) {
161 | if viewModel.inputValue.isEmpty {
162 | Text("input any number.")
163 | .foregroundColor(Color.gray)
164 | .font(.system(.body))
165 | .padding(.all)
166 | }
167 | TextEditor(text: $viewModel.inputValue)
168 | .foregroundColor(Color.gray)
169 | .font(.system(.body))
170 | .frame(height: 35)
171 | .cornerRadius(10)
172 | .overlay(textFieldBorder)
173 | .padding([.top, .bottom], 10)
174 | .keyboardType(.decimalPad)
175 | }
176 | Button {
177 | viewModel.setValue(inputValue: viewModel.inputValue)
178 | } label: {
179 | Label("Send transaction", systemImage: "paperplane")
180 | }
181 |
182 | viewModel.setValueErrorMessage == nil
183 | ? Text(viewModel.txHash ?? "")
184 | : Text(viewModel.setValueErrorMessage ?? "").foregroundColor(.red)
185 |
186 | if let txHash = viewModel.txHash {
187 | Button {
188 | viewModel.lookup(txHash: txHash)
189 | } label: {
190 | Label("Look up transaction status", systemImage: "hourglass")
191 | }
192 |
193 | viewModel.transactionStatusErrorMessage == nil
194 | ? Text(viewModel.transactionStatus ?? "")
195 | : Text(viewModel.transactionStatusErrorMessage ?? "").foregroundColor(.red)
196 |
197 | if let url = ExplorerURLType.txHash(txHash).url(network: viewModel.network) {
198 | Button {
199 | showSafari = true
200 | } label: {
201 | Label("Look up with flowscan", systemImage: "magnifyingglass")
202 | }.sheet(isPresented: $showSafari) {
203 | SafariView(url: url)
204 | }
205 | } else {
206 | Text("url not found").foregroundColor(.red)
207 | }
208 | }
209 | } header: {
210 | Label("Blockchain interaction", systemImage: "paperplane.circle")
211 | }
212 | }
213 | .navigationTitle("FCL-Swift Demo")
214 | }
215 | }
216 |
217 | var textFieldBorder: some View {
218 | RoundedRectangle(cornerRadius: 10)
219 | .stroke(Color.gray, lineWidth: 1)
220 | }
221 |
222 | }
223 |
224 | struct SafariView: UIViewControllerRepresentable {
225 |
226 | let url: URL
227 |
228 | func makeUIViewController(context: UIViewControllerRepresentableContext) -> SFSafariViewController {
229 | SFSafariViewController(url: url)
230 | }
231 |
232 | func updateUIViewController(_ uiViewController: SFSafariViewController, context: UIViewControllerRepresentableContext) {}
233 |
234 | }
235 |
236 | struct ContentView_Previews: PreviewProvider {
237 | static var previews: some View {
238 | ContentView()
239 | }
240 | }
241 |
--------------------------------------------------------------------------------
/Demo/FCL_SwiftUI_Demo/FCL_SwiftUI_Demo/ExplorerURLType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExplorerURLType.swift
3 | // FCL_SwiftUI_Demo
4 | //
5 | // Created by Andrew Wang on 2022/9/7.
6 | //
7 |
8 | import Foundation
9 | import FlowSDK
10 |
11 | enum ExplorerURLType {
12 | case txHash(String)
13 | case address(String)
14 |
15 | func url(network: Network) -> URL? {
16 | switch self {
17 | case let .txHash(hash):
18 | return network == .mainnet
19 | ? URL(string: "https://flowscan.org/transaction/\(hash)")
20 | : URL(string: "https://testnet.flowscan.org/transaction/\(hash)")
21 | case let .address(address):
22 | return network == .mainnet
23 | ? URL(string: "https://flowscan.org/account/\(address)")
24 | : URL(string: "https://testnet.flowscan.org/account/\(address)")
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Demo/FCL_SwiftUI_Demo/FCL_SwiftUI_Demo/FCL_SwiftUI_DemoApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FCL_SwiftUI_DemoApp.swift
3 | // FCL_SwiftUI_Demo
4 | //
5 | // Created by Andrew Wang on 2022/9/6.
6 | //
7 |
8 | import SwiftUI
9 | import FCL_SDK
10 |
11 | @main
12 | struct FCL_SwiftUI_DemoApp: App {
13 | var body: some Scene {
14 | WindowGroup {
15 | ContentView()
16 | .onOpenURL { url in
17 | fcl.application(open: url)
18 | }
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Demo/FCL_SwiftUI_Demo/FCL_SwiftUI_Demo/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Demo/FCL_SwiftUI_Demo/FCL_SwiftUI_Demo/ViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewModel.swift
3 | // FCL_SwiftUI_Demo
4 | //
5 | // Created by Andrew Wang on 2022/9/6.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 | import FCL_SDK
11 | import FlowSDK
12 | import Cadence
13 | import BloctoSDK
14 |
15 | class ViewModel: ObservableObject {
16 |
17 | @Published var network: Network = .testnet
18 | @Published var errorMessage: String?
19 |
20 | @Published var address: Cadence.Address?
21 | @Published var loginErrorMessage: String?
22 | @Published var usingAccountProof = false
23 | @Published var accountProof: AccountProofSignatureData?
24 | @Published var accountProofValid: Bool?
25 | @Published var verifyAccountProofErrorMessage: String?
26 |
27 | @Published var signingMessage: String = ""
28 | @Published var userSignatures: [FCLCompositeSignature] = []
29 | @Published var signingErrorMessage: String?
30 | @Published var signatureValid: Bool?
31 | @Published var verifySigningErrorMessage: String?
32 |
33 | @Published var onChainValue: Decimal?
34 | @Published var getValueErrorMessage: String?
35 | @Published var inputValue: String = ""
36 | @Published var txHash: String?
37 | @Published var setValueErrorMessage: String?
38 | @Published var transactionStatus: String?
39 | @Published var transactionStatusErrorMessage: String?
40 |
41 | private var accountProofAppName = "This is demo app."
42 | // minimum 32-byte random nonce as a hex string.
43 | private var nonce = "75f8587e5bd5f9dcc9909d0dae1f0ac5814458b2ae129620502cb936fde7120a"
44 |
45 | var bloctoSDKAppId: String {
46 | switch network {
47 | case .mainnet:
48 | return "d9fed043-5942-496e-8595-57ffe45b759c"
49 | case .testnet:
50 | return "d9fed043-5942-496e-8595-57ffe45b759c"
51 | case .canarynet,
52 | .emulator,
53 | .sandboxnet:
54 | return ""
55 | }
56 | }
57 |
58 | private var bloctoContract: String {
59 | switch network {
60 | case .mainnet:
61 | return "0xdb6b70764af4ff68"
62 | case .testnet:
63 | return "0x5b250a8a85b44a67"
64 | case .canarynet,
65 | .emulator,
66 | .sandboxnet:
67 | return ""
68 | }
69 | }
70 |
71 | private var valueDappContract: String {
72 | switch network {
73 | case .mainnet:
74 | return "0x8320311d63f3b336"
75 | case .testnet:
76 | return "0x5a8143da8058740c"
77 | case .canarynet,
78 | .emulator,
79 | .sandboxnet:
80 | return ""
81 | }
82 | }
83 |
84 | init() {
85 | setupFCL()
86 | }
87 |
88 | func updateNetwork() {
89 | _ = try? fcl.config
90 | .put(.network(network))
91 | setupFCL()
92 | }
93 |
94 | func authn(usingAccountProof: Bool) {
95 | address = nil
96 | accountProof = nil
97 | loginErrorMessage = nil
98 | verifyAccountProofErrorMessage = nil
99 |
100 | if usingAccountProof {
101 | /// 1. Authanticate like FCL
102 | let accountProofData = FCLAccountProofData(
103 | appId: accountProofAppName,
104 | nonce: nonce
105 | )
106 | Task { @MainActor in
107 | do {
108 | address = try await fcl.authanticate(accountProofData: accountProofData)
109 | accountProof = fcl.currentUser?.accountProof
110 | } catch {
111 | loginErrorMessage = String(describing: error)
112 | }
113 | }
114 | } else {
115 | /// 2. request account only
116 | Task { @MainActor in
117 | do {
118 | address = try await fcl.login()
119 | } catch {
120 | loginErrorMessage = String(describing: error)
121 | }
122 | }
123 | }
124 | }
125 |
126 | func verifyAccountProof() {
127 | verifyAccountProofErrorMessage = nil
128 | accountProofValid = nil
129 |
130 | guard let accountProof = fcl.currentUser?.accountProof else {
131 | verifyAccountProofErrorMessage = "no account proof."
132 | return
133 | }
134 |
135 | Task { @MainActor in
136 | do {
137 | let valid = try await AppUtilities.verifyAccountProof(
138 | appIdentifier: accountProofAppName,
139 | accountProofData: accountProof,
140 | fclCryptoContract: Address(hexString: bloctoContract)
141 | )
142 | accountProofValid = valid
143 | } catch {
144 | verifyAccountProofErrorMessage = String(describing: error)
145 | debugPrint(error)
146 | }
147 | }
148 | }
149 |
150 | func signMessage(message: String) {
151 | userSignatures = []
152 | signingErrorMessage = nil
153 |
154 | guard fcl.currentUser?.address.hexStringWithPrefix != nil else {
155 | signingErrorMessage = "User address not found. Please request account first."
156 | return
157 | }
158 |
159 | Task { @MainActor in
160 | do {
161 | let signatures = try await fcl.signUserMessage(message: message)
162 | userSignatures = signatures
163 | } catch {
164 | signingErrorMessage = String(describing: error)
165 | }
166 | }
167 | }
168 |
169 | func verifySignature() {
170 | signatureValid = nil
171 | verifySigningErrorMessage = nil
172 |
173 | guard userSignatures.isEmpty == false else {
174 | verifySigningErrorMessage = "signature not found."
175 | return
176 | }
177 |
178 | guard signingMessage.isEmpty == false else {
179 | verifySigningErrorMessage = "message must provided to verify signatures."
180 | return
181 | }
182 |
183 | Task { @MainActor in
184 | do {
185 | let valid = try await AppUtilities.verifyUserSignatures(
186 | message: Data(signingMessage.utf8).bloctoSDK.hexString,
187 | signatures: userSignatures,
188 | fclCryptoContract: Address(hexString: bloctoContract)
189 | )
190 | signatureValid = valid
191 | } catch {
192 | verifySigningErrorMessage = String(describing: error)
193 | }
194 | }
195 | }
196 |
197 | func getValue() {
198 | onChainValue = nil
199 | getValueErrorMessage = nil
200 |
201 | let script = """
202 | import ValueDapp from \(valueDappContract)
203 |
204 | pub fun main(): UFix64 {
205 | return ValueDapp.value
206 | }
207 | """
208 |
209 | Task { @MainActor in
210 | do {
211 | let argument = try await fcl.query(script: script)
212 | onChainValue = try argument.value.toSwiftValue()
213 | } catch {
214 | getValueErrorMessage = String(describing: error)
215 | }
216 | }
217 | }
218 |
219 | func setValue(inputValue: String) {
220 | txHash = nil
221 | setValueErrorMessage = nil
222 |
223 | guard let userWalletAddress = fcl.currentUser?.address else {
224 | setValueErrorMessage = "User address not found. Please request account first."
225 | return
226 | }
227 |
228 | guard inputValue.isEmpty == false,
229 | let input = Decimal(string: inputValue) else {
230 | setValueErrorMessage = "Input not found."
231 | return
232 | }
233 |
234 | Task { @MainActor in
235 | do {
236 |
237 | let scriptString = """
238 | import ValueDapp from VALUE_DAPP_CONTRACT
239 |
240 | transaction(value: UFix64) {
241 | prepare(authorizer: AuthAccount) {
242 | ValueDapp.setValue(value)
243 | }
244 | }
245 | """
246 |
247 | let argument = Cadence.Argument(.ufix64(input))
248 |
249 | let txHash = try await fcl.mutate(
250 | cadence: scriptString,
251 | arguments: [argument],
252 | limit: 100,
253 | authorizers: [userWalletAddress]
254 | )
255 | self.txHash = txHash.hexString
256 | } catch {
257 | setValueErrorMessage = String(describing: error)
258 | }
259 | }
260 | }
261 |
262 | func lookup(txHash: String) {
263 | transactionStatus = nil
264 | transactionStatusErrorMessage = nil
265 |
266 | Task { @MainActor in
267 | do {
268 | let result = try await fcl.getTransactionStatus(transactionId: txHash)
269 | transactionStatus = "status: \(String(describing: result.status ?? .unknown))\nerror message: \(result.errorMessage ?? "no error")"
270 | } catch {
271 | transactionStatusErrorMessage = String(describing: error)
272 | }
273 | }
274 | }
275 |
276 | private func setupFCL() {
277 | do {
278 | let bloctoWalletProvider = try BloctoWalletProvider(
279 | bloctoAppIdentifier: bloctoSDKAppId,
280 | window: nil,
281 | network: network
282 | )
283 | let dapperWalletProvider = DapperWalletProvider.default
284 | try fcl.config
285 | .put(.network(network))
286 | .put(.supportedWalletProviders(
287 | [
288 | bloctoWalletProvider,
289 | dapperWalletProvider,
290 | ]
291 | ))
292 | .put(.replace(placeHolder: "VALUE_DAPP_CONTRACT", with: Address(hexString: valueDappContract)))
293 | } catch {
294 | errorMessage = String(describing: error)
295 | debugPrint(error)
296 | }
297 | }
298 | }
299 |
--------------------------------------------------------------------------------
/FCL-SDK.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |s|
2 | s.name = 'FCL-SDK'
3 | s.version = '0.4.1'
4 | s.summary = 'Flow Client Library Swift version.'
5 |
6 | s.homepage = 'https://github.com/portto/fcl-swift'
7 | s.license = { :type => 'MIT', :file => 'LICENSE' }
8 | s.author = { 'Dawson' => 'dawson@portto.com', 'Scott' => 'scott@portto.com' }
9 | s.source = { :git => 'https://github.com/portto/fcl-swift.git', :tag => s.version.to_s }
10 | s.social_media_url = 'https://twitter.com/BloctoApp'
11 |
12 | s.swift_version = '5.0.0'
13 | s.ios.deployment_target = '13.0'
14 |
15 | s.source_files = "Sources/**/*"
16 | s.dependency "BloctoSDK/Flow", "~> 0.6.1"
17 | s.dependency "SwiftyJSON"
18 |
19 | end
20 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2022 portto
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.6
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "FCL-SDK",
8 | platforms: [
9 | .iOS(.v13),
10 | ],
11 | products: [
12 | .library(
13 | name: "FCL_SDK",
14 | targets: ["FCL-SDK"]
15 | ),
16 | ],
17 | dependencies: [
18 | .package(url: "https://github.com/portto/flow-swift-sdk.git", .upToNextMajor(from: "0.5.0")),
19 | .package(url: "https://github.com/SwiftyJSON/SwiftyJSON.git", from: "4.0.0"),
20 | .package(url: "https://github.com/portto/blocto-ios-sdk.git", .upToNextMinor(from: "0.6.1")),
21 | ],
22 | targets: [
23 | .target(
24 | name: "FCL-SDK",
25 | dependencies: [
26 | "SwiftyJSON",
27 | .product(name: "FlowSDK", package: "flow-swift-sdk"),
28 | .product(name: "BloctoSDK", package: "blocto-ios-sdk"),
29 | ]
30 | ),
31 | .testTarget(
32 | name: "FCLTests",
33 | dependencies: ["FCL-SDK"]
34 | ),
35 | ]
36 | )
37 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | 
3 |
4 |
5 |
6 |
7 | ## What is FCL?
8 |
9 | The Flow Client Library (FCL) is used to interact with user wallets and the Flow blockchain. When using FCL for authentication, dApps are able to support all FCL-compatible wallets on Flow and their users without any custom integrations or changes needed to the dApp code.
10 |
11 | For more description, please refer to [fcl.js](https://github.com/onflow/fcl-js)
12 |
13 | This repo is inspired by [fcl-js](https://github.com/onflow/fcl-js) and [fcl-swift](https://github.com/Outblock/fcl-swift)
14 |
15 | ---
16 | ## Getting Started
17 |
18 | ### Requirements
19 | - Swift version >= 5.6
20 | - iOS version >= 13
21 |
22 | ## Installation
23 |
24 | ### CocoaPods
25 |
26 | FCL-SDK is available through [CocoaPods](https://cocoapods.org). You can include specific subspec to install, simply add the following line to your Podfile:
27 |
28 | ```ruby
29 | pod 'FCL-SDK', '~> 0.4.1'
30 | ```
31 |
32 | ### Swift Package Manager
33 |
34 |
35 | ```swift
36 | .package(url: "https://github.com/portto/fcl-swift.git", .upToNextMinor(from: "0.4.1"))
37 | ```
38 |
39 | Here's an example PackageDescription:
40 |
41 | ```swift
42 | // swift-tools-version: 5.6
43 | import PackageDescription
44 |
45 | let package = Package(
46 | name: "MyPackage",
47 | products: [
48 | .library(
49 | name: "MyPackage",
50 | targets: ["MyPackage"]
51 | ),
52 | ],
53 | dependencies: [
54 | .package(url: "https://github.com/portto/fcl-swift.git", .upToNextMinor(from: "0.4.1"))
55 | ],
56 | targets: [
57 | .target(
58 | name: "MyPackage",
59 | dependencies: [
60 | .product(name: "FCL_SDK", package: "fcl-swift"),
61 | ]
62 | )
63 | ]
64 | )
65 | ```
66 |
67 | ### Platform
68 |
69 | We only support iOS platform now. Please switch your XCode build target to iOS device.
70 |
71 |
72 | ### Importing
73 |
74 | ```swift
75 | import FCL_SDK
76 | ```
77 | ---
78 | ## FCL for dApps
79 | ### Configuration
80 |
81 | Initialize `WalletProvider` instance e.g. `BloctoWalletProvider`, `DapperWalletProvider`. And simply specify `network` and put those wallet providers into config option `supportedWalletProviders` then you are good to go.
82 |
83 | ```swift
84 | import FCL_SDK
85 |
86 | do {
87 | let bloctoWalletProvider = try BloctoWalletProvider(
88 | bloctoAppIdentifier: bloctoSDKAppId,
89 | window: nil,
90 | network: .testnet
91 | )
92 | fcl.config
93 | .put(.network(.testnet))
94 | .put(.supportedWalletProviders(
95 | [
96 | bloctoWalletProvider,
97 | ]
98 | ))
99 | } catch {
100 | // handle error
101 | }
102 |
103 | Task {
104 | try await fcl.login()
105 | }
106 | ```
107 |
108 | > **Note**: bloctoSDKAppId can be found in [Blocto Developer Dashboard](https://developers.blocto.app/), for detail instruction please refer to [Blocto Docs](https://docs.blocto.app/blocto-sdk/register-app-id)
109 |
110 | ### User Signatures
111 |
112 | Cryptographic signatures are a key part of the blockchain. They are used to prove ownership of an address without exposing its private key. While primarily used for signing transactions, cryptographic signatures can also be used to sign arbitrary messages.
113 |
114 | FCL has a feature that let you send arbitrary data to a configured wallet/service where the user may approve signing it with their private keys.
115 |
116 | We can retrieve user signatures only after user had logged in, otherwise error will be thrown.
117 |
118 | ```swift
119 | Task {
120 | do {
121 | let signatures: [FCLCompositeSignature] = try await fcl.signUserMessage(message: "message you want user to sign.")
122 | } catch {
123 | // handle error
124 | }
125 | }
126 | ```
127 |
128 | The message could be signed by several private key of the same wallet address. Those signatures will be valid all together as long as their corresponding key weight sum up at least 1000.
129 | For more info about multiple signatures, please refer to [Flow docs](https://developers.flow.com/learn/concepts/accounts-and-keys#single-party-multiple-signatures)
130 |
131 |
132 | ### Blockchain Interactions
133 | - *Query the chain*: Send arbitrary Cadence scripts to the chain and receive back decoded values
134 | ```swift
135 | import FCL_SDK
136 |
137 | let script = """
138 | import ValueDapp from \(valueDappContract)
139 |
140 | pub fun main(): UFix64 {
141 | return ValueDapp.value
142 | }
143 | """
144 |
145 | Task {
146 | let argument = try await fcl.query(script: script)
147 | label.text = argument.value.description
148 | }
149 | ```
150 | - *Mutate the chain*: Send arbitrary transactions with specify authorizer to perform state changes on chain.
151 | ```swift
152 | import FCL_SDK
153 |
154 | Task { @MainActor in
155 | guard let userWalletAddress = fcl.currentUser?.address else {
156 | // handle error
157 | return
158 | }
159 |
160 | let scriptString = """
161 | import ValueDapp from 0x5a8143da8058740c
162 |
163 | transaction(value: UFix64) {
164 | prepare(authorizer: AuthAccount) {
165 | ValueDapp.setValue(value)
166 | }
167 | }
168 | """
169 |
170 | let argument = Cadence.Argument(.ufix64(10))
171 |
172 | let txHsh = try await fcl.mutate(
173 | cadence: scriptString,
174 | arguments: [argument],
175 | limit: 100,
176 | authorizers: [userWalletAddress]
177 | )
178 | }
179 | ```
180 |
181 | [Learn more about on-chain interactions >](https://docs.onflow.org/fcl/reference/api/#on-chain-interactions)
182 |
183 | ---
184 | ## Prove ownership
185 | To prove ownership of a wallet address, there are two approaches.
186 | - Account proof: in the beginning of authentication, there are `accountProofData` you can provide for user to sign and return generated signatures along with account address.
187 |
188 | `fcl.authanticate` is also called behide `fcl.login()` with accountProofData set to nil.
189 |
190 | ```swift
191 | let accountProofData = FCLAccountProofData(
192 | appId: "Here you can specify your app name.",
193 | nonce: "75f8587e5bd5f9dcc9909d0dae1f0ac5814458b2ae129620502cb936fde7120a" // minimum 32-byte random nonce as a hex string.
194 | )
195 | let address = try await fcl.authanticate(accountProofData: accountProofData)
196 | ```
197 |
198 | - [User signature](#User-Signatures): provide specific message for user to sign and generate one or more signatures.
199 |
200 | ### Verifying User Signatures
201 |
202 | What makes message signatures more interesting is that we can use Flow blockchain to verify the signatures. Cadence has a built-in function called verify that will verify a signature against a Flow account given the account address.
203 |
204 | FCL includes a utility function, verifyUserSignatures, for verifying one or more signatures against an account's public key on the Flow blockchain.
205 |
206 | You can use both in tandem to prove a user is in control of a private key or keys. This enables cryptographically-secure login flow using a message-signing-based authentication mechanism with a user’s public address as their identifier.
207 |
208 | To verify above ownership, there are two utility functions define accordingly in [AppUtilities](https://github.com/portto/fcl-swift/blob/main/Sources/FCL-SDK/AppUtilities/AppUtilities.swift).
209 |
210 | ---
211 | ## Utilities
212 | - Get account details from any Flow address
213 | ```swift
214 | let account: Account? = try await fcl.flowAPIClient.getAccountAtLatestBlock(address: address)
215 | ```
216 | - Get the latest block
217 | ```swift
218 | let block: Block? = try await fcl.flowAPIClient.getLatestBlock(isSealed: true)
219 | ```
220 | - Transaction status polling
221 | ```swift
222 | let result = try await fcl.getTransactionStatus(transactionId: txHash)
223 | ```
224 |
225 | [Learn more about utilities >](https://docs.onflow.org/fcl/reference/api/#pre-built-interactions)
226 |
227 | ---
228 | ## FCL for Wallet Providers
229 | Wallet providers on Flow have the flexibility to build their user interactions and UI through a variety of ways:
230 | - Native app intercommunication via Universal links or custom schemes.
231 | - Back channel communication via HTTP polling with webpage button approving.
232 |
233 | FCL is agnostic to the communication channel and be configured to create both custodial and non-custodial wallets. This enables users to interact with wallet providers both native app install or not.
234 |
235 | Native app should be considered first to provide better user experience if installed, otherwise fallback to back channel communication.
236 |
237 | The communication channels involve responding to a set of pre-defined FCL messages to deliver the requested information to the dApp. Implementing a FCL compatible wallet on Flow is as simple as filling in the responses with the appropriate data when FCL requests them.
238 |
239 |
240 | ### Current Wallet Providers
241 | - [Blocto](https://blocto.portto.io/en/) (fully supported) [Docs](https://docs.blocto.app/blocto-sdk/ios-sdk/flow)
242 | - [Dapper Wallet](https://www.meetdapper.com/) (support only authn for now)
243 |
244 | ### Wallet Selection
245 | - dApps can display and support all FCL compatible wallets who conform to `WalletProvider`.
246 | - Users don't need to sign up for new wallets - they can carry over their existing one to any dApps that use FCL for authentication and authorization.
247 | - Wallet selection panel will be shown automatically when `login()` is being called only if there are more than one wallet provider in `supportedWalletProviders`.
248 |
249 |
250 | ```swift
251 | import FCL_SDK
252 |
253 | do {
254 | let bloctoWalletProvider = try BloctoWalletProvider(
255 | bloctoAppIdentifier: bloctoSDKAppId,
256 | window: nil,
257 | network: .testnet
258 | )
259 | let dapperWalletProvider = DapperWalletProvider.default
260 | fcl.config
261 | .put(.network(.testnet))
262 | .put(.supportedWalletProviders(
263 | [
264 | bloctoWalletProvider,
265 | dapperWalletProvider,
266 | ]
267 | ))
268 | } catch {
269 | // handle error
270 | }
271 |
272 | Task {
273 | try await fcl.login()
274 | }
275 | ```
276 |
277 | ### Building your own wallet provider
278 |
279 | - Declare a wallet provider type and conform the protocol [WalletProvider](./Sources/FCL-SDK/WalletProvider/WalletProvider.swift).
280 | - If building a wallet involve back channel communication, read the [wallet guide](https://github.com/onflow/fcl-js/blob/master/packages/fcl/src/wallet-provider-spec/draft-v3.md) first to build the concept of the implementation and use method from `WalletProvider` to fulfill your business logic.
281 |
282 | Every walllet provider can use below property from `WalletProvider` to customize icon, title and description. Those info will be shown [here](#wallet-selection).
283 | ```
284 | var providerInfo: ProviderInfo { get }
285 | ```
286 |
287 | ---
288 |
289 | ## Next Steps
290 |
291 | Learn Flow's smart contract language to build any script or transactions: [Cadence](https://docs.onflow.org/cadence/).
292 |
293 | Explore all of Flow [docs and tools](https://docs.onflow.org).
294 |
295 | ---
296 |
297 | ## Support
298 |
299 | Notice a problem or want to request a feature? [Add an issue](https://github.com/portto/fcl-swift/issues) or [Make a pull request](https://github.com/portto/fcl-swift/compare).
300 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/AppUtilities/AppUtilities.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppUtilities.swift
3 | //
4 | //
5 | // Created by Andrew Wang on 2022/7/11.
6 | //
7 |
8 | import Foundation
9 | import Cadence
10 | import BigInt
11 | import FlowSDK
12 |
13 | public enum AppUtilities {
14 |
15 | public static func verifyAccountProof(
16 | appIdentifier: String,
17 | accountProofData: AccountProofVerifiable,
18 | fclCryptoContract: Address?
19 | ) async throws -> Bool {
20 | let verifyMessage = WalletUtilities.encodeAccountProof(
21 | address: accountProofData.address,
22 | nonce: accountProofData.nonce,
23 | appIdentifier: appIdentifier,
24 | includeDomainTag: false
25 | )
26 |
27 | var indices: [Cadence.Argument] = []
28 | var siganature: [Cadence.Argument] = []
29 | for signature in accountProofData.signatures {
30 | indices.append(.int(BigInt(signature.keyId)))
31 | siganature.append(.string(signature.signature))
32 | }
33 |
34 | let arguments: [Cadence.Argument] = [
35 | .address(accountProofData.address),
36 | .string(verifyMessage),
37 | .array(indices),
38 | .array(siganature),
39 | ]
40 |
41 | let verifyScript = try getVerifySignaturesScript(
42 | isAccountProof: true,
43 | fclCryptoContract: fclCryptoContract
44 | )
45 | let result = try await fcl.query(
46 | script: verifyScript,
47 | arguments: arguments
48 | )
49 | guard case let .bool(valid) = result.value else {
50 | throw FCLError.unexpectedResult
51 | }
52 | return valid
53 | }
54 |
55 | public static func verifyUserSignatures(
56 | message: String,
57 | signatures: [CompositeSignatureVerifiable],
58 | fclCryptoContract: Address?
59 | ) async throws -> Bool {
60 |
61 | guard let address = signatures.first?.address else {
62 | throw FCLError.compositeSignatureInvalid
63 | }
64 |
65 | var indices: [Cadence.Argument] = []
66 | var siganature: [Cadence.Argument] = []
67 | for signature in signatures {
68 | indices.append(.int(BigInt(signature.keyId)))
69 | siganature.append(.string(signature.signature))
70 | }
71 |
72 | let arguments: [Cadence.Argument] = [
73 | .address(Address(hexString: address)),
74 | .string(message),
75 | .array(indices),
76 | .array(siganature),
77 | ]
78 |
79 | let verifyScript = try getVerifySignaturesScript(
80 | isAccountProof: false,
81 | fclCryptoContract: fclCryptoContract
82 | )
83 | let result = try await fcl.query(
84 | script: verifyScript,
85 | arguments: arguments
86 | )
87 | guard case let .bool(valid) = result.value else {
88 | throw FCLError.unexpectedResult
89 | }
90 | return valid
91 | }
92 |
93 | static func accountProofContractAddress(
94 | network: Network
95 | ) throws -> Address {
96 | switch network {
97 | case .mainnet:
98 | return Address(hexString: "0xb4b82a1c9d21d284")
99 | case .testnet:
100 | return Address(hexString: "0x74daa6f9c7ef24b1")
101 | case .canarynet,
102 | .sandboxnet,
103 | .emulator,
104 | .custom:
105 | throw FCLError.currentNetworkNotSupported
106 | }
107 | }
108 |
109 | static func getVerifySignaturesScript(
110 | isAccountProof: Bool,
111 | fclCryptoContract: Address?
112 | ) throws -> String {
113 | let contractAddress: Address
114 | if let fclCryptoContract = fclCryptoContract {
115 | contractAddress = fclCryptoContract
116 | } else {
117 | contractAddress = try accountProofContractAddress(network: fcl.config.network)
118 | }
119 |
120 | let verifyFunction = isAccountProof
121 | ? "verifyAccountProofSignatures"
122 | : "verifyUserSignatures"
123 |
124 | return """
125 | import FCLCrypto from \(contractAddress.hexStringWithPrefix)
126 |
127 | pub fun main(
128 | address: Address,
129 | message: String,
130 | keyIndices: [Int],
131 | signatures: [String]
132 | ): Bool {
133 | return FCLCrypto.\(verifyFunction)(
134 | address: address,
135 | message: message,
136 | keyIndices: keyIndices,
137 | signatures: signatures)
138 | }
139 | """
140 | }
141 |
142 | }
143 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/Config/AddressReplacement.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AddressReplacement.swift
3 | // FCL
4 | //
5 | // Created by Andrew Wang on 2022/6/29.
6 | //
7 |
8 | import Foundation
9 | import Cadence
10 |
11 | struct AddressReplacement: Hashable {
12 |
13 | let placeholder: String
14 | let replacement: Address
15 |
16 | public init(
17 | placeholder: String,
18 | replacement: Address
19 | ) {
20 | self.placeholder = placeholder
21 | self.replacement = replacement
22 | }
23 |
24 | public func hash(into hasher: inout Hasher) {
25 | hasher.combine(placeholder)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/Config/AppDetail.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDetail.swift
3 | // FCL
4 | //
5 | // Created by Andrew Wang on 2022/6/29.
6 | //
7 |
8 | import Foundation
9 | import SwiftyJSON
10 |
11 | public struct AppDetail: Encodable {
12 |
13 | let title: String
14 | let icon: URL?
15 | var custom: [String: Encodable] = [:]
16 |
17 | enum CodingKeys: String, CodingKey {
18 | case title
19 | case icon
20 | }
21 |
22 | public init(
23 | title: String,
24 | icon: URL?,
25 | custom: [String: Encodable] = [:]
26 | ) {
27 | self.title = title
28 | self.icon = icon
29 | self.custom = custom
30 | }
31 |
32 | public func encode(to encoder: Encoder) throws {
33 | var container = encoder.container(keyedBy: CodingKeys.self)
34 | try container.encode(title, forKey: .title)
35 | try container.encode(icon, forKey: .icon)
36 | var dynamicContainer = encoder.container(keyedBy: DynamicKey.self)
37 | for (key, value) in custom {
38 | if let codingKey = DynamicKey(stringValue: key) {
39 | try dynamicContainer.encode(value, forKey: codingKey)
40 | }
41 | }
42 | }
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/Config/Config.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Config.swift
3 | //
4 | //
5 | // Created by Andrew Wang on 2022/6/24.
6 | //
7 |
8 | import Foundation
9 | import FlowSDK
10 | import Cadence
11 |
12 | public enum WalletSelection {
13 | case authn(URL)
14 | case discoveryWallets([WalletProvider])
15 | }
16 |
17 | public enum Scope: String, Encodable {
18 | case email
19 | case name
20 | }
21 |
22 | public class Config {
23 |
24 | var network: Network = .testnet
25 |
26 | var appDetail: AppDetail?
27 |
28 | var walletProviderCandidates: [WalletProvider] = []
29 |
30 | var selectedWalletProvider: WalletProvider?
31 |
32 | var addressReplacements: Set = []
33 |
34 | var computeLimit: UInt64 = defaultComputeLimit
35 |
36 | /// To switch on and off for logging message
37 | var logging: Bool = true
38 |
39 | var openIdScopes: [Scope] = []
40 |
41 | public enum Option {
42 | @available(*, unavailable, renamed: "network", message: "Use network instead.")
43 | case env(String)
44 | case network(Network)
45 |
46 | case appDetail(AppDetail)
47 |
48 | @available(*, unavailable, renamed: "wallets", message: "Use supportedWalletProviders instead.")
49 | case challengeHandshake
50 | // Wallet Discovery mechanism
51 | case supportedWalletProviders([WalletProvider])
52 |
53 | case replace(placeHolder: String, with: Address)
54 |
55 | case computeLimit(UInt64)
56 |
57 | case logging(Bool)
58 |
59 | // User info
60 | /* TODO: implementation
61 | case challengeScope "challenge.scope"
62 | case openId([Scope])
63 | */
64 | }
65 |
66 | @discardableResult
67 | public func put(_ option: Option) throws -> Self {
68 | switch option {
69 | case let .network(network):
70 | self.network = network
71 | try walletProviderCandidates.forEach {
72 | try $0.updateNetwork(network)
73 | }
74 | case .env:
75 | break
76 | case let .appDetail(appDetail):
77 | self.appDetail = appDetail
78 | case .challengeHandshake:
79 | break
80 | case let .supportedWalletProviders(walletProviders):
81 | walletProviderCandidates = walletProviders
82 | if walletProviders.count == 1,
83 | let firstProvider = walletProviders.first {
84 | selectedWalletProvider = firstProvider
85 | }
86 | try walletProviderCandidates.forEach {
87 | try $0.updateNetwork(network)
88 | }
89 | case let .replace(placeholder, replacement):
90 | let addressReplacement = AddressReplacement(placeholder: placeholder, replacement: replacement)
91 | addressReplacements.insert(addressReplacement)
92 | case let .computeLimit(limit):
93 | computeLimit = limit
94 | case let .logging(enable):
95 | logging = enable
96 | /* TODO: implementation
97 | case let .openId(scopes):
98 | openIdScopes = scopes
99 | */
100 | }
101 | return self
102 | }
103 |
104 | public func reset() {
105 | selectedWalletProvider = nil
106 | }
107 |
108 | }
109 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/Constants.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Constants.swift
3 | //
4 | //
5 | // Created by Andrew Wang on 2022/6/29.
6 | //
7 |
8 | import Foundation
9 |
10 | enum Constants {
11 | static let fclVersion = "1.0.0"
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/Extensions/CadenceArgumentExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CadenceArgumentExtension.swift
3 | // FCL
4 | //
5 | // Created by Andrew Wang on 2022/7/27.
6 | //
7 |
8 | import Foundation
9 | import Cadence
10 |
11 | extension Cadence.Argument {
12 |
13 | func toFCLArgument() -> Argument {
14 | func randomString(length: Int) -> String {
15 | let letters = "abcdefghijklmnopqrstuvwxyz0123456789"
16 | return String((0 ..< length).map { _ in letters.randomElement()! })
17 | }
18 |
19 | return Argument(
20 | kind: "ARGUMENT",
21 | tempId: randomString(length: 10),
22 | value: value,
23 | asArgument: self,
24 | xform: Xform(label: type.rawValue)
25 | )
26 | }
27 |
28 | }
29 |
30 | extension Array where Element == Cadence.Argument {
31 |
32 | func toFCLArguments() -> [(String, Argument)] {
33 | var list = [(String, Argument)]()
34 | forEach { arg in
35 | let fclArg = arg.toFCLArgument()
36 | list.append((fclArg.tempId, fclArg))
37 | }
38 | return list
39 | }
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/Extensions/Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Extensions.swift
3 | // FCL
4 | //
5 | // Created by Andrew Wang on 2022/7/27.
6 | //
7 |
8 | import Foundation
9 |
10 | extension Array where Element: Hashable {
11 | func uniqued() -> [Element] {
12 | var seen = Set()
13 | return filter { seen.insert($0).inserted }
14 | }
15 | }
16 |
17 | extension String {
18 |
19 | public var hexDecodedData: Data {
20 | // Convert to a CString and make sure it has an even number of characters (terminating 0 is included, so we
21 | // check for uneven!)
22 | guard let cString = cString(using: .ascii), (cString.count % 2) == 1 else {
23 | return Data()
24 | }
25 |
26 | var result = Data(capacity: (cString.count - 1) / 2)
27 | for i in stride(from: 0, to: cString.count - 1, by: 2) {
28 | guard let l = hexCharToByte(cString[i]),
29 | let r = hexCharToByte(cString[i + 1]) else {
30 | return Data()
31 | }
32 | var value: UInt8 = (l << 4) | r
33 | result.append(&value, count: MemoryLayout.size(ofValue: value))
34 | }
35 | return result
36 | }
37 |
38 | func sansPrefix() -> String {
39 | if hasPrefix("0x") || hasPrefix("Fx") {
40 | return String(dropFirst(2))
41 | }
42 | return self
43 | }
44 |
45 | private func hexCharToByte(_ c: CChar) -> UInt8? {
46 | if c >= 48 && c <= 57 { // 0 - 9
47 | return UInt8(c - 48)
48 | }
49 | if c >= 97 && c <= 102 { // a - f
50 | return UInt8(10) + UInt8(c - 97)
51 | }
52 | if c >= 65 && c <= 70 { // A - F
53 | return UInt8(10) + UInt8(c - 65)
54 | }
55 | return nil
56 | }
57 |
58 | }
59 |
60 | extension URLRequest {
61 |
62 | func toReadable() -> String {
63 | var result = httpMethod ?? ""
64 | result.append("\n\n")
65 | let urlString = url?.absoluteString ?? ""
66 |
67 | result.append(urlString)
68 | result.append("\n\n")
69 | do {
70 | if let header = allHTTPHeaderFields {
71 | let headerData = try JSONSerialization.data(withJSONObject: header, options: .prettyPrinted)
72 | result.append(String(data: headerData, encoding: .utf8) ?? "")
73 | result.append("\n\n")
74 | }
75 | if let body = httpBody {
76 | let object = try JSONSerialization.jsonObject(with: body, options: .fragmentsAllowed)
77 | let bodyData = try JSONSerialization.data(withJSONObject: object, options: .prettyPrinted)
78 | result.append(String(data: bodyData, encoding: .utf8) ?? "")
79 | result.append("\n\n")
80 | }
81 | } catch {
82 | debugPrint(error)
83 | }
84 | return result
85 | }
86 |
87 | }
88 |
89 | extension Data {
90 |
91 | func prettyData() throws -> Data {
92 | let object = try JSONSerialization.jsonObject(with: self, options: .fragmentsAllowed)
93 | return try JSONSerialization.data(withJSONObject: object, options: .prettyPrinted)
94 | }
95 |
96 | }
97 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/Extensions/TaskExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TaskExtension.swift
3 | //
4 | //
5 | // Created by Andrew Wang on 2022/7/6.
6 | //
7 |
8 | import Foundation
9 |
10 | extension Task where Success == Never, Failure == Never {
11 |
12 | public
13 | static func sleep(seconds: Double) async throws {
14 | let duration = UInt64(seconds * 1_000_000_000)
15 | try await Task.sleep(nanoseconds: duration)
16 | }
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/FCLDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FCLDelegate.swift
3 | //
4 | //
5 | // Created by Andrew Wang on 2022/7/6.
6 | //
7 |
8 | import Foundation
9 | import AuthenticationServices
10 |
11 | public protocol FCLDelegate {
12 | func webAuthenticationContextProvider() -> ASPresentationAnchor?
13 | }
14 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/FCLError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FCLError.swift
3 | //
4 | //
5 | // Created by Andrew Wang on 2022/6/29.
6 | //
7 |
8 | import Foundation
9 |
10 | public enum FCLError: Swift.Error {
11 | case `internal`
12 | case parameterEncodingFailed
13 | case authenticateFailed
14 | case walletProviderNotSpecified
15 | case walletProviderInitFailed
16 | case responseUnexpected
17 | case authnFailed(message: String)
18 | case currentNetworkNotSupported
19 | case unexpectedResult
20 | case serviceError
21 | case invalidRequest
22 | case compositeSignatureInvalid
23 | case invaildProposer
24 | case fetchAccountFailure
25 | case missingPayer
26 | case unauthenticated
27 | case encodeFailed
28 | case userCanceled
29 | case serviceNotImplemented
30 | case unsupported
31 |
32 | case userNotFound
33 | case presentableNotFound
34 | case urlNotFound
35 | case serviceNotFound
36 | case resolverNotFound
37 | case accountNotFound
38 | case preAuthzNotFound
39 | case scriptNotFound
40 | case valueNotFound
41 | case authDataNotFound
42 | case latestBlockNotFound
43 | case keyNotFound
44 | case serviceTypeNotFound
45 | }
46 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/Models/AccountProofData.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Andrew Wang on 2022/7/11.
6 | //
7 |
8 | import Foundation
9 | import Cadence
10 |
11 | public struct FCLAccountProofData {
12 | /// A human-readable string e.g. "Blocto", "NBA Top Shot"
13 | public let appId: String
14 | /// minimum 32-byte random nonce
15 | public let nonce: String
16 |
17 | public init(appId: String, nonce: String) {
18 | self.appId = appId
19 | self.nonce = nonce
20 | }
21 |
22 | }
23 |
24 | public protocol AccountProofVerifiable {
25 | var address: Address { get }
26 | var nonce: String { get }
27 | var signatures: [CompositeSignatureVerifiable] { get }
28 | }
29 |
30 | public struct AccountProofSignatureData: AccountProofVerifiable {
31 |
32 | public let address: Address
33 | public let nonce: String
34 | public let signatures: [CompositeSignatureVerifiable]
35 |
36 | public init(
37 | address: Address,
38 | nonce: String,
39 | signatures: [FCLCompositeSignature]
40 | ) {
41 | self.address = address
42 | self.nonce = nonce
43 | self.signatures = signatures
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/Models/AuthData.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AuthData.swift
3 | // FCL
4 | //
5 | // Created by Andrew Wang on 2022/7/6.
6 | //
7 |
8 | import Foundation
9 |
10 | public struct AuthData: Decodable {
11 | let fclType: String?
12 | let fclVersion: String?
13 | let address: String? // exist in dapper wallet authn response, blocto api/flow/payer
14 | let services: [Service]?
15 | let keyId: Int? // exist in user signature
16 | let signature: String? // exist in blocto api/flow/payer
17 |
18 | // pre-authz response (blocto only)
19 | let proposer: Service?
20 | let payer: [Service]?
21 | let authorization: [Service]?
22 |
23 | enum CodingKeys: String, CodingKey {
24 | case fclType = "f_type"
25 | case fclVersion = "f_vsn"
26 | case address = "addr"
27 | case services
28 | case keyId
29 | case signature
30 | case proposer
31 | case payer
32 | case authorization
33 | }
34 |
35 | public init(from decoder: Decoder) throws {
36 | let container = try decoder.container(keyedBy: CodingKeys.self)
37 | self.fclType = try container.decodeIfPresent(String.self, forKey: .fclType)
38 | self.fclVersion = try container.decodeIfPresent(String.self, forKey: .fclVersion)
39 | self.address = try container.decodeIfPresent(String.self, forKey: .address)
40 | self.services = try container.decodeIfPresent([Service].self, forKey: .services)
41 | self.keyId = try container.decodeIfPresent(Int.self, forKey: .keyId)
42 | self.signature = try container.decodeIfPresent(String.self, forKey: .signature)
43 | self.proposer = try container.decodeIfPresent(Service.self, forKey: .proposer)
44 | self.payer = try container.decodeIfPresent([Service].self, forKey: .payer) ?? []
45 | self.authorization = try container.decodeIfPresent([Service].self, forKey: .authorization) ?? []
46 | }
47 |
48 | init(
49 | proposer: Service?,
50 | payer: [Service]?,
51 | authorization: [Service]?
52 | ) {
53 | self.fclType = nil
54 | self.fclVersion = nil
55 | self.address = nil
56 | self.services = nil
57 | self.keyId = nil
58 | self.signature = nil
59 | self.proposer = proposer
60 | self.payer = payer
61 | self.authorization = authorization
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/Models/ClientInfo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ClientInfo.swift
3 | // FCL
4 | //
5 | // Created by Andrew Wang on 2022/7/30.
6 | //
7 |
8 | import Foundation
9 |
10 | struct ClientInfo: Encodable {
11 |
12 | let fclVersion: String = Constants.fclVersion
13 | let fclLibrary: String = "https://github.com/portto/fcl-swift"
14 | let hostname: String? = nil
15 |
16 | enum CodingKeys: String, CodingKey {
17 | case fclVersion
18 | case fclLibrary
19 | case hostname
20 | }
21 |
22 | func encode(to encoder: Encoder) throws {
23 | var container = encoder.container(keyedBy: CodingKeys.self)
24 | try container.encode(fclVersion, forKey: .fclVersion)
25 | try container.encode(fclLibrary, forKey: .fclLibrary)
26 | try container.encode(hostname, forKey: .hostname)
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/Models/DynamicKey.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DynamicKey.swift
3 | // FCL-SDK
4 | //
5 | // Created by Andrew Wang on 2023/2/23.
6 | //
7 |
8 | import Foundation
9 |
10 | struct DynamicKey: CodingKey {
11 | var stringValue: String
12 | var intValue: Int?
13 |
14 | init?(stringValue: String) {
15 | self.stringValue = stringValue
16 | }
17 |
18 | init?(intValue: Int) {
19 | fatalError("init(intValue:) has not been implemented")
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/Models/FCLCompositeSignature.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FCLCompositeSignature.swift
3 | //
4 | //
5 | // Created by Andrew Wang on 2022/6/30.
6 | //
7 |
8 | import Foundation
9 |
10 | public protocol CompositeSignatureVerifiable {
11 | var address: String { get }
12 | var keyId: Int { get }
13 | var signature: String { get }
14 | }
15 |
16 | public struct FCLCompositeSignature: CompositeSignatureVerifiable, Decodable {
17 |
18 | public let fclType: String
19 | public let fclVersion: String
20 | public let address: String
21 | public let keyId: Int
22 | // hex string
23 | public let signature: String
24 |
25 | enum CodingKeys: String, CodingKey {
26 | case fclType = "f_type"
27 | case fclVersion = "f_vsn"
28 | case address = "addr"
29 | case keyId
30 | case signature
31 | }
32 |
33 | public init(
34 | address: String,
35 | keyId: Int,
36 | signature: String
37 | ) {
38 | self.fclType = Pragma.compositeSignature.fclType
39 | self.fclVersion = Pragma.compositeSignature.fclVersion
40 | self.address = address
41 | self.keyId = keyId
42 | self.signature = signature
43 | }
44 |
45 | }
46 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/Models/Interaction.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Interaction.swift
3 | // FCL
4 | //
5 | // Created by Andrew Wang on 2022/7/25.
6 | //
7 |
8 | import Foundation
9 | import FlowSDK
10 | import Cadence
11 | import BigInt
12 |
13 | struct Interaction: Encodable {
14 | var tag: Tag = .unknown
15 | var assigns = [String: String]()
16 | var status: Status = .ok
17 | var reason: String?
18 | var accounts = [String: SignableUser]()
19 | var params = [String: String]()
20 | var arguments = [String: Argument]()
21 | var message = Message()
22 | var proposer: String?
23 | var authorizations = [String]()
24 | var payer: String?
25 | var events = Events()
26 | var transaction = Id()
27 | var block = Block()
28 | var account = Account()
29 | var collection = Id()
30 |
31 | enum Status: String, CaseIterable, Codable {
32 | case ok = "OK"
33 | case bad = "BAD"
34 | }
35 |
36 | enum Tag: String, CaseIterable, Codable {
37 | case unknown = "UNKNOWN"
38 | case script = "SCRIPT"
39 | case transaction = "TRANSACTION"
40 | case getTransactionStatus = "GET_TRANSACTION_STATUS"
41 | case getAccount = "GET_ACCOUNT"
42 | case getEvents = "GET_EVENTS"
43 | case getLatestBlock = "GET_LATEST_BLOCK"
44 | case ping = "PING"
45 | case getTransaction = "GET_TRANSACTION"
46 | case getBlockById = "GET_BLOCK_BY_ID"
47 | case getBlockByHeight = "GET_BLOCK_BY_HEIGHT"
48 | case getBlock = "GET_BLOCK"
49 | case getBlockHeader = "GET_BLOCK_HEADER"
50 | case getCollection = "GET_COLLECTION"
51 | }
52 |
53 | @discardableResult
54 | mutating func setTag(_ tag: Tag) -> Self {
55 | self.tag = tag
56 | return self
57 | }
58 |
59 | var findInsideSigners: [String] {
60 | // Inside Signers Are: (authorizers + proposer) - payer
61 | var inside = Set(authorizations)
62 | if let proposer = proposer {
63 | inside.insert(proposer)
64 | }
65 | if let payer = payer {
66 | inside.remove(payer)
67 | }
68 | return Array(inside)
69 | }
70 |
71 | var findOutsideSigners: [String] {
72 | // Outside Signers Are: (payer)
73 | guard let payer = payer else {
74 | return []
75 | }
76 | let outside = Set([payer])
77 | return Array(outside)
78 | }
79 |
80 | func createProposalKey() -> ProposalKey {
81 | guard let proposer = proposer,
82 | let account = accounts[proposer],
83 | let sequenceNum = account.sequenceNum else {
84 | return ProposalKey()
85 | }
86 |
87 | return ProposalKey(
88 | address: account.address.hexString,
89 | keyId: Int(account.keyId),
90 | sequenceNum: Int(sequenceNum)
91 | )
92 | }
93 |
94 | func createFlowProposalKey() async throws -> Transaction.ProposalKey {
95 | guard let proposer = proposer,
96 | var account = accounts[proposer],
97 | let sequenceNumber = account.sequenceNum else {
98 | throw FCLError.invaildProposer
99 | }
100 |
101 | let address = account.address
102 | let keyId = account.keyId
103 |
104 | if let sequenceNum = account.sequenceNum {
105 | let key = Transaction.ProposalKey(
106 | address: address,
107 | keyIndex: Int(keyId),
108 | sequenceNumber: UInt64(sequenceNum)
109 | )
110 | return key
111 | } else {
112 | let remoteAccount = try await fcl.flowAPIClient.getAccountAtLatestBlock(address: address)
113 | guard let remoteAccount = remoteAccount else {
114 | throw FCLError.accountNotFound
115 | }
116 | account.sequenceNum = remoteAccount.keys[Int(keyId)].sequenceNumber
117 | let key = Transaction.ProposalKey(
118 | address: address,
119 | keyIndex: Int(keyId),
120 | sequenceNumber: sequenceNumber
121 | )
122 | return key
123 | }
124 | }
125 |
126 | func buildPreSignable(role: Role) -> PreSignable {
127 | PreSignable(
128 | roles: role,
129 | cadence: message.cadence ?? "",
130 | args: message.arguments.compactMap { tempId in arguments[tempId]?.asArgument },
131 | interaction: self
132 | )
133 | }
134 |
135 | func toFlowTransaction() async throws -> Transaction {
136 | let proposalKey = try await createFlowProposalKey()
137 |
138 | guard let payerAccount = payer,
139 | let payerAddress = accounts[payerAccount]?.address else {
140 | throw FCLError.missingPayer
141 | }
142 |
143 | var tx = try Transaction(
144 | script: Data((message.cadence ?? "").utf8),
145 | arguments: message.arguments.compactMap { tempId in arguments[tempId]?.asArgument },
146 | referenceBlockId: Identifier(hexString: message.refBlock ?? ""),
147 | gasLimit: message.computeLimit,
148 | proposalKey: proposalKey,
149 | payer: payerAddress,
150 | authorizers: authorizations
151 | .compactMap { cid in accounts[cid]?.address }
152 | .uniqued()
153 | )
154 |
155 | let insideSigners = findInsideSigners
156 | insideSigners.forEach { address in
157 | if let account = accounts[address],
158 | let signature = account.signature {
159 | tx.addPayloadSignature(
160 | address: account.address,
161 | keyIndex: Int(account.keyId),
162 | signature: signature.hexDecodedData
163 | )
164 | }
165 | }
166 |
167 | let outsideSigners = findOutsideSigners
168 | outsideSigners.forEach { address in
169 | if let account = accounts[address],
170 | let signature = account.signature {
171 | tx.addEnvelopeSignature(
172 | address: account.address,
173 | keyIndex: Int(account.keyId),
174 | signature: signature.hexDecodedData
175 | )
176 | }
177 | }
178 | return tx
179 | }
180 | }
181 |
182 | struct Argument: Encodable {
183 | var kind: String
184 | var tempId: String
185 | var value: Cadence.Value
186 | var asArgument: Cadence.Argument
187 | var xform: Xform
188 | }
189 |
190 | struct Xform: Codable {
191 | var label: String
192 | }
193 |
194 | struct Id: Encodable {
195 | var id: String?
196 | }
197 |
198 | let defaultComputeLimit: UInt64 = 100
199 |
200 | struct Message: Encodable {
201 | var cadence: String?
202 | var refBlock: String?
203 | var computeLimit: UInt64 = defaultComputeLimit
204 | var proposer: String?
205 | var payer: String?
206 | var authorizations: [String] = []
207 | var params: [String] = []
208 | var arguments: [String] = []
209 | }
210 |
211 | struct Events: Encodable {
212 | var eventType: String?
213 | var start: String?
214 | var end: String?
215 | var blockIds: [String] = []
216 | }
217 |
218 | struct Block: Encodable {
219 | var id: String?
220 | var height: Int64?
221 | var isSealed: Bool?
222 | }
223 |
224 | struct Account: Encodable {
225 | var addr: String?
226 | }
227 |
228 | struct ProposalKey: Encodable {
229 | var address: String?
230 | var keyId: Int?
231 | var sequenceNum: Int?
232 | }
233 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/Models/Pragma.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Pragma.swift
3 | //
4 | //
5 | // Created by Andrew Wang on 2022/7/14.
6 | //
7 |
8 | import Foundation
9 |
10 | struct Pragma {
11 |
12 | static let user = Pragma(fclType: "USER", fclVersion: Constants.fclVersion)
13 | static let provider = Pragma(fclType: "Provider", fclVersion: Constants.fclVersion)
14 | static let service = Pragma(fclType: "Service", fclVersion: Constants.fclVersion)
15 | static let identity = Pragma(fclType: "Identity", fclVersion: Constants.fclVersion)
16 | static let pollingResponse = Pragma(fclType: "PollingResponse", fclVersion: Constants.fclVersion)
17 | static let compositeSignature = Pragma(fclType: "CompositeSignature", fclVersion: Constants.fclVersion)
18 | static let openId = Pragma(fclType: "OpenId", fclVersion: Constants.fclVersion)
19 | static let preSignable = Pragma(fclType: "PreSignable", fclVersion: "1.0.1")
20 | static let signable = Pragma(fclType: "Signable", fclVersion: "1.0.1")
21 |
22 | let fclType: String
23 | let fclVersion: String
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/Models/PreSignable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PreSignable.swift
3 | // FCL
4 | //
5 | // Created by Andrew Wang on 2022/7/26.
6 | //
7 |
8 | import Foundation
9 | import Cadence
10 |
11 | public struct PreSignable: Encodable {
12 | let fclType: String = Pragma.preSignable.fclType
13 | let fclVersion: String = Pragma.preSignable.fclVersion
14 | let roles: Role
15 | let cadence: String
16 | var args: [Cadence.Argument] = []
17 | let data = [String: String]()
18 | var interaction = Interaction()
19 |
20 | var voucher: Voucher {
21 | let insideSigners: [Singature] = interaction.findInsideSigners.compactMap { id in
22 | guard let account = interaction.accounts[id] else { return nil }
23 | return Singature(
24 | address: account.address.hexString,
25 | keyId: account.keyId,
26 | sig: account.signature
27 | )
28 | }
29 |
30 | let outsideSigners: [Singature] = interaction.findOutsideSigners.compactMap { id in
31 | guard let account = interaction.accounts[id] else { return nil }
32 | return Singature(
33 | address: account.address.hexString,
34 | keyId: account.keyId,
35 | sig: account.signature
36 | )
37 | }
38 |
39 | return Voucher(
40 | cadence: interaction.message.cadence,
41 | refBlock: interaction.message.refBlock,
42 | computeLimit: interaction.message.computeLimit,
43 | arguments: interaction.message.arguments.compactMap { tempId in
44 | interaction.arguments[tempId]?.asArgument
45 | },
46 | proposalKey: interaction.createProposalKey(),
47 | payer: interaction.payer,
48 | authorizers: interaction.authorizations
49 | .compactMap { cid in interaction.accounts[cid]?.address.hexString }
50 | .uniqued(),
51 | payloadSigs: insideSigners,
52 | envelopeSigs: outsideSigners
53 | )
54 | }
55 |
56 | enum CodingKeys: String, CodingKey {
57 | case fType = "f_type"
58 | case fVsn = "f_vsn"
59 | case roles
60 | case cadence
61 | case args
62 | case interaction
63 | case voucher
64 | }
65 |
66 | public func encode(to encoder: Encoder) throws {
67 | var container = encoder.container(keyedBy: CodingKeys.self)
68 | try container.encode(fclType, forKey: .fType)
69 | try container.encode(fclVersion, forKey: .fVsn)
70 | try container.encode(roles, forKey: .roles)
71 | try container.encode(cadence, forKey: .cadence)
72 | try container.encode(args, forKey: .args)
73 | try container.encode(interaction, forKey: .interaction)
74 | try container.encode(voucher, forKey: .voucher)
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/Models/ProviderInfo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProviderInfo.swift
3 | //
4 | //
5 | // Created by Andrew Wang on 2022/7/5.
6 | //
7 |
8 | import Foundation
9 |
10 | public struct ProviderInfo {
11 | let title: String
12 | let desc: String?
13 | let icon: URL?
14 | }
15 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/Models/ResponseStatus.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ResponseStatus.swift
3 | //
4 | //
5 | // Created by Andrew Wang on 2022/7/1.
6 | //
7 |
8 | import Foundation
9 |
10 | enum ResponseStatus: String, Decodable {
11 | case pending = "PENDING"
12 | case approved = "APPROVED"
13 | case declined = "DECLINED"
14 | }
15 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/Models/Role.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Role.swift
3 | // FCL-SDK
4 | //
5 | // Created by Andrew Wang on 2022/8/16.
6 | //
7 |
8 | import Foundation
9 |
10 | struct Role: Encodable {
11 | var proposer: Bool = false
12 | var authorizer: Bool = false
13 | var payer: Bool = false
14 | var param: Bool?
15 |
16 | mutating func merge(role: Role) {
17 | proposer = proposer || role.proposer
18 | authorizer = authorizer || role.authorizer
19 | payer = payer || role.payer
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/Models/RoleType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RoleType.swift
3 | // FCL-SDK
4 | //
5 | // Created by Andrew Wang on 2022/8/16.
6 | //
7 |
8 | import Foundation
9 |
10 | enum RoleType: String {
11 | case proposer = "PROPOSER"
12 | case payer = "PAYER"
13 | case authorizer = "AUTHORIZER"
14 | }
15 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/Models/Signable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Signable.swift
3 | // FCL
4 | //
5 | // Created by Andrew Wang on 2022/7/30.
6 | //
7 |
8 | import Foundation
9 | import Cadence
10 |
11 | struct Signable: Encodable {
12 |
13 | let fclType: String = Pragma.signable.fclType
14 | let fclVersion: String = Pragma.signable.fclVersion
15 | let data = [String: String]()
16 | let message: String
17 | let keyId: UInt32
18 | let address: Cadence.Address
19 | let roles: Role
20 | let cadence: String?
21 | let args: [Cadence.Argument]
22 | let interaction: Interaction
23 |
24 | enum CodingKeys: String, CodingKey {
25 | case fclType = "f_type"
26 | case fclVersion = "f_vsn"
27 | case address = "addr"
28 | case roles
29 | case data
30 | case message
31 | case keyId
32 | case cadence
33 | case args
34 | case interaction
35 | case voucher
36 | }
37 |
38 | var voucher: Voucher {
39 | let insideSigners: [Singature] = interaction.findInsideSigners.compactMap { id in
40 | guard let account = interaction.accounts[id] else { return nil }
41 | return Singature(
42 | address: account.address.hexString,
43 | keyId: account.keyId,
44 | sig: account.signature
45 | )
46 | }
47 |
48 | let outsideSigners: [Singature] = interaction.findOutsideSigners.compactMap { id in
49 | guard let account = interaction.accounts[id] else { return nil }
50 | return Singature(
51 | address: account.address.hexString,
52 | keyId: account.keyId,
53 | sig: account.signature
54 | )
55 | }
56 |
57 | return Voucher(
58 | cadence: interaction.message.cadence,
59 | refBlock: interaction.message.refBlock,
60 | computeLimit: interaction.message.computeLimit,
61 | arguments: interaction.message.arguments.compactMap { tempId in
62 | interaction.arguments[tempId]?.asArgument
63 | },
64 | proposalKey: interaction.createProposalKey(),
65 | payer: interaction.accounts[interaction.payer ?? ""]?.address.hexString,
66 | authorizers: interaction.authorizations
67 | .compactMap { cid in interaction.accounts[cid]?.address.hexString }
68 | .uniqued(),
69 | payloadSigs: insideSigners,
70 | envelopeSigs: outsideSigners
71 | )
72 | }
73 |
74 | func encode(to encoder: Encoder) throws {
75 | var container = encoder.container(keyedBy: CodingKeys.self)
76 | try container.encode(fclType, forKey: .fclType)
77 | try container.encode(fclVersion, forKey: .fclVersion)
78 | try container.encode(data, forKey: .data)
79 | try container.encode(message, forKey: .message)
80 | try container.encode(keyId, forKey: .keyId)
81 | try container.encode(roles, forKey: .roles)
82 | try container.encode(cadence, forKey: .cadence)
83 | try container.encode(address.hexString, forKey: .address)
84 | try container.encode(args, forKey: .args)
85 | try container.encode(interaction, forKey: .interaction)
86 | try container.encode(voucher, forKey: .voucher)
87 | }
88 |
89 | }
90 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/Models/SignableUser.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SignableUser.swift
3 | // FCL-SDK
4 | //
5 | // Created by Andrew Wang on 2022/8/16.
6 | //
7 |
8 | import Foundation
9 | import Cadence
10 |
11 | struct SignableUser: Encodable {
12 | var address: Cadence.Address
13 | var keyId: UInt32
14 | var role: Role
15 |
16 | // Assigned in SignatureResolver
17 | var signature: String?
18 | // Assigned in SequenceNumberResolver
19 | var sequenceNum: UInt64?
20 |
21 | var tempId: String {
22 | address.hexString + "-" + String(keyId)
23 | }
24 |
25 | var signingFunction: (Data) async throws -> AuthResponse
26 |
27 | enum CodingKeys: String, CodingKey {
28 | case address = "addr"
29 | case keyId
30 | case role
31 | case signature
32 | case sequenceNum
33 | case tempId
34 | }
35 |
36 | func encode(to encoder: Encoder) throws {
37 | var container = encoder.container(keyedBy: CodingKeys.self)
38 | try container.encode(address.hexString, forKey: .address)
39 | try container.encode(keyId, forKey: .keyId)
40 | try container.encode(role, forKey: .role)
41 | try container.encode(signature, forKey: .signature)
42 | try container.encode(sequenceNum, forKey: .sequenceNum)
43 | try container.encode(tempId, forKey: .tempId)
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/Models/Singature.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Singature.swift
3 | // FCL-SDK
4 | //
5 | // Created by Andrew Wang on 2022/8/16.
6 | //
7 |
8 | import Foundation
9 |
10 | struct Singature: Encodable {
11 | let address: String
12 | let keyId: UInt32
13 | let sig: String?
14 | }
15 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/Models/TransactionFeePayer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TransactionFeePayer.swift
3 | // FCL
4 | //
5 | // Created by Andrew Wang on 2022/7/29.
6 | //
7 |
8 | import Foundation
9 | import Cadence
10 |
11 | /// The key that specifies the fee payer address with key for a transaction.
12 | public struct TransactionFeePayer: Equatable {
13 |
14 | public let address: Address
15 | public let keyIndex: Int
16 |
17 | public var tempId: String {
18 | address.hexString + "-" + String(keyIndex)
19 | }
20 |
21 | public init(address: Address, keyIndex: Int) {
22 | self.address = address
23 | self.keyIndex = keyIndex
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/Models/Voucher.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Voucher.swift
3 | // FCL
4 | //
5 | // Created by Andrew Wang on 2022/7/26.
6 | //
7 |
8 | import Foundation
9 | import Cadence
10 |
11 | struct Voucher: Encodable {
12 | let cadence: String?
13 | let refBlock: String?
14 | let computeLimit: UInt64
15 | let arguments: [Cadence.Argument]
16 | let proposalKey: ProposalKey
17 | var payer: String?
18 | let authorizers: [String]?
19 | let payloadSigs: [Singature]?
20 | let envelopeSigs: [Singature]?
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/Network/PollingResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PollingResponse.swift
3 | //
4 | //
5 | // Created by Andrew Wang on 2022/7/1.
6 | //
7 |
8 | import Foundation
9 |
10 | /// Used for authn, authz and pre-authz
11 | struct AuthResponse: Decodable {
12 | let fclType: String?
13 | let fclVersion: String?
14 | let status: ResponseStatus
15 | var updates: Service? // authn
16 | var local: Service? // authn, authz
17 | var data: AuthData?
18 | let reason: String?
19 | let compositeSignature: AuthData? // authz
20 | let authorizationUpdates: Service? // authz
21 | let userSignatures: [FCLCompositeSignature]
22 |
23 | enum CodingKeys: String, CodingKey {
24 | case fclType = "f_type"
25 | case fclVersion = "f_vsn"
26 | case status
27 | case updates
28 | case local
29 | case data
30 | case reason
31 | case compositeSignature
32 | case authorizationUpdates
33 | }
34 |
35 | init(from decoder: Decoder) throws {
36 | let container = try decoder.container(keyedBy: CodingKeys.self)
37 | fclType = try? container.decode(String.self, forKey: .fclType)
38 | fclVersion = try? container.decode(String.self, forKey: .fclVersion)
39 | status = try container.decode(ResponseStatus.self, forKey: .status)
40 | updates = try? container.decode(Service.self, forKey: .updates)
41 | do {
42 | local = try container.decode(Service.self, forKey: .local)
43 | } catch {
44 | let locals = try? container.decode([Service].self, forKey: .local)
45 | local = locals?.first
46 | }
47 | data = try? container.decode(AuthData.self, forKey: .data)
48 | userSignatures = (try? container.decode([FCLCompositeSignature].self, forKey: .data)) ?? []
49 | reason = try? container.decode(String.self, forKey: .reason)
50 | compositeSignature = try? container.decode(AuthData.self, forKey: .compositeSignature)
51 | authorizationUpdates = try? container.decode(Service.self, forKey: .authorizationUpdates)
52 | }
53 | }
54 |
55 | /// Used for post pre-authz response
56 | struct PollingWrappedResponse: Decodable {
57 | let fclType: String?
58 | let fclVersion: String?
59 | let status: ResponseStatus
60 | let data: Model?
61 | let reason: String?
62 |
63 | enum CodingKeys: String, CodingKey {
64 | case fclType = "f_type"
65 | case fclVersion = "f_vsn"
66 | case status
67 | case data
68 | case reason
69 | }
70 |
71 | init(from decoder: Decoder) throws {
72 | let container = try decoder.container(keyedBy: CodingKeys.self)
73 | self.fclType = try? container.decode(String.self, forKey: .fclType)
74 | self.fclVersion = try? container.decode(String.self, forKey: .fclVersion)
75 | self.status = try container.decode(ResponseStatus.self, forKey: .status)
76 | switch status {
77 | case .pending:
78 | self.reason = nil
79 | self.data = nil
80 | case .approved:
81 | self.reason = nil
82 | self.data = try container.decode(Model.self, forKey: .data)
83 | case .declined:
84 | self.reason = try? container.decode(String.self, forKey: .reason)
85 | self.data = nil
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/Network/URLSessionExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URLSessionExtension.swift
3 | //
4 | //
5 | // Created by Andrew Wang on 2022/7/6.
6 | //
7 |
8 | import UIKit
9 |
10 | extension URLSession {
11 |
12 | static let decoder = JSONDecoder()
13 |
14 | func dataAuthnResponse(for request: URLRequest) async throws -> AuthResponse {
15 | log(message: request.toReadable())
16 | let data = try await data(for: request)
17 | log(message: String(data: try data.prettyData(), encoding: .utf8) ?? "")
18 | return try Self.decoder.decode(AuthResponse.self, from: data)
19 | }
20 |
21 | private func data(for request: URLRequest) async throws -> Data {
22 | try await withCheckedThrowingContinuation { continuation in
23 | dataTask(with: request) { data, _, error in
24 | if let error = error {
25 | continuation.resume(with: .failure(error))
26 | } else if let data = data {
27 | continuation.resume(with: .success(data))
28 | } else {
29 | continuation.resume(with: .failure(FCLError.responseUnexpected))
30 | }
31 | }.resume()
32 | }
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/Resolve/AccountsResolver.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AccountsResolver.swift
3 | // FCL
4 | //
5 | // Created by Andrew Wang on 2022/7/26.
6 | //
7 |
8 | import Foundation
9 | import FlowSDK
10 | import Cadence
11 |
12 | final class AccountsResolver: Resolver {
13 |
14 | func resolve(ix: Interaction) async throws -> Interaction {
15 | if ix.tag == .transaction {
16 | return try await collectAccounts(ix: ix, accounts: Array(ix.accounts.values))
17 | }
18 |
19 | return try await withCheckedThrowingContinuation { continuation in
20 | continuation.resume(with: .success(ix))
21 | }
22 | }
23 |
24 | func collectAccounts(ix: Interaction, accounts: [SignableUser]) async throws -> Interaction {
25 | guard let currentUser = fcl.currentUser,
26 | currentUser.loggedIn else {
27 | throw FCLError.unauthenticated
28 | }
29 |
30 | guard let walletProvider = fcl.config.selectedWalletProvider else {
31 | throw FCLError.walletProviderNotSpecified
32 | }
33 |
34 | let preSignable = ix.buildPreSignable(role: Role())
35 | let authData = try await walletProvider.preAuthz(preSignable: preSignable)
36 |
37 | let signableUsers = buildSignableUsers(authData: authData)
38 | var accounts = [String: SignableUser]()
39 |
40 | var newIX = ix
41 | newIX.authorizations.removeAll()
42 | signableUsers.forEach { user in
43 | let tempId = user.tempId
44 |
45 | if accounts.keys.contains(tempId) {
46 | accounts[tempId]?.role.merge(role: user.role)
47 | }
48 | accounts[tempId] = user
49 |
50 | if user.role.proposer {
51 | newIX.proposer = tempId
52 | }
53 |
54 | if user.role.payer {
55 | newIX.payer = tempId
56 | }
57 |
58 | if user.role.authorizer {
59 | newIX.authorizations.append(tempId)
60 | }
61 | }
62 | newIX.accounts = accounts
63 | return newIX
64 | }
65 |
66 | func buildSignableUsers(authData: AuthData) -> [SignableUser] {
67 | var axs = [(role: RoleType, service: Service)]()
68 | if let proposer = authData.proposer {
69 | axs.append((RoleType.proposer, proposer))
70 | }
71 | for az in authData.payer ?? [] {
72 | axs.append((RoleType.payer, az))
73 | }
74 | for az in authData.authorization ?? [] {
75 | axs.append((RoleType.authorizer, az))
76 | }
77 |
78 | return axs.compactMap { role, service in
79 |
80 | guard let address = service.identity?.address,
81 | let keyId = service.identity?.keyId else {
82 | return nil
83 | }
84 |
85 | return SignableUser(
86 | address: Cadence.Address(hexString: address),
87 | keyId: keyId,
88 | role: Role(
89 | proposer: role == .proposer,
90 | authorizer: role == .authorizer,
91 | payer: role == .payer,
92 | param: nil
93 | )
94 | ) { data in
95 | let request = try service.getURLRequest(body: data)
96 | return try await fcl.pollingRequest(request, type: .authz)
97 | }
98 | }
99 | }
100 |
101 | }
102 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/Resolve/CadenceResolver.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CadenceResolver.swift
3 | // FCL
4 | //
5 | // Created by Andrew Wang on 2022/7/26.
6 | //
7 |
8 | import Foundation
9 |
10 | final class CadenceResolver: Resolver {
11 |
12 | func resolve(ix: Interaction) async throws -> Interaction {
13 | guard let cadenceSript = ix.message.cadence else {
14 | throw FCLError.scriptNotFound
15 | }
16 | if ix.tag == .transaction || ix.tag == .script {
17 | var newIx = ix
18 | newIx.message.cadence = fcl.config.addressReplacements.reduce(cadenceSript) { result, replacement in
19 | result.replacingOccurrences(
20 | of: replacement.placeholder,
21 | with: replacement.replacement.hexStringWithPrefix
22 | )
23 | }
24 | return newIx
25 | }
26 | return ix
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/Resolve/RefBlockResolver.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RefBlockResolver.swift
3 | // FCL
4 | //
5 | // Created by Andrew Wang on 2022/7/26.
6 | //
7 |
8 | import Foundation
9 | import FlowSDK
10 |
11 | final class RefBlockResolver: Resolver {
12 |
13 | func resolve(ix: Interaction) async throws -> Interaction {
14 | let block = try await fcl.flowAPIClient.getLatestBlock(isSealed: true)
15 | var newIX = ix
16 | newIX.message.refBlock = block?.blockHeader.id.hexString
17 | return newIX
18 | }
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/Resolve/Resolver.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Resolver.swift
3 | // FCL
4 | //
5 | // Created by Andrew Wang on 2022/7/26.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol Resolver {
11 | func resolve(ix: Interaction) async throws -> Interaction
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/Resolve/SequenceNumberResolver.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SequenceNumberResolver.swift
3 | // FCL
4 | //
5 | // Created by Andrew Wang on 2022/7/26.
6 | //
7 |
8 | import Cadence
9 | import Foundation
10 |
11 | final class SequenceNumberResolver: Resolver {
12 |
13 | func resolve(ix: Interaction) async throws -> Interaction {
14 | guard let proposer = ix.proposer,
15 | let account = ix.accounts[proposer] else {
16 | throw FCLError.internal
17 | }
18 |
19 | if account.sequenceNum == nil {
20 | let remoteAccount = try await fcl.flowAPIClient.getAccountAtLatestBlock(address: account.address)
21 | guard let remoteAccount = remoteAccount else {
22 | throw FCLError.accountNotFound
23 | }
24 | var newIX = ix
25 | newIX.accounts[proposer]?.sequenceNum = remoteAccount.keys[Int(account.keyId)].sequenceNumber
26 | return newIX
27 | } else {
28 | return ix
29 | }
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/Resolve/SignatureResolver.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SignatureResolver.swift
3 | // FCL
4 | //
5 | // Created by Andrew Wang on 2022/7/26.
6 | //
7 |
8 | import Foundation
9 | import FlowSDK
10 |
11 | final class SignatureResolver: Resolver {
12 |
13 | func resolve(ix interaction: Interaction) async throws -> Interaction {
14 | var ix = interaction
15 |
16 | guard ix.tag == .transaction else {
17 | throw FCLError.internal
18 | }
19 |
20 | let insideSigners = interaction.findInsideSigners
21 |
22 | let tx = try await ix.toFlowTransaction()
23 |
24 | let payloadSignatureMap = try await withThrowingTaskGroup(
25 | of: (id: String, signature: String).self,
26 | returning: [String: String].self,
27 | body: { taskGroup in
28 |
29 | let insidePayload = tx.encodedPayload.toHexString()
30 |
31 | let interaction = ix
32 | for address in insideSigners {
33 | taskGroup.addTask {
34 | try await self.fetchSignature(
35 | ix: interaction,
36 | payload: insidePayload,
37 | id: address
38 | )
39 | }
40 | }
41 |
42 | var returning: [String: String] = [:]
43 | for try await result in taskGroup {
44 | returning[result.id] = result.signature
45 | }
46 | return returning
47 | }
48 | )
49 |
50 | payloadSignatureMap.forEach { id, signature in
51 | ix.accounts[id]?.signature = signature
52 | }
53 |
54 | let outsideSigners = ix.findOutsideSigners
55 | let envelopeSignatureMap = try await withThrowingTaskGroup(
56 | of: (id: String, signature: String).self,
57 | returning: [String: String].self,
58 | body: { taskGroup in
59 |
60 | let envelopeMessage = encodeEnvelopeMessage(
61 | transaction: tx,
62 | ix: ix,
63 | insideSigners: insideSigners
64 | )
65 |
66 | let interaction = ix
67 | for address in outsideSigners {
68 | taskGroup.addTask {
69 | try await self.fetchSignature(
70 | ix: interaction,
71 | payload: envelopeMessage,
72 | id: address
73 | )
74 | }
75 | }
76 | var returning: [String: String] = [:]
77 | for try await result in taskGroup {
78 | returning[result.id] = result.signature
79 | }
80 | return returning
81 | }
82 |
83 | )
84 | envelopeSignatureMap.forEach { id, signature in
85 | ix.accounts[id]?.signature = signature
86 | }
87 | return ix
88 | }
89 |
90 | func fetchSignature(
91 | ix: Interaction,
92 | payload: String,
93 | id: String
94 | ) async throws -> (id: String, signature: String) {
95 | guard let account = ix.accounts[id],
96 | let signable = buildSignable(
97 | ix: ix,
98 | payload: payload,
99 | account: account
100 | ),
101 | let data = try? JSONEncoder().encode(signable) else {
102 | throw FCLError.internal
103 | }
104 |
105 | let response = try await account.signingFunction(data)
106 | return (id: id, signature: (response.data?.signature ?? response.compositeSignature?.signature) ?? "")
107 | }
108 |
109 | func encodeEnvelopeMessage(
110 | transaction: Transaction,
111 | ix: Interaction,
112 | insideSigners: [String]
113 | ) -> String {
114 | var tx = transaction
115 | insideSigners.forEach { address in
116 | if let account = ix.accounts[address],
117 | let signature = account.signature {
118 | tx.addPayloadSignature(
119 | address: account.address,
120 | keyIndex: Int(account.keyId),
121 | signature: signature.hexDecodedData
122 | )
123 | }
124 | }
125 |
126 | return tx.encodedEnvelope.toHexString()
127 | }
128 |
129 | func buildSignable(
130 | ix: Interaction,
131 | payload: String,
132 | account: SignableUser
133 | ) -> Signable? {
134 | Signable(
135 | message: payload,
136 | keyId: account.keyId,
137 | address: account.address,
138 | roles: account.role,
139 | cadence: ix.message.cadence,
140 | args: ix.message.arguments.compactMap { tempId in
141 | ix.arguments[tempId]?.asArgument
142 | },
143 | interaction: ix
144 | )
145 | }
146 |
147 | }
148 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/User/Service/Service.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Service.swift
3 | // FCL
4 | //
5 | // Created by Andrew Wang on 2022/6/30.
6 | //
7 |
8 | import Foundation
9 | import SwiftyJSON
10 | import UIKit
11 |
12 | public struct Service: Decodable {
13 | let fclType: String?
14 | let fclVersion: String?
15 | let type: ServiceType?
16 | let method: ServiceMethod?
17 | let endpoint: URL?
18 | let uid: String?
19 | let id: String?
20 | let identity: ServiceIdentity?
21 | let provider: ServiceProvider?
22 | let params: [String: String]
23 | let data: ServiceDataType
24 |
25 | enum CodingKeys: String, CodingKey {
26 | case fclType = "f_type"
27 | case fclVersion = "f_vsn"
28 | case type
29 | case method
30 | case endpoint
31 | case uid
32 | case id
33 | case identity
34 | case provider
35 | case params
36 | case data
37 | }
38 |
39 | public init(from decoder: Decoder) throws {
40 | let container = try decoder.container(keyedBy: CodingKeys.self)
41 | self.fclType = try? container.decode(String.self, forKey: .fclType)
42 | self.fclVersion = try? container.decode(String.self, forKey: .fclVersion)
43 | self.type = try? container.decode(ServiceType.self, forKey: .type)
44 | self.method = try? container.decode(ServiceMethod.self, forKey: .method)
45 | self.endpoint = try? container.decode(URL.self, forKey: .endpoint)
46 | self.uid = try? container.decode(String.self, forKey: .uid)
47 | self.id = try? container.decode(String.self, forKey: .id)
48 | self.identity = try? container.decode(ServiceIdentity.self, forKey: .identity)
49 | self.provider = try? container.decode(ServiceProvider.self, forKey: .provider)
50 | self.params = (try? container.decode([String: String].self, forKey: .params)) ?? [:]
51 | switch type {
52 | case .openId:
53 | if let openId = try? container.decode(JSON.self, forKey: .data) {
54 | self.data = .openId(openId)
55 | } else {
56 | throw DecodingError.dataCorrupted(
57 | DecodingError.Context(
58 | codingPath: decoder.codingPath,
59 | debugDescription: "open id data structure not exist."
60 | )
61 | )
62 | }
63 | case .accountProof:
64 | if let accountProof = try? container.decode(ServiceAccountProof.self, forKey: .data) {
65 | self.data = .accountProof(accountProof)
66 | } else {
67 | self.data = .notExist
68 | }
69 | case .authn,
70 | .localView,
71 | .authz,
72 | .preAuthz,
73 | .backChannel,
74 | .userSignature,
75 | .authnRefresh:
76 | if let json = try? container.decode(JSON.self, forKey: .data) {
77 | self.data = .json(json)
78 | } else {
79 | self.data = .notExist
80 | }
81 | case .none:
82 | self.data = .notExist
83 | }
84 | }
85 | }
86 |
87 | extension Service {
88 |
89 | func getURLRequest(body: Data? = nil) throws -> URLRequest {
90 | switch type {
91 | case .authn:
92 | throw FCLError.serviceNotImplemented
93 | case .localView,
94 | .preAuthz,
95 | .userSignature,
96 | .backChannel,
97 | .authz,
98 | .none:
99 | guard let endpoint = endpoint else {
100 | throw FCLError.serviceError
101 | }
102 | guard let requestURL = buildURL(url: endpoint, params: params) else {
103 | throw FCLError.invalidRequest
104 | }
105 | let object = try body?.toDictionary() ?? [:]
106 | return try RequstBuilder.buildURLRequest(url: requestURL, method: method, body: object)
107 | case .openId:
108 | throw FCLError.serviceNotImplemented
109 | case .accountProof:
110 | throw FCLError.serviceNotImplemented
111 | case .authnRefresh:
112 | throw FCLError.serviceNotImplemented
113 | }
114 | }
115 |
116 | private func buildURL(url: URL, params: [String: String] = [:]) -> URL? {
117 | guard var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
118 | return nil
119 | }
120 |
121 | var queryItems: [URLQueryItem] = []
122 |
123 | for (name, value) in params {
124 | queryItems.append(
125 | URLQueryItem(name: name, value: value)
126 | )
127 | }
128 |
129 | urlComponents.queryItems = queryItems
130 | return urlComponents.url
131 | }
132 |
133 | }
134 |
135 | extension Encodable {
136 | /// Converting object to postable dictionary
137 | func toDictionary(_ encoder: JSONEncoder = JSONEncoder()) throws -> [String: Any] {
138 | let data = try encoder.encode(self)
139 | let object = try JSONSerialization.jsonObject(with: data)
140 | guard let json = object as? [String: Any] else {
141 | let context = DecodingError.Context(codingPath: [], debugDescription: "Deserialized object is not a dictionary")
142 | throw DecodingError.typeMismatch(type(of: object), context)
143 | }
144 | return json
145 | }
146 | }
147 |
148 | extension Data {
149 |
150 | func toDictionary() throws -> [String: Any] {
151 | let object = try JSONSerialization.jsonObject(with: self)
152 | guard let json = object as? [String: Any] else {
153 | let context = DecodingError.Context(codingPath: [], debugDescription: "Deserialized data is not a dictionary")
154 | throw DecodingError.typeMismatch(type(of: object), context)
155 | }
156 | return json
157 | }
158 |
159 | }
160 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/User/Service/ServiceAccountProof.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ServiceAccountProof.swift
3 | //
4 | //
5 | // Created by Andrew Wang on 2022/6/30.
6 | //
7 |
8 | import Foundation
9 |
10 | public struct ServiceAccountProof: Decodable {
11 |
12 | let fclType: String
13 | let fclVersion: String
14 | let address: String
15 | let nonce: String
16 | let signatures: [FCLCompositeSignature]
17 |
18 | enum CodingKeys: String, CodingKey {
19 | case fclType = "f_type"
20 | case fclVersion = "f_vsn"
21 | case address
22 | case nonce
23 | case signatures
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/User/Service/ServiceDataType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ServiceDataType.swift
3 | //
4 | //
5 | // Created by Andrew Wang on 2022/6/30.
6 | //
7 |
8 | import Foundation
9 | import SwiftyJSON
10 |
11 | // https://github.com/onflow/fcl-js/blob/master/packages/fcl/src/current-user/normalize/open-id.js
12 | /*
13 | {
14 | "f_type": "Service",
15 | "f_vsn": "1.0.0",
16 | "type": "open-id",
17 | "uid": "uniqueDedupeKey",
18 | "method: "data",
19 | "data": {
20 | "profile": {
21 | "name": "Bob",
22 | "family_name": "Builder",
23 | "given_name": "Robert",
24 | "middle_name": "the",
25 | "nickname": "Bob the Builder",
26 | "perferred_username": "bob",
27 | "profile": "https://www.bobthebuilder.com/",
28 | "picture": "https://avatars.onflow.org/avatar/bob",
29 | "gender": "...",
30 | "birthday": "2001-01-18",
31 | "zoneinfo": "America/Vancouver",
32 | "locale": "en-us",
33 | "updated_at": "1614970797388"
34 | },
35 | "email": {
36 | "email": "bob@bob.bob",
37 | "email_verified": true
38 | },
39 | "address": {
40 | "address": "One Apple Park Way, Cupertino, CA 95014, USA"
41 | },
42 | "phone": {
43 | "phone_number": "+1 (xxx) yyy-zzzz",
44 | "phone_number_verified": true
45 | },
46 | "social": {
47 | "twitter": "@_qvvg",
48 | "twitter_verified": true
49 | },
50 | }
51 | }
52 | */
53 |
54 | public enum ServiceDataType {
55 | case openId(JSON)
56 | case accountProof(ServiceAccountProof)
57 | case json(JSON)
58 | case notExist
59 | }
60 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/User/Service/ServiceIdentity.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ServiceIdentity.swift
3 | //
4 | //
5 | // Created by Andrew Wang on 2022/6/30.
6 | //
7 |
8 | import Foundation
9 |
10 | struct ServiceIdentity: Decodable {
11 | public let fclType: String? // proposer, payer, authorization in PreAuthzResponse do not have this key.
12 | public let fclVersion: String? // proposer, payer, authorization in PreAuthzResponse do not have this key.
13 | public let address: String
14 | let keyId: UInt32
15 |
16 | enum CodingKeys: String, CodingKey {
17 | case fclType = "f_type"
18 | case fclVersion = "f_vsn"
19 | case address
20 | case keyId
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/User/Service/ServiceMethod.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ServiceMethod.swift
3 | //
4 | //
5 | // Created by Andrew Wang on 2022/6/30.
6 | //
7 |
8 | import Foundation
9 |
10 | public enum ServiceMethod: String, Decodable {
11 | case httpPost = "HTTP/POST"
12 | case httpGet = "HTTP/GET"
13 | case iframe = "VIEW/IFRAME"
14 | case iframeRPC = "IFRAME/RPC"
15 | case browserIframe = "BROWSER/IFRAME"
16 | case data = "DATA"
17 |
18 | var httpMethod: String? {
19 | switch self {
20 | case .httpGet:
21 | return "GET"
22 | case .httpPost:
23 | return "POST"
24 | default:
25 | return nil
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/User/Service/ServiceProvider.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ServiceProvider.swift
3 | //
4 | //
5 | // Created by Andrew Wang on 2022/6/30.
6 | //
7 |
8 | import Foundation
9 |
10 | struct ServiceProvider: Decodable {
11 | public let fclType: String
12 | public let fclVersion: String
13 | public let address: String?
14 | public let name: String?
15 | public let iconString: String?
16 |
17 | public var iconURL: URL? {
18 | if let iconString = iconString {
19 | return URL(string: iconString)
20 | }
21 | return nil
22 | }
23 |
24 | enum CodingKeys: String, CodingKey {
25 | case fclType = "f_type"
26 | case fclVersion = "f_vsn"
27 | case address
28 | case name
29 | case iconString = "icon"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/User/Service/ServiceType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ServiceType.swift
3 | //
4 | //
5 | // Created by Andrew Wang on 2022/6/30.
6 | //
7 |
8 | import Foundation
9 |
10 | public enum ServiceType: String, Decodable {
11 | case authn
12 | case authz
13 | case preAuthz = "pre-authz"
14 | case userSignature = "user-signature"
15 | case backChannel = "back-channel-rpc"
16 | case openId = "open-id"
17 | case accountProof = "account-proof"
18 | case authnRefresh = "authn-refresh"
19 | case localView = "local-view"
20 | }
21 |
22 | extension ServiceType: Equatable {}
23 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/User/User.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Andrew Wang on 2022/6/29.
6 | //
7 |
8 | import Foundation
9 | import Cadence
10 |
11 | public struct User: Decodable {
12 |
13 | public var fclType: String = Pragma.user.fclType
14 | public var fclVersion: String = Pragma.user.fclVersion
15 | public let address: Address
16 | public var loggedIn: Bool = false
17 | public let expiresAt: TimeInterval
18 | private var accountProofData: AccountProofSignatureData?
19 | public let services: [Service]
20 |
21 | public var accountProof: AccountProofSignatureData? {
22 | if let proof = accountProofData {
23 | return proof.signatures.isEmpty ? nil : proof
24 | } else {
25 | do {
26 | let accountProofService = try fcl.serviceOfType(type: .accountProof)
27 | if case let .accountProof(serviceAccountProof) = accountProofService?.data {
28 | return AccountProofSignatureData(
29 | address: address,
30 | nonce: serviceAccountProof.nonce,
31 | signatures: serviceAccountProof.signatures)
32 | }
33 | return nil
34 | } catch {
35 | return nil
36 | }
37 | }
38 | }
39 |
40 | var expiresAtDate: Date {
41 | Date(timeIntervalSince1970: expiresAt)
42 | }
43 |
44 | enum CodingKeys: String, CodingKey {
45 | case fclType = "f_type"
46 | case fclVersion = "f_vsn"
47 | case address = "addr"
48 | case loggedIn
49 | case expiresAt
50 | case services
51 | }
52 |
53 | public init(
54 | fclType: String,
55 | fclVersion: String,
56 | address: Address,
57 | accountProof: AccountProofSignatureData?,
58 | loggedIn: Bool = false,
59 | expiresAt: TimeInterval,
60 | services: [Service]
61 | ) {
62 | self.fclType = fclType
63 | self.fclVersion = fclVersion
64 | self.address = address
65 | self.accountProofData = accountProof
66 | self.loggedIn = loggedIn
67 | self.expiresAt = expiresAt
68 | self.services = services
69 | }
70 |
71 | public init(
72 | address: Address,
73 | accountProof: AccountProofSignatureData?,
74 | loggedIn: Bool = false,
75 | expiresAt: TimeInterval,
76 | services: [Service]
77 | ) {
78 | self.address = address
79 | self.accountProofData = accountProof
80 | self.loggedIn = loggedIn
81 | self.expiresAt = expiresAt
82 | self.services = services
83 | }
84 |
85 | }
86 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/Utilities/RequestBuilder.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RequestBuilder.swift
3 | // FCL
4 | //
5 | // Created by Andrew Wang on 2022/8/12.
6 | //
7 |
8 | import Foundation
9 |
10 | enum RequstBuilder {
11 |
12 | static func buildURLRequest(
13 | url: URL,
14 | method: ServiceMethod?,
15 | body: [String: Any] = [:]
16 | ) throws -> URLRequest {
17 | var urlRequest = URLRequest(url: url)
18 | urlRequest.httpMethod = method?.httpMethod
19 |
20 | guard let selectedWalletProvider = fcl.config.selectedWalletProvider else {
21 | throw FCLError.walletProviderNotSpecified
22 | }
23 |
24 | var newRequest = selectedWalletProvider.modifyRequest(urlRequest)
25 |
26 | if newRequest.httpMethod == ServiceMethod.httpPost.httpMethod {
27 | var object = body
28 | if let appDetail = fcl.config.appDetail {
29 | let appDetailDic = try appDetail.toDictionary()
30 | object = object.merging(appDetailDic, uniquingKeysWith: { $1 })
31 | }
32 | if fcl.config.openIdScopes.isEmpty == false {
33 | let openIdScopesDic = try fcl.config.openIdScopes.toDictionary()
34 | object = object.merging(openIdScopesDic, uniquingKeysWith: { $1 })
35 | }
36 | let clientInfoDic = try ClientInfo().toDictionary()
37 | object = object.merging(clientInfoDic, uniquingKeysWith: { $1 })
38 | let body = try? JSONSerialization.data(withJSONObject: object)
39 | newRequest.httpBody = body
40 | newRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
41 | newRequest.addValue("application/json", forHTTPHeaderField: "Accept")
42 | }
43 | return newRequest
44 | }
45 |
46 | static func buildURLRequest(
47 | url: URL,
48 | method: ServiceMethod?,
49 | encodableBody: Encodable?
50 | ) throws -> URLRequest {
51 | var urlRequest = URLRequest(url: url)
52 | urlRequest.httpMethod = method?.httpMethod
53 |
54 | guard let selectedWalletProvider = fcl.config.selectedWalletProvider else {
55 | throw FCLError.walletProviderNotSpecified
56 | }
57 |
58 | var newRequest = selectedWalletProvider.modifyRequest(urlRequest)
59 |
60 | if newRequest.httpMethod == ServiceMethod.httpPost.httpMethod {
61 | var object: [String: Any] = [:]
62 | if let appDetail = fcl.config.appDetail {
63 | let appDetailDic = try appDetail.toDictionary()
64 | object = object.merging(appDetailDic, uniquingKeysWith: { $1 })
65 | }
66 | if fcl.config.openIdScopes.isEmpty == false {
67 | let openIdScopesDic = try fcl.config.openIdScopes.toDictionary()
68 | object = object.merging(openIdScopesDic, uniquingKeysWith: { $1 })
69 | }
70 | if let encodableBody = encodableBody {
71 | let encodableBodyDic = try encodableBody.toDictionary()
72 | object = object.merging(encodableBodyDic, uniquingKeysWith: { $1 })
73 | }
74 | let clientInfoDic = try ClientInfo().toDictionary()
75 | object = object.merging(clientInfoDic, uniquingKeysWith: { $1 })
76 | let body = try? JSONSerialization.data(withJSONObject: object)
77 | newRequest.httpBody = body
78 | newRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
79 | newRequest.addValue("application/json", forHTTPHeaderField: "Accept")
80 | }
81 | return newRequest
82 | }
83 |
84 | }
85 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/WalletProvider/BloctoWalletProvider.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BloctoWalletProvider.swift
3 | //
4 | //
5 | // Created by Andrew Wang on 2022/7/5.
6 | //
7 |
8 | import Foundation
9 | import UIKit
10 | import FlowSDK
11 | import BloctoSDK
12 | import SwiftyJSON
13 | import Cadence
14 |
15 | public final class BloctoWalletProvider: WalletProvider {
16 |
17 | var bloctoFlowSDK: BloctoFlowSDK
18 | public let providerInfo: ProviderInfo = ProviderInfo(
19 | title: "Blocto",
20 | desc: "Entrance to blockchain world.",
21 | icon: URL(string: "https://ipfs.blocto.app/ipfs/QmTmQQBz5KfVUcHW83S3kxqh29vSQ3cH7pcsc6cngYsG5U")
22 | )
23 | private(set) var network: Network
24 | private(set) var environment: BloctoEnvironment
25 |
26 | private let bloctoAppIdentifier: String
27 |
28 | private var bloctoAppScheme: String {
29 | switch environment {
30 | case .prod:
31 | return "blocto://"
32 | case .dev:
33 | return "blocto-dev://"
34 | }
35 | }
36 |
37 | private var bloctoApiBaseURLString: String {
38 | switch environment {
39 | case .prod:
40 | return "https://api.blocto.app"
41 | case .dev:
42 | return "https://api-dev.blocto.app"
43 | }
44 | }
45 |
46 | private var webAuthnURL: URL? {
47 | switch environment {
48 | case .prod:
49 | return URL(string: "https://wallet-v2.blocto.app/api/flow/authn")
50 | case .dev:
51 | return URL(string: "https://wallet-v2-dev.blocto.app/api/flow/authn")
52 | }
53 | }
54 |
55 | /// Initial wallet provider
56 | /// - Parameters:
57 | /// - bloctoAppIdentifier: identifier from app registered in blocto developer dashboard.
58 | /// testnet dashboard: https://developers-staging.blocto.app/
59 | /// mainnet dashboard: https://developers.blocto.app/
60 | /// - window: used for presenting webView if no Blocto app installed. If pass nil then we will get the top ViewContoller from keyWindow.
61 | /// - network: indicate flow network to use.
62 | /// - logging: Enabling log message, default is true.
63 | public init(
64 | bloctoAppIdentifier: String,
65 | window: UIWindow?,
66 | network: Network,
67 | logging: Bool = true
68 | ) throws {
69 | self.bloctoAppIdentifier = bloctoAppIdentifier
70 | let getWindow = { () throws -> UIWindow in
71 | guard let window = window ?? fcl.getKeyWindow() else {
72 | throw FCLError.walletProviderInitFailed
73 | }
74 | return window
75 | }
76 | self.network = network
77 | if let environment = Self.getBloctoEnvironment(by: network) {
78 | self.environment = environment
79 | } else {
80 | throw FCLError.currentNetworkNotSupported
81 | }
82 | BloctoSDK.shared.initialize(
83 | with: bloctoAppIdentifier,
84 | getWindow: getWindow,
85 | logging: logging,
86 | environment: environment
87 | )
88 | self.bloctoFlowSDK = BloctoSDK.shared.flow
89 | }
90 |
91 | /// Get called when config network changed
92 | /// - Parameter network: Flow network
93 | public func updateNetwork(_ network: Network) throws {
94 | self.network = network
95 | if let environment = Self.getBloctoEnvironment(by: network) {
96 | self.environment = environment
97 | } else {
98 | throw FCLError.currentNetworkNotSupported
99 | }
100 | BloctoSDK.shared.updateEnvironment(environment)
101 | }
102 |
103 | /// Ask user to authanticate and get flow address along with account proof if provide accountProofData
104 | /// - Parameter accountProofData: AccountProofData used for proving a user controls an on-chain account, optional.
105 | public func authn(accountProofData: FCLAccountProofData?) async throws {
106 | if let bloctoAppSchemeURL = URL(string: bloctoAppScheme),
107 | await UIApplication.shared.canOpenURL(bloctoAppSchemeURL) {
108 | // blocto app installed
109 | try await setupUserByBloctoSDK(accountProofData)
110 | } else {
111 | // blocto app not install
112 | guard let authnURL = webAuthnURL else {
113 | throw FCLError.urlNotFound
114 | }
115 |
116 | let body = RequestBody(
117 | nonce: accountProofData?.nonce,
118 | appIdentifier: accountProofData?.appId,
119 | config: FlowConfig(app: Config(id: bloctoAppIdentifier))
120 | )
121 |
122 | let authnRequest = try RequstBuilder.buildURLRequest(
123 | url: authnURL,
124 | method: .httpPost,
125 | encodableBody: body
126 | )
127 | let authResponse = try await fcl.pollingRequest(authnRequest, type: .authn)
128 | fcl.currentUser = try fcl.buildUser(authn: authResponse)
129 | }
130 | }
131 |
132 | public func getUserSignature(_ message: String) async throws -> [FCLCompositeSignature] {
133 | guard let user = fcl.currentUser else { throw FCLError.userNotFound }
134 | if let bloctoAppSchemeURL = URL(string: bloctoAppScheme),
135 | await UIApplication.shared.canOpenURL(bloctoAppSchemeURL) {
136 | // blocto app installed
137 | return try await withCheckedThrowingContinuation { continuation in
138 | bloctoFlowSDK.signMessage(
139 | from: user.address.hexStringWithPrefix,
140 | message: message
141 | ) { result in
142 | switch result {
143 | case let .success(flowCompositeSignatures):
144 | continuation.resume(returning: flowCompositeSignatures.map {
145 | FCLCompositeSignature(
146 | address: $0.address,
147 | keyId: $0.keyId,
148 | signature: $0.signature
149 | )
150 | })
151 | case let .failure(error):
152 | continuation.resume(throwing: error)
153 | }
154 | }
155 | }
156 | } else {
157 | // blocto app not install
158 | guard let userSignatureService = try fcl.serviceOfType(type: .userSignature) else {
159 | throw FCLError.serviceNotFound
160 | }
161 |
162 | let encoder = JSONEncoder()
163 | let encodeData = try encoder.encode(["message": Data(message.utf8).toHexString()])
164 | let response = try await fcl.polling(
165 | service: userSignatureService,
166 | data: encodeData
167 | )
168 | return response.userSignatures
169 | }
170 | }
171 |
172 | public func mutate(
173 | cadence: String,
174 | arguments: [Cadence.Argument],
175 | limit: UInt64,
176 | authorizers: [Cadence.Address]
177 | ) async throws -> Identifier {
178 | if let bloctoAppSchemeURL = URL(string: bloctoAppScheme),
179 | await UIApplication.shared.canOpenURL(bloctoAppSchemeURL) {
180 |
181 | guard let userAddress = fcl.currentUser?.address else {
182 | throw FCLError.userNotFound
183 | }
184 | guard let account = try await fcl.flowAPIClient.getAccountAtLatestBlock(address: userAddress) else {
185 | throw FCLError.accountNotFound
186 | }
187 | guard let block = try await fcl.flowAPIClient.getLatestBlock(isSealed: true) else {
188 | throw FCLError.latestBlockNotFound
189 | }
190 |
191 | guard let cosignerKey = account.keys
192 | .first(where: { $0.weight == 999 && $0.revoked == false }) else {
193 | throw FCLError.keyNotFound
194 | }
195 |
196 | let proposalKey = Transaction.ProposalKey(
197 | address: userAddress,
198 | keyIndex: cosignerKey.index,
199 | sequenceNumber: cosignerKey.sequenceNumber
200 | )
201 |
202 | let feePayer = try await bloctoFlowSDK.getFeePayerAddress()
203 |
204 | let transaction = try FlowSDK.Transaction(
205 | script: Data(cadence.utf8),
206 | arguments: arguments,
207 | referenceBlockId: block.blockHeader.id,
208 | gasLimit: limit,
209 | proposalKey: proposalKey,
210 | payer: feePayer,
211 | authorizers: authorizers
212 | )
213 | return try await withCheckedThrowingContinuation { [weak self] continuation in
214 | guard let self = self else {
215 | continuation.resume(throwing: FCLError.internal)
216 | return
217 | }
218 | Task { @MainActor in
219 | self.bloctoFlowSDK.sendTransaction(
220 | from: userAddress,
221 | transaction: transaction
222 | ) { result in
223 | switch result {
224 | case let .success(txId):
225 | continuation.resume(returning: Identifier(hexString: txId))
226 | case let .failure(error):
227 | continuation.resume(throwing: error)
228 | }
229 | }
230 | }
231 | }
232 | } else {
233 | return try await fcl.send([
234 | .transaction(script: cadence),
235 | .computeLimit(limit),
236 | .arguments(arguments),
237 | ])
238 | }
239 | }
240 |
241 | /// Retrive preSignable info for Flow transaction
242 | /// - Parameter preSignable: Pre-defined type.
243 | /// - Returns: Data includes proposer, payer, authorization.
244 | /// Only used if Blocto native app not install.
245 | public func preAuthz(preSignable: PreSignable?) async throws -> AuthData {
246 | guard fcl.currentUser != nil else { throw FCLError.userNotFound }
247 | // blocto app not install
248 | guard let service = try fcl.serviceOfType(type: .preAuthz) else {
249 | throw FCLError.preAuthzNotFound
250 | }
251 |
252 | var data: Data?
253 | if let preSignable = preSignable {
254 | data = try JSONEncoder().encode(preSignable)
255 | }
256 |
257 | // for blocto pre-authz it will response approved directly once request.
258 | let authResponse = try await fcl.polling(service: service, data: data)
259 | guard let authData = authResponse.data else {
260 | throw FCLError.authDataNotFound
261 | }
262 | return authData
263 | }
264 |
265 | public func modifyRequest(_ request: URLRequest) -> URLRequest {
266 | var newRequest = request
267 | newRequest.addValue(bloctoAppIdentifier, forHTTPHeaderField: "Blocto-Application-Identifier")
268 | return newRequest
269 | }
270 |
271 | /// Entry of Universal Links
272 | /// - Parameter userActivity: the same userActivity from UIApplicationDelegate
273 | public func continueForLinks(_ userActivity: NSUserActivity) {
274 | BloctoSDK.shared.continue(userActivity)
275 | }
276 |
277 | /// Entry of custom scheme
278 | /// - Parameters:
279 | /// - url: custom scheme URL
280 | public func application(open url: URL) {
281 | BloctoSDK.shared.application(open: url)
282 | }
283 |
284 | // MARK: - Private
285 |
286 | private func setupUserByBloctoSDK(_ accountProofData: FCLAccountProofData?) async throws {
287 | let (address, accountProof): (String, AccountProofSignatureData?) = try await withCheckedThrowingContinuation { continuation in
288 | var bloctoAccountProofData: FlowAccountProofData?
289 | if let accountProofData = accountProofData {
290 | bloctoAccountProofData = FlowAccountProofData(
291 | appId: accountProofData.appId,
292 | nonce: accountProofData.nonce
293 | )
294 | }
295 | bloctoFlowSDK.authanticate(accountProofData: bloctoAccountProofData) { result in
296 | switch result {
297 | case let .success((address, accountProof)):
298 | if let fclAccountProofData = accountProofData {
299 | let fclAccountProofSignatures = accountProof.map {
300 | FCLCompositeSignature(
301 | address: $0.address,
302 | keyId: $0.keyId,
303 | signature: $0.signature
304 | )
305 | }
306 | let accountProofSignatureData = AccountProofSignatureData(
307 | address: Address(hexString: address),
308 | nonce: fclAccountProofData.nonce,
309 | signatures: fclAccountProofSignatures
310 | )
311 | continuation.resume(returning: (address, accountProofSignatureData))
312 | } else {
313 | continuation.resume(returning: (address, nil))
314 | }
315 | case let .failure(error):
316 | continuation.resume(throwing: FCLError.authnFailed(message: String(describing: error)))
317 | }
318 | }
319 | }
320 |
321 | fcl.currentUser = User(
322 | address: Address(hexString: address),
323 | accountProof: accountProof,
324 | loggedIn: true,
325 | expiresAt: 0,
326 | services: []
327 | )
328 | }
329 |
330 | private static func getBloctoEnvironment(by network: Network) -> BloctoEnvironment? {
331 | switch network {
332 | case .mainnet:
333 | return .prod
334 | case .testnet:
335 | return .dev
336 | case .canarynet:
337 | return nil
338 | case .sandboxnet:
339 | return nil
340 | case .emulator:
341 | return nil
342 | case .custom:
343 | return nil
344 | }
345 | }
346 |
347 | }
348 |
349 | extension BloctoWalletProvider {
350 |
351 | struct FlowConfig: Encodable {
352 | let app: Config
353 | }
354 |
355 | struct Config: Encodable {
356 | let id: String // Blocto dApp id
357 | }
358 |
359 | struct RequestBody: Encodable {
360 | let nonce: String? // Nonce for account-proof. Must be a minimum 32-byte hex string
361 | let appIdentifier: String? // Human-readable string that uniquely identifies your application name
362 | let config: FlowConfig
363 | }
364 |
365 | }
366 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/WalletProvider/DapperWalletProvider.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DapperWalletProvider.swift
3 | //
4 | //
5 | // Created by Andrew Wang on 2022/7/5.
6 | //
7 |
8 | import Foundation
9 | import FlowSDK
10 | import Cadence
11 |
12 | public final class DapperWalletProvider: WalletProvider {
13 |
14 | public static let `default`: DapperWalletProvider = {
15 | let info = ProviderInfo(
16 | title: "Dapper Wallet",
17 | desc: nil,
18 | icon: URL(string: "https://ipfs.blocto.app/ipfs/Qmb81oGbB9qxUct7udtHsAqiJkRf4ey2bxuDhdg1ojFDfr")
19 | )
20 | return DapperWalletProvider(providerInfo: info)
21 | }()
22 |
23 | public var providerInfo: ProviderInfo
24 | var user: User?
25 |
26 | // mainnet only for now
27 | private var accessNodeApiString: String {
28 | switch fcl.config.network {
29 | case .testnet,
30 | .canarynet,
31 | .sandboxnet,
32 | .emulator,
33 | .custom:
34 | return ""
35 | case .mainnet:
36 | return "https://dapper-http-post.vercel.app/api/authn"
37 | }
38 | }
39 |
40 | init(providerInfo: ProviderInfo) {
41 | self.providerInfo = providerInfo
42 | }
43 |
44 | public func updateNetwork(_ network: Network) {}
45 |
46 | public func authn(accountProofData: FCLAccountProofData?) async throws {
47 | let session = URLSession(configuration: .default)
48 | let urlComponent = URLComponents(string: accessNodeApiString)
49 | guard let requestURL = urlComponent?.url else {
50 | throw FCLError.urlNotFound
51 | }
52 | var request = URLRequest(url: requestURL)
53 | request.httpMethod = "POST"
54 |
55 | let pollingResponse = try await session.dataAuthnResponse(for: request)
56 |
57 | guard let localService = pollingResponse.local else {
58 | throw FCLError.authenticateFailed
59 | }
60 |
61 | guard let updatesService = pollingResponse.updates else {
62 | throw FCLError.authenticateFailed
63 | }
64 |
65 | if accountProofData != nil {
66 | log(message: "Dapper not support native account proof for now.")
67 | }
68 |
69 | let openBrowserTask = Task { @MainActor in
70 | try fcl.openWithWebAuthenticationSession(localService)
71 | let authnResponse = try await fcl.polling(service: updatesService)
72 | fcl.currentUser = try fcl.buildUser(authn: authnResponse)
73 | }
74 | _ = try await openBrowserTask.result.get()
75 | }
76 |
77 | public func getUserSignature(_ message: String) async throws -> [FCLCompositeSignature] {
78 | throw FCLError.unsupported
79 | }
80 |
81 | public func mutate(
82 | cadence: String,
83 | arguments: [Cadence.Argument],
84 | limit: UInt64,
85 | authorizers: [Cadence.Address]
86 | ) async throws -> Identifier {
87 | throw FCLError.unsupported
88 | }
89 |
90 | public func preAuthz(preSignable: PreSignable?) async throws -> AuthData {
91 | throw FCLError.unsupported
92 | }
93 |
94 | public func modifyRequest(_ request: URLRequest) -> URLRequest {
95 | /// Workaround
96 | if fcl.config.selectedWalletProvider is DapperWalletProvider,
97 | let url = request.url,
98 | url.absoluteString.contains("https://dapper-http-post.vercel.app/api/authn-poll") {
99 | /// Though POST https://dapper-http-post.vercel.app/api/authn?l6n=https://foo.com response back-channel-rpc using method HTTP/POST
100 | /// Requesting using GET will only be accepted by dapper wallet.
101 | var newRequest = request
102 | newRequest.httpMethod = ServiceMethod.httpGet.httpMethod
103 | return newRequest
104 | }
105 | return request
106 | }
107 |
108 | }
109 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/WalletProvider/WalletProvider.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WalletProvider.swift
3 | //
4 | //
5 | // Created by Andrew Wang on 2022/7/5.
6 | //
7 |
8 | import Foundation
9 | import FlowSDK
10 | import Cadence
11 |
12 | public protocol WalletProvider {
13 |
14 | /// Info to describe wallet provider
15 | var providerInfo: ProviderInfo { get }
16 |
17 | /// Method called by user changing network of Flow blockchain.
18 | /// - Parameter network: Flow network
19 | func updateNetwork(_ network: Network) throws
20 |
21 | /// Authentication of Flow blockchain account address. if valid account proof data provided,
22 | /// - Parameter accountProofData: Pre-defined struct used to sign for account proot.
23 | func authn(accountProofData: FCLAccountProofData?) async throws
24 |
25 | /// To retrive user signatures of specific input message.
26 | /// - Parameter message: A human readable string e.g. "message to be sign"
27 | /// - Returns: Pre-defined signature array.
28 | func getUserSignature(_ message: String) async throws -> [FCLCompositeSignature]
29 |
30 | /// Modify Flow blockchain state with transaction compositions.
31 | /// - Parameters:
32 | /// - cadence: Transaction script of Flow transaction.
33 | /// - arguments: Arguments of Flow transaction.
34 | /// - limit: Gas limit (compute limit) of Flow transaction.
35 | /// - authorizers: Addresses of accounts data being modify by current transaction.
36 | /// - Returns: Transaction identifier (tx hash).
37 | func mutate(
38 | cadence: String,
39 | arguments: [Cadence.Argument],
40 | limit: UInt64,
41 | authorizers: [Cadence.Address]
42 | ) async throws -> Identifier
43 |
44 | /// Retrive preSignable info for Flow transaction.
45 | /// - Parameter preSignable: Pre-defined type.
46 | /// - Returns: Data includes proposer, payer, authorization.
47 | /// Only be used if wallet provider implement web send transaction.
48 | func preAuthz(preSignable: PreSignable?) async throws -> AuthData
49 |
50 | /// Method to modify url request before sending. Default implementation will not modify request.
51 | /// - Parameter request: URLRequest about to send.
52 | /// - Returns: URLRequest that has been modified.
53 | func modifyRequest(_ request: URLRequest) -> URLRequest
54 |
55 | /// Entry of Universal Links
56 | /// - Parameter userActivity: the same userActivity from UIApplicationDelegate
57 | /// Only be used if wallet provider involve other native app authentication.
58 | func continueForLinks(_ userActivity: NSUserActivity)
59 |
60 | /// Entry of custom scheme
61 | /// - Parameters:
62 | /// - url: custom scheme URL
63 | /// Only be used if wallet provider involve other native app authentication.
64 | func application(open url: URL)
65 |
66 | // TODO: implementation
67 | /*
68 | func openId() async throws -> JSON {}
69 | */
70 | }
71 |
72 | extension WalletProvider {
73 |
74 | func modifyRequest(_ request: URLRequest) -> URLRequest {
75 | request
76 | }
77 |
78 | public func continueForLinks(_ userActivity: NSUserActivity) {}
79 |
80 | public func application(open url: URL) {}
81 |
82 | }
83 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/WalletProviderSelectionViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WalletProviderSelectionViewController.swift
3 | // FCL-SDK
4 | //
5 | // Created by Andrew Wang on 2022/8/24.
6 | //
7 |
8 | import Foundation
9 | import UIKit
10 |
11 | final class WalletProviderSelectionViewController: UIViewController {
12 |
13 | var onSelect: ((WalletProvider) -> Void)?
14 | var onCancel: (() -> Void)?
15 |
16 | private let providers: [WalletProvider]
17 |
18 | private lazy var containerView: UIView = {
19 | let view = UIView()
20 | view.backgroundColor = .white
21 | view.translatesAutoresizingMaskIntoConstraints = false
22 | view.addSubview(titleLabel)
23 | titleLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 20).isActive = true
24 | titleLabel.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20).isActive = true
25 | titleLabel.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20).isActive = true
26 | view.addSubview(stackView)
27 | stackView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 20).isActive = true
28 | stackView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20).isActive = true
29 | stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -20).isActive = true
30 | stackView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20).isActive = true
31 | return view
32 | }()
33 |
34 | private lazy var titleLabel: UILabel = {
35 | let label = UILabel()
36 | label.text = "Select a wallet"
37 | label.font = .systemFont(ofSize: 24)
38 | label.textColor = .black
39 | label.translatesAutoresizingMaskIntoConstraints = false
40 | return label
41 | }()
42 |
43 | private lazy var cancelButton: UIButton = {
44 | let button = UIButton()
45 | button.translatesAutoresizingMaskIntoConstraints = false
46 | button.setBackgroundImage(nil, for: .normal)
47 | button.addTarget(self, action: #selector(self.onCancel(sender:)), for: .touchUpInside)
48 | return button
49 | }()
50 |
51 | private lazy var stackView: UIStackView = {
52 | let stackView = UIStackView()
53 | stackView.axis = .vertical
54 | stackView.distribution = .equalSpacing
55 | stackView.alignment = .fill
56 | stackView.spacing = 12
57 | stackView.translatesAutoresizingMaskIntoConstraints = false
58 | return stackView
59 | }()
60 |
61 | init(providers: [WalletProvider]) {
62 | self.providers = providers
63 | super.init(nibName: nil, bundle: nil)
64 | }
65 |
66 | @available(*, unavailable)
67 | required init?(coder: NSCoder) {
68 | fatalError("init(coder:) has not been implemented")
69 | }
70 |
71 | override func viewDidLoad() {
72 | super.viewDidLoad()
73 |
74 | view.addSubview(cancelButton)
75 | cancelButton.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
76 | cancelButton.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
77 | cancelButton.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
78 | cancelButton.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
79 |
80 | cancelButton.addSubview(containerView)
81 | containerView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
82 | containerView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
83 | containerView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 50).isActive = true
84 | containerView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -50).isActive = true
85 |
86 | for (index, provider) in providers.enumerated() {
87 | let button = createProviderSelectionButton(from: provider.providerInfo, index: index)
88 | button.addTarget(self, action: #selector(onClicked(sender:)), for: .touchUpInside)
89 | stackView.addArrangedSubview(button)
90 | }
91 | }
92 |
93 | override func viewDidLayoutSubviews() {
94 | super.viewDidLayoutSubviews()
95 | containerView.layer.cornerRadius = 10
96 | containerView.clipsToBounds = true
97 | }
98 |
99 | private func createProviderSelectionButton(from info: ProviderInfo, index: Int) -> UIButton {
100 | let iconImage = UIImageView()
101 | if let iconURL = info.icon {
102 | let request = URLRequest(url: iconURL)
103 | URLSession(configuration: .default)
104 | .dataTask(with: request) { data, _, error in
105 | if let error = error {
106 | log(message: String(describing: error))
107 | return
108 | }
109 | guard let data = data else {
110 | log(message: "Icon image data not found.")
111 | return
112 | }
113 |
114 | DispatchQueue.main.async {
115 | iconImage.image = UIImage(data: data)
116 | }
117 | }.resume()
118 | }
119 | let button = UIButton()
120 | button.translatesAutoresizingMaskIntoConstraints = false
121 | button.heightAnchor.constraint(equalToConstant: 65).isActive = true
122 | button.tag = index
123 | button.layer.cornerRadius = 10
124 | button.layer.borderWidth = 1
125 | button.layer.borderColor = UIColor(red: 225 / 255, green: 225 / 255, blue: 225 / 255, alpha: 1).cgColor
126 | button.clipsToBounds = true
127 | let titleLabel = UILabel()
128 | titleLabel.font = .systemFont(ofSize: 16)
129 | titleLabel.textColor = .black
130 | titleLabel.text = info.title
131 |
132 | button.addSubview(iconImage)
133 |
134 | let container = UIStackView()
135 | container.isUserInteractionEnabled = false
136 | container.axis = .vertical
137 | container.alignment = .leading
138 | container.distribution = .equalSpacing
139 | button.addSubview(container)
140 |
141 | container.addArrangedSubview(titleLabel)
142 |
143 | if let desc = info.desc {
144 | let descLabel = UILabel()
145 | descLabel.font = .systemFont(ofSize: 12)
146 | descLabel.textColor = .gray
147 | descLabel.text = desc
148 | container.addArrangedSubview(descLabel)
149 | }
150 |
151 | iconImage.translatesAutoresizingMaskIntoConstraints = false
152 | iconImage.topAnchor.constraint(equalTo: button.topAnchor, constant: 12).isActive = true
153 | iconImage.leftAnchor.constraint(equalTo: button.leftAnchor, constant: 12).isActive = true
154 | iconImage.bottomAnchor.constraint(equalTo: button.bottomAnchor, constant: -12).isActive = true
155 | iconImage.widthAnchor.constraint(equalTo: iconImage.heightAnchor).isActive = true
156 |
157 | container.translatesAutoresizingMaskIntoConstraints = false
158 | container.topAnchor.constraint(equalTo: button.topAnchor, constant: 12).isActive = true
159 | container.leftAnchor.constraint(equalTo: iconImage.rightAnchor, constant: 12).isActive = true
160 | container.rightAnchor.constraint(equalTo: button.rightAnchor, constant: -12).isActive = true
161 | container.bottomAnchor.constraint(equalTo: button.bottomAnchor, constant: -12).isActive = true
162 | return button
163 | }
164 |
165 | @objc
166 | private func onClicked(sender: UIButton) {
167 | cancelButton.isUserInteractionEnabled = false
168 | let index = sender.tag
169 | onSelect?(providers[index])
170 | onSelect = nil
171 | onCancel = nil
172 | }
173 |
174 | @objc
175 | private func onCancel(sender: UIButton) {
176 | cancelButton.isUserInteractionEnabled = false
177 | onCancel?()
178 | onCancel = nil
179 | onSelect = nil
180 | }
181 | }
182 |
183 | // MARK: UIAdaptivePresentationControllerDelegate
184 |
185 | extension WalletProviderSelectionViewController: UIAdaptivePresentationControllerDelegate {
186 |
187 | func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
188 | false
189 | }
190 |
191 | }
192 |
--------------------------------------------------------------------------------
/Sources/FCL-SDK/WalletUtilities/WalletUtilities.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WalletUtilities.swift
3 | //
4 | //
5 | // Created by Andrew Wang on 2022/7/11.
6 | //
7 |
8 | import Foundation
9 | import FlowSDK
10 | import Cadence
11 |
12 | public enum WalletUtilities {
13 |
14 | public static func encodeAccountProof(
15 | address: Address,
16 | nonce: String,
17 | appIdentifier: String,
18 | includeDomainTag: Bool
19 | ) -> String {
20 | let accountProofData: RLPEncodable = [
21 | appIdentifier,
22 | Data(hex: String(address.hexString)),
23 | Data(hex: nonce),
24 | ]
25 | if includeDomainTag {
26 | return (DomainTag.accountProof.rightPaddedData + accountProofData.rlpData).toHexString()
27 | } else {
28 | return accountProofData.rlpData.toHexString()
29 | }
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/Tests/FCLTests/FCLTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import FCL_SDK
3 |
4 | final class FCLTests: XCTestCase {
5 | func testExample() throws {
6 | // This is an example of a functional test case.
7 | // Use XCTAssert and related functions to verify your tests produce the correct
8 | // results.
9 | // XCTAssertEqual(FCL().text, "Hello, World!")
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/docs-asset/FCL-Swift.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocto/fcl-swift/3a90dcd96910ef9ad405e1e96b2447b3ca3759b5/docs-asset/FCL-Swift.jpg
--------------------------------------------------------------------------------
/docs-asset/wallet-discovery.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocto/fcl-swift/3a90dcd96910ef9ad405e1e96b2447b3ca3759b5/docs-asset/wallet-discovery.png
--------------------------------------------------------------------------------
/docs-asset/xcode-build-target.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blocto/fcl-swift/3a90dcd96910ef9ad405e1e96b2447b3ca3759b5/docs-asset/xcode-build-target.png
--------------------------------------------------------------------------------
/scripts/bump-publish-version.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | if pod spec lint FCL-SDK.podspec
4 | then
5 | echo "lint suceess"
6 | else
7 | echo "failed"
8 | exit 0
9 | fi
10 |
11 | BUMPED_VERSION="$1"
12 |
13 | sed -i '' -e 's/s.version \= [^\;]*/s.version = '"'$BUMPED_VERSION'"'/' FCL-SDK.podspec
14 |
15 | # docs
16 | sed -i '' -e 's/pod '\''FCL-SDK'\'', '\''~> [^\;]*'\''/pod '\''FCL-SDK'\'', '\''~> '$BUMPED_VERSION''\''/' README.md
17 | sed -i '' -e 's/.package(url: "https:\/\/github.com\/portto\/fcl-swift.git", .upToNextMinor(from: [^\;]*))/.package(url: "https:\/\/github.com\/portto\/fcl-swift.git", .upToNextMinor(from: "'$BUMPED_VERSION'"))/' README.md
18 |
19 | # commit all changes
20 | git add README.md FCL-SDK.podspec
21 | git commit -m "Bump version"
22 | git push origin main
23 |
24 | # add tag and push to remote
25 | git tag $BUMPED_VERSION
26 | git push origin $BUMPED_VERSION
27 |
28 | # publish cocoapods
29 | pod trunk push FCL-SDK.podspec --allow-warnings
--------------------------------------------------------------------------------