├── .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 | ![FCL Swift](./docs-asset/FCL-Swift.jpg) 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 --------------------------------------------------------------------------------