├── .github └── ISSUE_TEMPLATE │ └── BOUNTY.yml ├── .gitignore ├── .travis.yml ├── Example ├── AccountViewController.swift ├── Default-568h@2x.png ├── Podfile ├── Podfile.lock ├── Tests │ ├── AccessKeySpec.swift │ ├── AccountSpec.swift │ ├── EnviromentConfig.swift │ ├── FormatSpec.swift │ ├── Info.plist │ ├── KeyPairSpec.swift │ ├── KeystoreSpec.swift │ ├── MergeKeyStoreSpec.swift │ ├── PromisesSpec.swift │ ├── ProviderSpec.swift │ ├── SerializeSpec.swift │ ├── SignerSpec.swift │ ├── TestUtils.swift │ ├── Transaction.json │ ├── WalletAccountSpec.swift │ ├── Wasm.swift │ └── main.wasm ├── nearclientios.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── nearclientios-Example.xcscheme ├── nearclientios.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── nearclientios │ ├── AppDelegate.swift │ ├── Base.lproj │ ├── LaunchScreen.xib │ └── Main.storyboard │ ├── Images.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json │ ├── Info.plist │ └── WelcomeViewController.swift ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── _Pods.xcodeproj ├── nearclientios.h ├── nearclientios.podspec ├── nearclientios.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist └── nearclientios ├── Assets └── .gitkeep └── Sources ├── Account.swift ├── AccountCreator.swift ├── Connection.swift ├── Contract.swift ├── DefaultAuthService.swift ├── KeyStores ├── FileSystemKeyStore.swift ├── InMemoryKeyStore.swift ├── KeyStore.swift ├── KeychainKeyStore.swift ├── MergeKeyStore.swift └── SecureEnclaveKeyStore.swift ├── Near.swift ├── Providers ├── JSONRPCProvider.swift └── Provider.swift ├── Signer.swift ├── Transaction.swift ├── Utils ├── AppInfo.swift ├── Borsh │ ├── BinaryReader.swift │ ├── Borsh.swift │ ├── BorshDecoder.swift │ ├── BorshDeserialize.swift │ ├── BorshEncoder.swift │ └── BorshSerialize.swift ├── FileManager.swift ├── FixedLengthByteArray.swift ├── Format.swift ├── Int2x │ ├── Int2X.swift │ └── UInt2X.swift ├── KeyPair.swift ├── Network.swift ├── SHA256.swift ├── Serialize.swift ├── URL+Params.swift └── Web.swift └── WalletAccount.swift /.github/ISSUE_TEMPLATE/BOUNTY.yml: -------------------------------------------------------------------------------- 1 | name: "Simple Bounty" 2 | description: "Use this template to create a HEROES Simple Bounty via Github bot" 3 | title: "Bounty: " 4 | labels: ["bounty"] 5 | assignees: heroes-bot-test 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | Hi! Let's set up your bounty! Please don't change the template - @heroes-bot-test won't be able to help you. 11 | 12 | - type: dropdown 13 | id: type 14 | attributes: 15 | label: What talent are you looking for? 16 | options: 17 | - Marketing 18 | - Development 19 | - Design 20 | - Other 21 | - Content 22 | - Research 23 | - Audit 24 | 25 | - type: textarea 26 | id: description 27 | attributes: 28 | label: What you need to be done? 29 | 30 | - type: dropdown 31 | id: tags 32 | attributes: 33 | label: Tags 34 | description: Add tags that match the topic of the work 35 | multiple: true 36 | options: 37 | - API 38 | - Blockchain 39 | - Community 40 | - CSS 41 | - DAO 42 | - dApp 43 | - DeFi 44 | - Design 45 | - Documentation 46 | - HTML 47 | - Javascript 48 | - NFT 49 | - React 50 | - Rust 51 | - Smart contract 52 | - Typescript 53 | - UI/UX 54 | - web3 55 | - Translation 56 | - Illustration 57 | - Branding 58 | - Copywriting 59 | - Blogging 60 | - Editing 61 | - Video Creation 62 | - Social Media 63 | - Graphic Design 64 | - Transcription 65 | - Product Design 66 | - Artificial Intelligence 67 | - Quality Assurance 68 | - Risk Assessment 69 | - Security Audit 70 | - Bug Bounty 71 | - Code Review 72 | - Blockchain Security 73 | - Smart Contract Testing 74 | - Penetration Testing 75 | - Vulnerability Assessment 76 | - BOS 77 | - News 78 | - Hackathon 79 | - NEARCON2023 80 | - NEARWEEK 81 | 82 | - type: input 83 | id: deadline 84 | attributes: 85 | label: Deadline 86 | description: "Set a deadline for your bounty. Please enter the date in format: DD.MM.YYYY" 87 | placeholder: "19.05.2027" 88 | 89 | - type: dropdown 90 | id: currencyType 91 | attributes: 92 | label: Currency 93 | description: What is the currency you want to pay? 94 | options: 95 | - USDC.e 96 | - USDT.e 97 | - DAI 98 | - wNEAR 99 | - USDt 100 | - XP 101 | - marmaj 102 | - NEKO 103 | - JUMP 104 | - USDC 105 | - NEARVIDIA 106 | default: 0 107 | validations: 108 | required: true 109 | 110 | - type: input 111 | id: currencyAmount 112 | attributes: 113 | label: Amount 114 | description: How much it will be cost? 115 | 116 | - type: markdown 117 | attributes: 118 | value: "## Advanced settings" 119 | 120 | - type: checkboxes 121 | id: kyc 122 | attributes: 123 | label: KYC 124 | description: "Use HEROES' KYC Verification, only applicants who passed HEROES' KYC can apply and work on this bounty!" 125 | options: 126 | - label: Use KYC Verification 127 | 128 | - type: markdown 129 | attributes: 130 | value: | 131 | ### This cannot be changed once the bounty is live! 132 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## macOS 2 | .DS_Store 3 | 4 | ## User settings 5 | xcuserdata/ 6 | 7 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 8 | *.xcscmblueprint 9 | *.xccheckout 10 | 11 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 12 | build/ 13 | DerivedData/ 14 | *.moved-aside 15 | *.pbxuser 16 | !default.pbxuser 17 | *.mode1v3 18 | !default.mode1v3 19 | *.mode2v3 20 | !default.mode2v3 21 | *.perspectivev3 22 | !default.perspectivev3 23 | 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | 27 | ## App packaging 28 | *.ipa 29 | *.dSYM.zip 30 | *.dSYM 31 | 32 | ## Playgrounds 33 | timeline.xctimeline 34 | playground.xcworkspace 35 | 36 | .build/ 37 | 38 | # CocoaPods 39 | # 40 | # We recommend against adding the Pods directory to your .gitignore. However 41 | # you should judge for yourself, the pros and cons are mentioned at: 42 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 43 | # 44 | Pods/ 45 | # 46 | # Add this line if you want to avoid checking in source code from the Xcode workspace 47 | # *.xcworkspace 48 | 49 | # Carthage 50 | # 51 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 52 | # Carthage/Checkouts 53 | 54 | Carthage/Build/ 55 | 56 | # Accio dependency management 57 | Dependencies/ 58 | .accio/ 59 | 60 | # fastlane 61 | # 62 | # It is recommended to not store the screenshots in the git repo. 63 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 64 | # For more information about the recommended setup visit: 65 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 66 | 67 | fastlane/report.xml 68 | fastlane/Preview.html 69 | fastlane/screenshots/**/*.png 70 | fastlane/test_output 71 | 72 | # Code Injection 73 | # 74 | # After new code Injection tools there's a generated folder /iOSInjectionProject 75 | # https://github.com/johnno1962/injectionforxcode 76 | 77 | iOSInjectionProject/ 78 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # references: 2 | # * https://www.objc.io/issues/6-build-tools/travis-ci/ 3 | # * https://github.com/supermarin/xcpretty#usage 4 | 5 | osx_image: xcode11.1 6 | language: swift 7 | cache: cocoapods 8 | podfile: Example/Podfile 9 | before_install: 10 | - gem install cocoapods 11 | - pod install --project-directory=Example 12 | script: 13 | - set -o pipefail && xcodebuild test -enableCodeCoverage YES -workspace Example/nearclientios.xcworkspace -scheme nearclientios-Example -sdk iphonesimulator13.1 -destination 'platform=iOS Simulator,name=iPhone 8,OS=13.1' ONLY_ACTIVE_ARCH=NO | xcpretty 14 | - pod lib lint 15 | -------------------------------------------------------------------------------- /Example/AccountViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccountViewController.swift 3 | // nearclientios_Example 4 | // 5 | // Created by Viktor Siruk on 06.02.2020. 6 | // Copyright © 2020 CocoaPods. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import nearclientios 11 | 12 | struct AccountStateField { 13 | let title: String 14 | let value: String 15 | } 16 | 17 | class AccountViewController: UITableViewController { 18 | 19 | private var walletAccount: WalletAccount? 20 | private var near: Near? 21 | private var accountState: AccountState? 22 | private var data = [AccountStateField]() 23 | 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | title = "Near account" 27 | setupSignOutButton() 28 | Task { 29 | accountState = try await fetchAccountState() 30 | await setupData(with: accountState!) 31 | } 32 | // Do any additional setup after loading the view, typically from a nib. 33 | } 34 | 35 | override func didReceiveMemoryWarning() { 36 | super.didReceiveMemoryWarning() 37 | // Dispose of any resources that can be recreated. 38 | } 39 | } 40 | 41 | extension AccountViewController { 42 | private func setupSignOutButton() { 43 | navigationItem.hidesBackButton = true 44 | let newBackButton = UIBarButtonItem(title: "Sign out", style: UIBarButtonItem.Style.plain, target: self, action: #selector(AccountViewController.back(sender:))) 45 | self.navigationItem.leftBarButtonItem = newBackButton 46 | } 47 | 48 | @objc private func back(sender: UIBarButtonItem) { 49 | Task { 50 | await walletAccount?.signOut() 51 | navigationController?.popViewController(animated: true) 52 | } 53 | } 54 | } 55 | 56 | enum AccountError: Error { 57 | case cannotFetchAccountState 58 | } 59 | 60 | extension AccountViewController { 61 | func setup(near: Near, wallet: WalletAccount) { 62 | self.near = near 63 | walletAccount = wallet 64 | } 65 | 66 | private func fetchAccountState() async throws -> AccountState { 67 | do { 68 | let account = try await near!.account(accountId: walletAccount!.getAccountId()) 69 | let state = try await account.state() 70 | return state 71 | } catch { 72 | throw AccountError.cannotFetchAccountState 73 | } 74 | } 75 | 76 | private func setupData(with accountState: AccountState) async { 77 | data.append(AccountStateField(title: "Account ID", value: await walletAccount!.getAccountId())) 78 | let account = try! await near!.account(accountId: walletAccount!.getAccountId()) 79 | let accountBalance = try! await account.getAccountBalance() 80 | let balance = "\(accountBalance.available.toNearAmount(fracDigits: 5)) Ⓝ" 81 | data.append(AccountStateField(title: "Balance", value: balance)) 82 | data.append(AccountStateField(title: "Storage (used/paid)", value: "\(accountState.storageUsage.toStorageUnit())/\(accountState.storagePaidAt.toStorageUnit())")) 83 | await MainActor.run { 84 | tableView.reloadData() 85 | } 86 | } 87 | } 88 | 89 | extension AccountViewController { 90 | override func numberOfSections(in tableView: UITableView) -> Int { 91 | return 1 92 | } 93 | 94 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 95 | return data.count 96 | } 97 | 98 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 99 | var cell = tableView.dequeueReusableCell(withIdentifier: "cellIdentifier") 100 | if cell == nil { 101 | cell = UITableViewCell(style: .value1, reuseIdentifier: "cellIdentifier") 102 | } 103 | let field = data[indexPath.row] 104 | cell!.textLabel?.text = field.title 105 | cell!.detailTextLabel?.text = field.value 106 | return cell! 107 | } 108 | } 109 | 110 | extension Int { 111 | func toStorageUnit() -> String { 112 | let formatter = NumberFormatter() 113 | formatter.numberStyle = .decimal 114 | return formatter.string(from: NSNumber(value: self))! 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Example/Default-568h@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/near/near-api-swift/95a9c118065f072aba2fa92c476a8cb53b1559a6/Example/Default-568h@2x.png -------------------------------------------------------------------------------- /Example/Podfile: -------------------------------------------------------------------------------- 1 | use_frameworks! 2 | 3 | target 'nearclientios_Example' do 4 | pod 'nearclientios', :path => '../' 5 | 6 | target 'nearclientios_Tests' do 7 | inherit! :search_paths 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /Example/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - AnyCodable-FlightSchool (0.6.3) 3 | - Base58Swift (2.1.10): 4 | - BigInt (~> 5.0.0) 5 | - BigInt (5.0.0) 6 | - KeychainAccess (4.2.2) 7 | - nearclientios (1.0.29): 8 | - AnyCodable-FlightSchool (~> 0.6.0) 9 | - Base58Swift (~> 2.1.10) 10 | - KeychainAccess (~> 4.2.2) 11 | - secp256k1.swift 12 | - TweetNacl (~> 1.0) 13 | - secp256k1.swift (0.1.4) 14 | - TweetNacl (1.0.2) 15 | 16 | DEPENDENCIES: 17 | - nearclientios (from `../`) 18 | 19 | SPEC REPOS: 20 | trunk: 21 | - AnyCodable-FlightSchool 22 | - Base58Swift 23 | - BigInt 24 | - KeychainAccess 25 | - secp256k1.swift 26 | - TweetNacl 27 | 28 | EXTERNAL SOURCES: 29 | nearclientios: 30 | :path: "../" 31 | 32 | SPEC CHECKSUMS: 33 | AnyCodable-FlightSchool: 1c9890be5884140c60da5fde1b66380dec4c244a 34 | Base58Swift: 53d551f0b33d9478fa63b3445e453a772d6b31a7 35 | BigInt: 74b4d88367b0e819d9f77393549226d36faeb0d8 36 | KeychainAccess: c0c4f7f38f6fc7bbe58f5702e25f7bd2f65abf51 37 | nearclientios: 33b7eb8dd9305cd2ca8f95864c9402519b2c1ee5 38 | secp256k1.swift: a7e7a214f6db6ce5db32cc6b2b45e5c4dd633634 39 | TweetNacl: 3abf4d1d2082b0114e7a67410e300892448951e6 40 | 41 | PODFILE CHECKSUM: bf2ede311928de0026e7155c3c779a7c3a7fed9d 42 | 43 | COCOAPODS: 1.11.2 44 | -------------------------------------------------------------------------------- /Example/Tests/AccessKeySpec.swift: -------------------------------------------------------------------------------- 1 | //// 2 | //// AccessKeySpec.swift 3 | //// nearclientios_Tests 4 | //// 5 | //// Created by Dmytro Kurochka on 02.12.2019. 6 | //// Copyright © 2019 CocoaPods. All rights reserved. 7 | //// 8 | // 9 | import XCTest 10 | 11 | @testable import nearclientios 12 | class AccessKeySpec: XCTestCase { 13 | 14 | static var near: Near! 15 | static var testAccount: Account! 16 | 17 | override class func setUp() { 18 | super.setUp() 19 | unsafeWaitFor { 20 | try! await setUpAll() 21 | } 22 | } 23 | 24 | class func setUpAll() async throws { 25 | near = try await TestUtils.setUpTestConnection() 26 | let masterAccount = try await self.near.account(accountId: testAccountName) 27 | let amount = INITIAL_BALANCE * UInt128(100) 28 | testAccount = try await TestUtils.createAccount(masterAccount: masterAccount, amount: amount) 29 | } 30 | 31 | var workingAccount: Account! 32 | var contractId: String! 33 | var contract: Contract! 34 | 35 | override func setUp() async throws { 36 | self.contractId = TestUtils.generateUniqueString(prefix: "test") 37 | self.workingAccount = try await TestUtils.createAccount(masterAccount: AccessKeySpec.testAccount) 38 | self.contract = try await TestUtils.deployContract(workingAccount: self.workingAccount, 39 | contractId: self.contractId) 40 | } 41 | 42 | func testMakeFunctionCallsUsingAcccessKey() async throws { 43 | let keyPair = try keyPairFromRandom() 44 | let publicKey = keyPair.getPublicKey() 45 | try await self.workingAccount.addKey(publicKey: publicKey, 46 | contractId: self.contractId, 47 | methodName: "", 48 | amount: UInt128(stringLiteral: "2000000000000000000000000")) 49 | // Override in the key store the workingAccount key to the given access key. 50 | let signer = AccessKeySpec.near.connection.signer as! InMemorySigner 51 | try await signer.keyStore.setKey(networkId: networkId, 52 | accountId: self.workingAccount.accountId, 53 | keyPair: keyPair) 54 | let setCallValue = TestUtils.generateUniqueString(prefix: "setCallPrefix") 55 | try await self.contract.change(methodName: .setValue, args: ["value": setCallValue]) 56 | let testValue: String = try await self.contract.view(methodName: .getValue) 57 | XCTAssertEqual(testValue, setCallValue) 58 | } 59 | 60 | func testMakeFunctionCallsUsingSepc256k1AcccessKey() async throws { 61 | let keyPair = try keyPairFromRandom(curve: .SECP256k1) 62 | let publicKey = keyPair.getPublicKey() 63 | try await self.workingAccount.addKey(publicKey: publicKey, 64 | contractId: self.contractId, 65 | methodName: "", 66 | amount: UInt128(stringLiteral: "2000000000000000000000000")) 67 | // Override in the key store the workingAccount key to the given access key. 68 | let signer = AccessKeySpec.near.connection.signer as! InMemorySigner 69 | try await signer.keyStore.setKey(networkId: networkId, 70 | accountId: self.workingAccount.accountId, 71 | keyPair: keyPair) 72 | let setCallValue = TestUtils.generateUniqueString(prefix: "setCallPrefix") 73 | try await self.contract.change(methodName: .setValue, args: ["value": setCallValue]) 74 | let testValue: String = try await self.contract.view(methodName: .getValue) 75 | XCTAssertEqual(testValue, setCallValue) 76 | } 77 | 78 | func testRemoveAccessKeyNoLongerWorks() async throws { 79 | let keyPair = try keyPairFromRandom() 80 | let publicKey = keyPair.getPublicKey() 81 | try await self.workingAccount.addKey(publicKey: publicKey, 82 | contractId: self.contractId, 83 | methodName: "", 84 | amount: UInt128(400000)) 85 | try await self.workingAccount.deleteKey(publicKey: publicKey) 86 | // Override in the key store the workingAccount key to the given access key. 87 | let signer = AccessKeySpec.near.connection.signer as! InMemorySigner 88 | try await signer.keyStore.setKey(networkId: networkId, 89 | accountId: self.workingAccount.accountId, 90 | keyPair: keyPair) 91 | 92 | await AssertThrowsError(try await self.contract.change(methodName: .setValue, args: ["value": "test"]) as Any) { error in 93 | XCTAssertTrue(error is RPCError) 94 | } 95 | } 96 | 97 | func testViewAccountDetailsAfterAddingAccessKeys() async throws { 98 | let keyPair = try keyPairFromRandom() 99 | try await self.workingAccount.addKey(publicKey: keyPair.getPublicKey(), 100 | contractId: self.contractId, 101 | methodName: "", 102 | amount: UInt128(1000000000)) 103 | let contract2 = try await TestUtils.deployContract(workingAccount: self.workingAccount, 104 | contractId: TestUtils.generateUniqueString(prefix: "test_contract2")) 105 | let keyPair2 = try keyPairFromRandom() 106 | try await self.workingAccount.addKey(publicKey: keyPair2.getPublicKey(), 107 | contractId: contract2.contractId, 108 | methodName: "", 109 | amount: UInt128(2000000000)) 110 | let details = try await self.workingAccount.getAccountDetails() 111 | let expectedResult: [AuthorizedApp] = [AuthorizedApp(contractId: self.contractId, 112 | amount: UInt128(1000000000), 113 | publicKey: keyPair.getPublicKey().toString()), 114 | AuthorizedApp(contractId: contract2.contractId, 115 | amount: UInt128(2000000000), 116 | publicKey: keyPair2.getPublicKey().toString())] 117 | 118 | XCTAssertTrue(details.authorizedApps.contains(expectedResult[0])) 119 | XCTAssertTrue(details.authorizedApps.contains(expectedResult[1])) 120 | } 121 | 122 | func testLoadingAccountAfterAddingAFullKey() async throws { 123 | let keyPair = try keyPairFromRandom() 124 | // wallet calls this with an empty string for contract id and method 125 | try await self.workingAccount.addKey(publicKey: keyPair.getPublicKey(), 126 | contractId: "", 127 | methodName: "", 128 | amount: nil) 129 | let accessKeys = try await self.workingAccount.getAccessKeys() 130 | XCTAssertEqual(accessKeys.keys.count, 2) 131 | let addedKey = accessKeys.keys.first(where: {$0.publicKey == keyPair.getPublicKey().toString()}) 132 | XCTAssertNotNil(addedKey) 133 | if case AccessKeyPermission.fullAccess(let permission) = addedKey!.accessKey.permission { 134 | XCTAssertEqual(permission, FullAccessPermission()) 135 | } else { 136 | XCTFail("AccessKeyPermission in not FullAccess") 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /Example/Tests/AccountSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccountSpec.swift 3 | // nearclientios_Tests 4 | // 5 | // Created by Dmytro Kurochka on 28.11.2019. 6 | // Copyright © 2019 CocoaPods. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | @testable import nearclientios 12 | class AccountSpec: XCTestCase { 13 | static var near: Near! 14 | static var workingAccount: Account! 15 | 16 | static let contractId = TestUtils.generateUniqueString(prefix: "test_contract") 17 | static var contract: Contract! 18 | 19 | override class func setUp() { 20 | super.setUp() 21 | unsafeWaitFor { 22 | do { 23 | try await setUpAll() 24 | } catch let error { 25 | print(error) 26 | } 27 | } 28 | } 29 | 30 | class func setUpAll() async throws { 31 | // Account setup 32 | near = try await TestUtils.setUpTestConnection() 33 | let masterAccount = try await near.account(accountId: testAccountName) 34 | let amount = INITIAL_BALANCE 35 | workingAccount = try await TestUtils.createAccount(masterAccount: masterAccount, amount: amount) 36 | 37 | // Contract setup 38 | let newPublicKey = try await near.connection.signer.createKey(accountId: contractId, networkId: networkId, curve: .ED25519) 39 | let data = Wasm().data 40 | try await workingAccount.createAndDeployContract(contractId: contractId, publicKey: newPublicKey, data: data.bytes, amount: HELLO_WASM_BALANCE) 41 | let options = ContractOptions(viewMethods: [.hello, .getValue, .getAllKeys, .returnHiWithLogs], changeMethods: [.setValue, .generateLogs, .triggerAssert, .testSetRemove], sender: nil) 42 | contract = Contract(account: workingAccount, contractId: contractId, options: options) 43 | } 44 | 45 | func testViewPredefinedAccountWithCorrectName() async throws { 46 | let status = try await AccountSpec.workingAccount.state() 47 | XCTAssertEqual(status.codeHash, "11111111111111111111111111111111") 48 | } 49 | 50 | func testCreateAccountAndViewNewAccount() async throws { 51 | let newAccountName = TestUtils.generateUniqueString(prefix: "test") 52 | let newAccountPublicKey = try PublicKey.fromString(encodedKey: "9AhWenZ3JddamBoyMqnTbp7yVbRuvqAv3zwfrWgfVRJE") 53 | let workingState = try await AccountSpec.workingAccount.state() 54 | let amount = workingState.amount 55 | let newAmount = UInt128(stringLiteral: amount) / UInt128(100) 56 | try await AccountSpec.workingAccount.createAccount(newAccountId: newAccountName, publicKey: newAccountPublicKey, amount: newAmount) 57 | let newAccount = Account(connection: AccountSpec.near.connection, accountId: newAccountName) 58 | let state = try await newAccount.state() 59 | XCTAssertEqual(state.amount, "\(newAmount)") 60 | } 61 | 62 | func testCreateAccountAndViewNewAccountUsingSecp256k1Curve() async throws { 63 | let newAccountName = TestUtils.generateUniqueString(prefix: "test") 64 | let newAccountPublicKey = try PublicKey.fromString(encodedKey: "secp256k1:45KcWwYt6MYRnnWFSxyQVkuu9suAzxoSkUMEnFNBi9kDayTo5YPUaqMWUrf7YHUDNMMj3w75vKuvfAMgfiFXBy28") 65 | let workingState = try await AccountSpec.workingAccount.state() 66 | let amount = workingState.amount 67 | let newAmount = UInt128(stringLiteral: amount) / UInt128(100) 68 | try await AccountSpec.workingAccount.createAccount(newAccountId: newAccountName, publicKey: newAccountPublicKey, amount: newAmount) 69 | let newAccount = Account(connection: AccountSpec.near.connection, accountId: newAccountName) 70 | let state = try await newAccount.state() 71 | XCTAssertEqual(state.amount, "\(newAmount)") 72 | } 73 | 74 | func testSendMoney() async throws { 75 | let workingState = try await AccountSpec.workingAccount.state() 76 | let amountFraction = UInt128(stringLiteral: workingState.amount) / UInt128(100) 77 | let sender = try await TestUtils.createAccount(masterAccount: AccountSpec.workingAccount, amount: amountFraction) 78 | let receiver = try await TestUtils.createAccount(masterAccount: AccountSpec.workingAccount, amount: amountFraction) 79 | try await sender.sendMoney(receiverId: receiver.accountId, amount: UInt128(10000)) 80 | try await receiver.fetchState() 81 | let state = try await receiver.state() 82 | XCTAssertEqual(state.amount, "\(amountFraction + UInt128(10000))") 83 | } 84 | 85 | func testDeleteAccount() async throws { 86 | let workingState = try await AccountSpec.workingAccount.state() 87 | let amountFraction = UInt128(stringLiteral: workingState.amount) / UInt128(100) 88 | let sender = try await TestUtils.createAccount(masterAccount: AccountSpec.workingAccount, amount: amountFraction) 89 | let receiver = try await TestUtils.createAccount(masterAccount: AccountSpec.workingAccount, amount: amountFraction) 90 | try await sender.deleteAccount(beneficiaryId: receiver.accountId) 91 | try await receiver.fetchState() 92 | let senderState = try await receiver.state() 93 | XCTAssertGreaterThan(UInt128(stringLiteral: senderState.amount), amountFraction) 94 | 95 | let reloaded = Account(connection: sender.connection, accountId: sender.accountId) 96 | await AssertThrowsError(try await reloaded.state()) { error in 97 | XCTAssertTrue(error is HTTPError) 98 | } 99 | } 100 | 101 | // Errors 102 | func testCreatingAnExistingAccountShouldThrow() async throws { 103 | await AssertThrowsError(try await AccountSpec.workingAccount.createAccount(newAccountId: AccountSpec.workingAccount.accountId, publicKey: PublicKey.fromString(encodedKey: "9AhWenZ3JddamBoyMqnTbp7yVbRuvqAv3zwfrWgfVRJE"), amount: 100)) { error in 104 | XCTAssertTrue(error is TypedError) 105 | } 106 | } 107 | 108 | // With deploy contract 109 | func testMakeFunctionCallsViaAccount() async throws { 110 | let result: String = try await AccountSpec.workingAccount.viewFunction(contractId: AccountSpec.contractId, methodName: "hello", args: ["name": "trex"]) 111 | XCTAssertEqual(result, "hello trex") 112 | 113 | let setCallValue = TestUtils.generateUniqueString(prefix: "setCallPrefix") 114 | let result2 = try await AccountSpec.workingAccount.functionCall(contractId: AccountSpec.contractId, methodName: "setValue", args: ["value": setCallValue], amount: 0) 115 | XCTAssertEqual(getTransactionLastResult(txResult: result2) as? String, setCallValue) 116 | 117 | let testSetCallValue: String = try await AccountSpec.workingAccount.viewFunction(contractId: AccountSpec.contractId, methodName: "getValue", args: [:]) 118 | XCTAssertEqual(testSetCallValue, setCallValue) 119 | } 120 | 121 | func testMakeFunctionCallsViaAccountWithGas() async throws { 122 | let result: String = try await AccountSpec.contract.view(methodName: .hello, args: ["name": "trex"]) 123 | XCTAssertEqual(result, "hello trex") 124 | 125 | let setCallValue = TestUtils.generateUniqueString(prefix: "setCallPrefix") 126 | let result2 = try await AccountSpec.contract.change(methodName: .setValue, args: ["value": setCallValue], gas: 1000000 * 1000000) as? String 127 | XCTAssertEqual(result2, setCallValue) 128 | 129 | let testSetCallValue: String = try await AccountSpec.contract.view(methodName: .getValue) 130 | XCTAssertEqual(testSetCallValue, setCallValue) 131 | } 132 | 133 | // func testShouldGetLogsFromMethodResult() async throws { 134 | // let logs = try await contract.change(methodName: .generateLogs) 135 | // expect(logs).to(equal([`[${contractId}]: LOG: log1`, `[${contractId}]: LOG: log2`]))] 136 | // } 137 | 138 | func testCanGetLogsFromViewCall() async throws { 139 | let result: String = try await AccountSpec.contract.view(methodName: .returnHiWithLogs) 140 | XCTAssertEqual(result, "Hi") 141 | //expect(logs).toEqual([`[${contractId}]: LOG: loooog1`, `[${contractId}]: LOG: loooog2`]); 142 | } 143 | 144 | func testCanGetAssertMessageFromMethodResult() async throws { 145 | await AssertThrowsError(try await AccountSpec.contract.change(methodName: .triggerAssert) as Any) { error in 146 | XCTAssertTrue(error is TypedError) 147 | // This method in the testing contract is just designed to test logging after failure. 148 | //expect(logs[0]).toEqual(`[${contractId}]: LOG: log before assert`) 149 | //expect(logs[1]).toMatch(new RegExp(`^\\[${contractId}\\]: ABORT: "?expected to fail"?,? 150 | } 151 | } 152 | 153 | func testAttemptSetRemove() async throws { 154 | do { 155 | try await AccountSpec.contract.change(methodName: .testSetRemove, args: ["value": "123"]) 156 | } catch let error { 157 | XCTFail(error.localizedDescription) 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /Example/Tests/EnviromentConfig.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EnviromentConfig.swift 3 | // nearclientios_Tests 4 | // 5 | // Created by Dmytro Kurochka on 27.11.2019. 6 | // Copyright © 2019 CocoaPods. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | internal enum Environment: String { 12 | case production, development, local, test, testRemote = "test-remote", ci, ciStaging = "ci-staging" 13 | } 14 | 15 | internal struct EnvironmentConfig { 16 | let networkId: String 17 | let nodeUrl: URL 18 | let masterAccount: String 19 | } 20 | 21 | func getConfig(env: Environment) -> EnvironmentConfig { 22 | switch env { 23 | case .production, .development: 24 | return EnvironmentConfig(networkId: "default", 25 | nodeUrl: URL(string: "https://rpc.mainnet.near.org")!, 26 | masterAccount: "test.near") 27 | case .local: 28 | //process.env.HOME ? 29 | // "masterAccount": "\(process.env.HOME)/.near/validator_key.json"] 30 | return EnvironmentConfig(networkId: "local", 31 | nodeUrl: URL(string: "http://localhost:3030")!, 32 | masterAccount: "test.near") 33 | case .test: 34 | return EnvironmentConfig(networkId: "local", 35 | nodeUrl: URL(string: "http://localhost:3030")!, 36 | masterAccount: "test.near") 37 | case .testRemote, .ci: 38 | return EnvironmentConfig(networkId: "shared-test", 39 | nodeUrl: URL(string: "https://rpc.ci-testnet.near.org")!, 40 | masterAccount: "test.near") 41 | case .ciStaging: 42 | return EnvironmentConfig(networkId: "shared-test-staging", 43 | nodeUrl: URL(string: "http://staging-shared-test.nearprotocol.com:3030")!, 44 | masterAccount: "test.near") 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Example/Tests/FormatSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FormatSpec.swift 3 | // nearclientios_Tests 4 | // 5 | // Created by Kevin McConnaughay on 3/22/22. 6 | // Copyright © 2022 CocoaPods. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import nearclientios 11 | 12 | struct YNTestCase { 13 | let balance: String 14 | let fracDigits: Int? 15 | let expected: String 16 | let expectedDouble: Double 17 | } 18 | 19 | struct NYTestCase { 20 | let amount: String 21 | let expected: String 22 | } 23 | 24 | let yoctoToNearStringTestCases: [YNTestCase] = [ 25 | YNTestCase(balance: "8999999999837087887", fracDigits: nil, expected: "0.000008999999999837087887", expectedDouble: 0.000008999999999837087887), 26 | YNTestCase(balance: "8099099999837087887", fracDigits: nil, expected: "0.000008099099999837087887", expectedDouble: 0.000008099099999837087887), 27 | YNTestCase(balance: "999998999999999837087887000", fracDigits: nil, expected: "999.998999999999837087887", expectedDouble: 999.998999999999837087887), 28 | YNTestCase(balance: "1" + String(repeating: "0", count: 13), fracDigits: nil, expected: "0.00000000001", expectedDouble: 0.00000000001), 29 | YNTestCase(balance: "9999989999999998370878870000000", fracDigits: nil, expected: "9,999,989.99999999837087887", expectedDouble: 9999989.99999999837087887), 30 | YNTestCase(balance: "000000000000000000000000", fracDigits: nil, expected: "0", expectedDouble: 0), 31 | YNTestCase(balance: "1000000000000000000000000", fracDigits: nil, expected: "1", expectedDouble: 1), 32 | YNTestCase(balance: "999999999999999999000000", fracDigits: nil, expected: "0.999999999999999999", expectedDouble: 0.999999999999999999), 33 | YNTestCase(balance: "999999999999999999000000", fracDigits: 10, expected: "1", expectedDouble: 1), 34 | YNTestCase(balance: "1003000000000000000000000", fracDigits: 3, expected: "1.003", expectedDouble: 1.003), 35 | YNTestCase(balance: "3000000000000000000000", fracDigits: 3, expected: "0.003", expectedDouble: 0.003), 36 | YNTestCase(balance: "3000000000000000000000", fracDigits: 4, expected: "0.003", expectedDouble: 0.003), 37 | YNTestCase(balance: "3500000000000000000000", fracDigits: 3, expected: "0.004", expectedDouble: 0.004), 38 | YNTestCase(balance: "03500000000000000000000", fracDigits: 3, expected: "0.004", expectedDouble: 0.004), 39 | YNTestCase(balance: "10000000999999997410000000", fracDigits: nil, expected: "10.00000099999999741", expectedDouble: 10.00000099999999741), 40 | YNTestCase(balance: "10100000999999997410000000", fracDigits: nil, expected: "10.10000099999999741", expectedDouble: 10.10000099999999741), 41 | YNTestCase(balance: "10040000999999997410000000", fracDigits: 2, expected: "10.04", expectedDouble: 10.04), 42 | YNTestCase(balance: "10999000999999997410000000", fracDigits: 2, expected: "11", expectedDouble: 11), 43 | YNTestCase(balance: "1000000100000000000000000000000", fracDigits: nil, expected: "1,000,000.1", expectedDouble: 1000000.1), 44 | YNTestCase(balance: "1000100000000000000000000000000", fracDigits: nil, expected: "1,000,100", expectedDouble: 1000100), 45 | YNTestCase(balance: "910000000000000000000000", fracDigits: 0, expected: "1", expectedDouble: 1) 46 | ] 47 | 48 | let nearStringToYoctoStringTestCases: [NYTestCase] = [ 49 | NYTestCase(amount: "5.3", expected: "5300000000000000000000000"), 50 | NYTestCase(amount: "5", expected: "5000000000000000000000000"), 51 | NYTestCase(amount: "1", expected: "1000000000000000000000000"), 52 | NYTestCase(amount: "10", expected: "10000000000000000000000000"), 53 | NYTestCase(amount: "0.000008999999999837087887", expected: "8999999999837087887"), 54 | NYTestCase(amount: "0.000008099099999837087887", expected: "8099099999837087887"), 55 | NYTestCase(amount: "999.998999999999837087887000", expected: "999998999999999837087887000"), 56 | NYTestCase(amount: "0.000000000000001", expected: "1000000000"), 57 | NYTestCase(amount: "0", expected: "0"), 58 | NYTestCase(amount: "0.000", expected: "0"), 59 | NYTestCase(amount: "0.000001", expected: "1000000000000000000"), 60 | NYTestCase(amount: ".000001", expected: "1000000000000000000"), 61 | NYTestCase(amount: "000000.000001", expected: "1000000000000000000"), 62 | NYTestCase(amount: "1,000,000.1", expected: "1000000100000000000000000000000"), 63 | ] 64 | 65 | class FormatSpec: XCTestCase { 66 | func testFormatNearAmount() throws { 67 | for testCase in yoctoToNearStringTestCases { 68 | let balance = UInt128(stringLiteral: testCase.balance) 69 | let formatResult = balance.toNearAmount(fracDigits: testCase.fracDigits ?? NEAR_NOMINATION_EXP) 70 | let doubleResult = balance.toNearDouble(fracDigits: testCase.fracDigits ?? NEAR_NOMINATION_EXP) 71 | XCTAssertEqual(formatResult, testCase.expected) 72 | XCTAssertTrue(fabs(doubleResult - testCase.expectedDouble) < .ulpOfOne) 73 | } 74 | } 75 | 76 | func testFormatNearAmountFromString() throws { 77 | for testCase in yoctoToNearStringTestCases { 78 | let balance = testCase.balance 79 | let formatResult = testCase.fracDigits != nil ? balance.toNearAmount(fracDigits: testCase.fracDigits!) : balance.toNearAmount() 80 | XCTAssertEqual(formatResult, testCase.expected) 81 | } 82 | } 83 | 84 | func testFormatYoctoAmount() throws { 85 | for testCase in nearStringToYoctoStringTestCases { 86 | let formatResult = try testCase.amount.toYoctoNearString() 87 | XCTAssertEqual(formatResult, testCase.expected) 88 | } 89 | 90 | XCTAssertThrowsError(try "".toYoctoNearString()) 91 | XCTAssertThrowsError(try ".".toYoctoNearString()) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Example/Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | NSAppTransportSecurity 24 | 25 | NSAllowsArbitraryLoads 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Example/Tests/KeyPairSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyPairSpec.swift 3 | // nearclientios_Tests 4 | // 5 | // Created by Dmytro Kurochka on 27.11.2019. 6 | // Copyright © 2019 CocoaPods. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import nearclientios 11 | 12 | class KeyPairSpec: XCTestCase { 13 | 14 | func testSignAndVerify() { 15 | let keyPair = try! KeyPairEd25519(secretKey: "26x56YPzPDro5t2smQfGcYAPy3j7R2jB2NUb7xKbAGK23B6x4WNQPh3twb6oDksFov5X8ts5CtntUNbpQpAKFdbR") 16 | XCTAssertEqual(keyPair.getPublicKey().toString(), "ed25519:AYWv9RAN1hpSQA4p1DLhCNnpnNXwxhfH9qeHN8B4nJ59") 17 | let message = "message".data(using: .utf8)!.digest 18 | let signature = try! keyPair.sign(message: message) 19 | XCTAssertEqual(signature.signature.baseEncoded, "26gFr4xth7W9K7HPWAxq3BLsua8oTy378mC1MYFiEXHBBpeBjP8WmJEJo8XTBowetvqbRshcQEtBUdwQcAqDyP8T") 20 | } 21 | 22 | func testSignAndVerifyWithRandom() { 23 | let keyPair = try! KeyPairEd25519.fromRandom() 24 | let message = "message".data(using: .utf8)!.digest 25 | let signature = try! keyPair.sign(message: message) 26 | XCTAssertTrue(try! keyPair.verify(message: message, signature: signature.signature)) 27 | } 28 | 29 | func testSignAndVerifyWithSepc256k1Random() { 30 | let keyPair = try! KeyPairSecp256k1.fromRandom() 31 | let message = "message".data(using: .utf8)!.digest 32 | let signature = try! keyPair.sign(message: message) 33 | XCTAssertTrue(try! keyPair.verify(message: message, signature: signature.signature)) 34 | } 35 | 36 | func testSecp256k1InitFromSecret() { 37 | let keyPair = try! KeyPairSecp256k1(secretKey: "Cqmi5vHc59U1MHhq7JCxTSJentvVBYMcKGUA7s7kwnKn") 38 | XCTAssertEqual(keyPair.getPublicKey().toString(), "secp256k1:45KcWwYt6MYRnnWFSxyQVkuu9suAzxoSkUMEnFNBi9kDayTo5YPUaqMWUrf7YHUDNMMj3w75vKuvfAMgfiFXBy28") 39 | } 40 | 41 | func testInitFromSecret() { 42 | let keyPair = try! KeyPairEd25519(secretKey: "5JueXZhEEVqGVT5powZ5twyPP8wrap2K7RdAYGGdjBwiBdd7Hh6aQxMP1u3Ma9Yanq1nEv32EW7u8kUJsZ6f315C") 43 | XCTAssertEqual(keyPair.getPublicKey().toString(), "ed25519:EWrekY1deMND7N3Q7Dixxj12wD7AVjFRt2H9q21QHUSW") 44 | } 45 | 46 | func testConvertToString() { 47 | let keyPair = try! KeyPairEd25519.fromRandom() 48 | let newKeyPair = try! keyPairFromString(encodedKey: keyPair.toString()) as! KeyPairEd25519 49 | XCTAssertEqual(newKeyPair.getSecretKey(), keyPair.getSecretKey()) 50 | let keyString = "ed25519:2wyRcSwSuHtRVmkMCGjPwnzZmQLeXLzLLyED1NDMt4BjnKgQL6tF85yBx6Jr26D2dUNeC716RBoTxntVHsegogYw" 51 | let keyPair2 = try! keyPairFromString(encodedKey: keyString) 52 | XCTAssertEqual(keyPair2.toString(), keyString) 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /Example/Tests/KeystoreSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeystoreSpec.swift 3 | // nearclientios 4 | // 5 | // Created by Dmytro Kurochka on 26.11.2019. 6 | // Copyright © 2019 CocoaPods. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import nearclientios 11 | import LocalAuthentication 12 | 13 | let NETWORK_ID_SINGLE_KEY = "singlekeynetworkid" 14 | let ACCOUNT_ID_SINGLE_KEY = "singlekey_accountid" 15 | let secretKey = "2wyRcSwSuHtRVmkMCGjPwnzZmQLeXLzLLyED1NDMt4BjnKgQL6tF85yBx6Jr26D2dUNeC716RBoTxntVHsegogYw" 16 | let KEYPAIR_SINGLE_KEY = try! KeyPairEd25519(secretKey: secretKey) 17 | 18 | class KeyStoreSpec: XCTestCase { 19 | static var keyStores: [KeyStore] = [] 20 | override class func setUp() { 21 | keyStores.append(InMemoryKeyStore()) 22 | keyStores.append(UnencryptedFileSystemKeyStore(keyDir: "test-keys")) 23 | keyStores.append(KeychainKeyStore(keychain: .init(service: "test.keystore"))) 24 | keyStores.append(SecureEnclaveKeyStore(keychain: .init(service: "testEnclave.keystore"))) 25 | keyStores.append(MergeKeyStore(keyStores: [InMemoryKeyStore(), InMemoryKeyStore()])) 26 | } 27 | 28 | func withAllKeyStores(run: (_: KeyStore) async throws -> Void) async throws { 29 | for store in KeyStoreSpec.keyStores { 30 | try await run(store) 31 | } 32 | } 33 | 34 | override func setUp() async throws { 35 | try await withAllKeyStores(run: setUp(keyStore:)) 36 | } 37 | 38 | func setUp(keyStore: KeyStore) async throws { 39 | try! await(keyStore.setKey(networkId: NETWORK_ID_SINGLE_KEY, 40 | accountId: ACCOUNT_ID_SINGLE_KEY, 41 | keyPair: KEYPAIR_SINGLE_KEY)) 42 | } 43 | 44 | override func tearDown() async throws { 45 | try await withAllKeyStores(run: tearDown(keyStore:)) 46 | } 47 | 48 | func tearDown(keyStore: KeyStore) async throws { 49 | try! await(keyStore.clear()) 50 | } 51 | 52 | func testGetAllKeysWithEmptyNetworkReturnsEmptyList() async throws { 53 | try await withAllKeyStores(run: getAllKeysWithEmptyNetworkReturnsEmptyList) 54 | } 55 | func getAllKeysWithEmptyNetworkReturnsEmptyList(keyStore: KeyStore) async throws { 56 | let emptyList = try! await keyStore.getAccounts(networkId: "emptynetwork") 57 | XCTAssertEqual(emptyList.count, 0) 58 | } 59 | 60 | func testGetAllKeysWithSingleKeyInKeyStore() async throws { 61 | try await withAllKeyStores(run: getAllKeysWithSingleKeyInKeyStore) 62 | } 63 | func getAllKeysWithSingleKeyInKeyStore(keyStore: KeyStore) async throws { 64 | let accountIds = try! await keyStore.getAccounts(networkId: NETWORK_ID_SINGLE_KEY) 65 | XCTAssertEqual(accountIds, [ACCOUNT_ID_SINGLE_KEY]) 66 | } 67 | 68 | func testGetNonExistingAccount() async throws { 69 | try await withAllKeyStores(run: getNonExistingAccount) 70 | } 71 | func getNonExistingAccount(keyStore: KeyStore) async throws { 72 | let account = try! await(keyStore.getKey(networkId: "somenetwork", accountId: "someaccount")) 73 | XCTAssertNil(account) 74 | } 75 | 76 | func testGetAccountIdFromNetworkWithSingleKey() async throws { 77 | try await withAllKeyStores(run: getAccountIdFromNetworkWithSingleKey) 78 | } 79 | func getAccountIdFromNetworkWithSingleKey(keyStore: KeyStore) async throws { 80 | let key = try! await keyStore.getKey(networkId: NETWORK_ID_SINGLE_KEY, 81 | accountId: ACCOUNT_ID_SINGLE_KEY) as? KeyPairEd25519 82 | XCTAssertEqual(key, KEYPAIR_SINGLE_KEY) 83 | } 84 | 85 | func testGetNetworks() async throws { 86 | try await withAllKeyStores(run: getNetworks) 87 | } 88 | func getNetworks(keyStore: KeyStore) async throws { 89 | let networks = try! await keyStore.getNetworks() 90 | XCTAssertEqual(networks, [NETWORK_ID_SINGLE_KEY]) 91 | } 92 | 93 | func testAddTwoKeysToNetworkAndRetrieveThem() async throws { 94 | try await withAllKeyStores(run: addTwoKeysToNetworkAndRetrieveThem) 95 | } 96 | func addTwoKeysToNetworkAndRetrieveThem(keyStore: KeyStore) async throws { 97 | if keyStore is SecureEnclaveKeyStore && (keyStore as! SecureEnclaveKeyStore).requireUserPresence { 98 | (keyStore as! SecureEnclaveKeyStore).context = LAContext() 99 | } 100 | let networkId = "twoKeyNetwork" 101 | let accountId1 = "acc1" 102 | let accountId2 = "acc2" 103 | let key1Expected = try! keyPairFromRandom() as! KeyPairEd25519 104 | let key2Expected = try! keyPairFromRandom() as! KeyPairEd25519 105 | try! await keyStore.setKey(networkId: networkId, accountId: accountId1, keyPair: key1Expected) 106 | try! await keyStore.setKey(networkId: networkId, accountId: accountId2, keyPair: key2Expected) 107 | let key1 = try await keyStore.getKey(networkId: networkId, accountId: accountId1) as! KeyPairEd25519 108 | let key2 = try await keyStore.getKey(networkId: networkId, accountId: accountId2) as! KeyPairEd25519 109 | XCTAssertEqual(key1, key1Expected) 110 | XCTAssertEqual(key2, key2Expected) 111 | let accountIds = try! await keyStore.getAccounts(networkId: networkId) 112 | XCTAssertEqual(accountIds.sorted(), [accountId1, accountId2].sorted()) 113 | let networks = try! await(keyStore.getNetworks()) 114 | XCTAssertEqual(networks.sorted(), [NETWORK_ID_SINGLE_KEY, networkId].sorted()) 115 | } 116 | 117 | } 118 | 119 | -------------------------------------------------------------------------------- /Example/Tests/MergeKeyStoreSpec.swift: -------------------------------------------------------------------------------- 1 | 2 | // MergeKeyStoreSpec.swift 3 | // nearclientios_Tests 4 | // 5 | // Created by Dmytro Kurochka on 26.11.2019. 6 | // Copyright © 2019 CocoaPods. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import nearclientios 11 | 12 | class MergeKeyStoreSpec: XCTestCase { 13 | private let stores: [KeyStore] = [InMemoryKeyStore(), InMemoryKeyStore()] 14 | private lazy var keyStore: KeyStore! = MergeKeyStore(keyStores: stores) 15 | 16 | override func tearDown() async throws { 17 | try! await(self.keyStore.clear()) 18 | } 19 | 20 | func testLookUpKeyFromFallbackKeystoreIfNeeded() async throws { 21 | let key1 = try! keyPairFromRandom() as! KeyPairEd25519 22 | try! await self.stores[1].setKey(networkId: "network", accountId: "account", keyPair: key1) 23 | let key = try! await self.keyStore.getKey(networkId: "network", accountId: "account") as! KeyPairEd25519 24 | XCTAssertEqual(key, key1) 25 | } 26 | 27 | func testKeyLookupOrder() async throws { 28 | let key1 = try! keyPairFromRandom() as! KeyPairEd25519 29 | let key2 = try! keyPairFromRandom() as! KeyPairEd25519 30 | try! await self.stores[0].setKey(networkId: "network", accountId: "account", keyPair: key1) 31 | try! await self.stores[1].setKey(networkId: "network", accountId: "account", keyPair: key2) 32 | let key = try! await self.keyStore.getKey(networkId: "network", accountId: "account") as! KeyPairEd25519 33 | XCTAssertEqual(key, key1) 34 | } 35 | 36 | func testSetsKeysOnlyInFirstKeyStore() async throws { 37 | let key1 = try! keyPairFromRandom() as! KeyPairEd25519 38 | try! await self.keyStore.setKey(networkId: "network", accountId: "account", keyPair: key1) 39 | let account1 = try! await self.stores[0].getAccounts(networkId: "network") 40 | let account2 = try! await self.stores[1].getAccounts(networkId: "network") 41 | XCTAssertEqual(account1.count, 1) 42 | XCTAssertEqual(account2.count, 0) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Example/Tests/ProviderSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProviderSpec.swift 3 | // nearclientios_Tests 4 | // 5 | // Created by Dmytro Kurochka on 27.11.2019. 6 | // Copyright © 2019 CocoaPods. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import nearclientios 11 | 12 | class ProviderSpec: XCTestCase { 13 | private var provider: Provider! 14 | override func setUp() { 15 | let url = getConfig(env: .ci).nodeUrl 16 | self.provider = JSONRPCProvider(url: url) 17 | } 18 | 19 | func testFetchNodeStatus() async { 20 | let response = try! await self.provider.status() 21 | XCTAssertTrue(response.chainId.contains("ci-testnet")) 22 | } 23 | 24 | func testFetchNetworkInfo() async { 25 | let response = try! await self.provider.networkInfo() 26 | XCTAssertNotNil(response.peerMaxCount) 27 | } 28 | 29 | func testCorrectFinalTransactionResult() { 30 | 31 | let outcome = ExecutionOutcome( 32 | status: .successReceiptId("11112"), 33 | logs: [], 34 | receiptIds: ["11112"], 35 | gasBurnt: 1 36 | ) 37 | let transactionOutcome = ExecutionOutcomeWithId(id: "11111", outcome: outcome) 38 | let firstRecipientOutcome = ExecutionOutcome( 39 | status: .successValue("e30="), 40 | logs: [], 41 | receiptIds: ["11112"], 42 | gasBurnt: 9001 43 | ) 44 | let secondRecipientOutcome = ExecutionOutcome( 45 | status: .successValue(""), 46 | logs: [], 47 | receiptIds: [], 48 | gasBurnt: 0 49 | ) 50 | let receipts = [ExecutionOutcomeWithId(id: "11112", outcome: firstRecipientOutcome), 51 | ExecutionOutcomeWithId(id: "11113", outcome: secondRecipientOutcome)] 52 | let transaction = Transaction( 53 | hash: "11111", 54 | publicKey: "11112", 55 | signature: "11113" 56 | ) 57 | 58 | let result = FinalExecutionOutcome( 59 | transaction: transaction, 60 | status: .successValue("e30="), 61 | transactionOutcome: transactionOutcome, 62 | receiptsOutcome: receipts, receipts: nil 63 | ) 64 | XCTAssertNotNil(getTransactionLastResult(txResult: result)) 65 | } 66 | 67 | func testFinalTransactionResultWithNil() { 68 | let outcome = ExecutionOutcome( 69 | status: .successReceiptId("11112"), 70 | logs: [], 71 | receiptIds: ["11112"], 72 | gasBurnt: 1 73 | ) 74 | let transactionOutcome = ExecutionOutcomeWithId(id: "11111", outcome: outcome) 75 | let firstRecipientOutcome = ExecutionOutcome( 76 | status: .failure(ExecutionError()), 77 | logs: [], 78 | receiptIds: ["11112"], 79 | gasBurnt: 9001 80 | ) 81 | let secondRecipientOutcome = ExecutionOutcome( 82 | status: .successValue(""), 83 | logs: [], 84 | receiptIds: [], 85 | gasBurnt: 0 86 | ) 87 | let receipts = [ExecutionOutcomeWithId(id: "11112", outcome: firstRecipientOutcome), 88 | ExecutionOutcomeWithId(id: "11113", outcome: secondRecipientOutcome)] 89 | let transaction = Transaction( 90 | hash: "11111", 91 | publicKey: "11112", 92 | signature: "11113" 93 | ) 94 | let result = FinalExecutionOutcome( 95 | transaction: transaction, 96 | status: .failure(ExecutionError()), 97 | transactionOutcome: transactionOutcome, 98 | receiptsOutcome: receipts, receipts: nil) 99 | XCTAssertNil(getTransactionLastResult(txResult: result)) 100 | } 101 | 102 | func testFetchBlockInfo() async throws { 103 | let status = try await self.provider.status() 104 | 105 | let height = status.syncInfo.latestBlockHeight - 1 106 | let blockHeight = BlockId.blockHeight(height) 107 | let response = try await provider.block(blockQuery: BlockReference.blockId(blockHeight)) 108 | XCTAssertEqual(response.header.height, height) 109 | 110 | let sameBlock = try await provider.block(blockQuery: BlockReference.blockId(BlockId.blockHash(response.header.hash))) 111 | XCTAssertEqual(sameBlock.header.height, height) 112 | 113 | let optimisticBlock = try await provider.block(blockQuery: BlockReference.finality(Finality.optimistic)) 114 | XCTAssertLessThan(optimisticBlock.header.height - height, 5) 115 | 116 | let finalBlock = try await provider.block(blockQuery: BlockReference.finality(Finality.final)) 117 | XCTAssertLessThan(finalBlock.header.height - height, 5) 118 | } 119 | 120 | func testFetchBlockChanges() async throws { 121 | let status = try await self.provider.status() 122 | let latestHash = BlockId.blockHash(status.syncInfo.latestBlockHash) 123 | let blockQuery = BlockReference.blockId(latestHash) 124 | let response = try await self.provider.blockChanges(blockQuery: blockQuery) 125 | XCTAssertNotNil(response.blockHash) 126 | XCTAssertNotNil(response.changes) 127 | 128 | let latestHeight = BlockId.blockHeight(status.syncInfo.latestBlockHeight) 129 | let blockQuery2 = BlockReference.blockId(latestHeight) 130 | let response2 = try await self.provider.blockChanges(blockQuery: blockQuery2) 131 | XCTAssertNotNil(response2.blockHash) 132 | XCTAssertNotNil(response2.changes) 133 | 134 | let blockQuery3 = BlockReference.finality(Finality.final) 135 | let response3 = try await self.provider.blockChanges(blockQuery: blockQuery3) 136 | XCTAssertNotNil(response3.blockHash) 137 | XCTAssertNotNil(response3.changes) 138 | } 139 | 140 | func testFetchChunkInfo() async throws { 141 | let status = try await self.provider.status() 142 | let height = status.syncInfo.latestBlockHeight - 1 143 | let blockShardId = BlockShardId(blockId: BlockId.blockHeight(height), shardId: 0) 144 | let chunkId = ChunkId.blockShardId(blockShardId) 145 | let response = try await self.provider.chunk(chunkId: chunkId) 146 | XCTAssertEqual(response.header.shardId, 0) 147 | 148 | let sameChunk = try await self.provider.chunk(chunkId: ChunkId.chunkHash(response.header.chunkHash)) 149 | XCTAssertEqual(sameChunk.header.chunkHash, response.header.chunkHash) 150 | XCTAssertEqual(sameChunk.header.shardId, 0) 151 | } 152 | 153 | func testGasPrice() async throws { 154 | let status = try await self.provider.status() 155 | 156 | let blockHeight = NullableBlockId.blockHeight(status.syncInfo.latestBlockHeight) 157 | let response1 = try await self.provider.gasPrice(blockId: blockHeight) 158 | XCTAssertGreaterThan(Int(response1.gasPrice) ?? 0, 0) 159 | 160 | let blockHash = NullableBlockId.blockHash(status.syncInfo.latestBlockHash) 161 | let response2 = try await self.provider.gasPrice(blockId: blockHash) 162 | XCTAssertGreaterThan(Int(response2.gasPrice) ?? 0, 0) 163 | 164 | let response3 = try await self.provider.gasPrice(blockId: NullableBlockId.null) 165 | XCTAssertGreaterThan(Int(response3.gasPrice) ?? 0, 0) 166 | } 167 | 168 | func testExperimentalGenesisConfig() async throws { 169 | let response = try await self.provider.experimentalGenesisConfig() 170 | 171 | XCTAssertNotNil(response.chainId) 172 | XCTAssertNotNil(response.genesisHeight) 173 | } 174 | 175 | func testExperimentalProtocolConfig() async throws { 176 | let status = try await self.provider.status() 177 | let latestHash = BlockId.blockHash(status.syncInfo.latestBlockHash) 178 | let blockQuery = BlockReference.blockId(latestHash) 179 | let response = try await self.provider.experimentalProtocolConfig(blockQuery: blockQuery) 180 | 181 | XCTAssertNotNil(response.chainId) 182 | XCTAssertNotNil(response.genesisHeight) 183 | XCTAssertNotNil(response.runtimeConfig) 184 | XCTAssertNotNil(response.runtimeConfig?.storageAmountPerByte) 185 | } 186 | 187 | func testFetchValidatorInfo() async throws { 188 | let validators = try await self.provider.validators(blockId: NullableBlockId.null) 189 | XCTAssertGreaterThanOrEqual(validators.currentValidators.count, 1) 190 | } 191 | 192 | func testAccessKeyChanges() async throws { 193 | let status = try await self.provider.status() 194 | let changes = try await provider.accessKeyChanges(accountIdArray: [testAccountName], blockQuery: BlockReference.blockId(BlockId.blockHash(status.syncInfo.latestBlockHash))) 195 | XCTAssertEqual(status.syncInfo.latestBlockHash, changes.blockHash) 196 | XCTAssertNotNil(changes.changes) 197 | } 198 | 199 | func testSingleAccessKeyChanges() async throws { 200 | let status = try await self.provider.status() 201 | let near = try await TestUtils.setUpTestConnection() 202 | let testAccount = try await near.account(accountId: testAccountName) 203 | let keyBox = try await testAccount.getAccessKeys() 204 | let publicKey = keyBox.keys.first?.publicKey 205 | let accessKeyWithPublicKey = AccessKeyWithPublicKey(accountId: testAccountName, publicKey: publicKey!) 206 | 207 | let changes = try await self.provider.singleAccessKeyChanges(accessKeyArray: [accessKeyWithPublicKey], blockQuery: BlockReference.blockId(BlockId.blockHash(status.syncInfo.latestBlockHash))) 208 | XCTAssertEqual(status.syncInfo.latestBlockHash, changes.blockHash) 209 | XCTAssertNotNil(changes.changes) 210 | } 211 | 212 | func testAccountChanges() async throws { 213 | let status = try await self.provider.status() 214 | let changes = try await self.provider.accountChanges(accountIdArray: [testAccountName], blockQuery: BlockReference.blockId(BlockId.blockHash(status.syncInfo.latestBlockHash))) 215 | XCTAssertEqual(status.syncInfo.latestBlockHash, changes.blockHash) 216 | XCTAssertNotNil(changes.changes) 217 | } 218 | 219 | func testContractStateChanges() async throws { 220 | let status = try await self.provider.status() 221 | let changes = try await self.provider.contractStateChanges(accountIdArray: [testAccountName], blockQuery: BlockReference.blockId(BlockId.blockHash(status.syncInfo.latestBlockHash)), keyPrefix: nil) 222 | XCTAssertEqual(status.syncInfo.latestBlockHash, changes.blockHash) 223 | XCTAssertNotNil(changes.changes) 224 | } 225 | 226 | func testContractCodeChanges() async throws { 227 | let status = try await self.provider.status() 228 | let changes = try await self.provider.contractCodeChanges(accountIdArray: [testAccountName], blockQuery: BlockReference.blockId(BlockId.blockHash(status.syncInfo.latestBlockHash))) 229 | XCTAssertEqual(status.syncInfo.latestBlockHash, changes.blockHash) 230 | XCTAssertNotNil(changes.changes) 231 | } 232 | 233 | func testTransactionStatus() async throws { 234 | let near = try await TestUtils.setUpTestConnection() 235 | let testAccount = try await near.account(accountId: testAccountName) 236 | let sender = try await TestUtils.createAccount(masterAccount: testAccount) 237 | let receiver = try await TestUtils.createAccount(masterAccount: testAccount) 238 | let outcome = try await sender.sendMoney(receiverId: receiver.accountId, amount: UInt128(1)) 239 | let response = try await self.provider.txStatus(txHash: outcome.transactionOutcome.id.baseDecoded, accountId: sender.accountId) 240 | XCTAssertEqual(response, outcome) 241 | } 242 | 243 | func testTransactionStatusWithReceipts() async throws { 244 | let near = try await TestUtils.setUpTestConnection() 245 | let testAccount = try await near.account(accountId: testAccountName) 246 | let sender = try await TestUtils.createAccount(masterAccount: testAccount) 247 | let receiver = try await TestUtils.createAccount(masterAccount: testAccount) 248 | let outcome = try await sender.sendMoney(receiverId: receiver.accountId, amount: UInt128(1)) 249 | let response = try await self.provider.experimentalTxStatusWithReceipts(txHash: outcome.transactionOutcome.id.baseDecoded, accountId: sender.accountId) 250 | XCTAssertNil(outcome.receipts) 251 | XCTAssertNotNil(response.receipts) 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /Example/Tests/SignerSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignerSpec.swift 3 | // nearclientios_Tests 4 | // 5 | // Created by Dmytro Kurochka on 28.11.2019. 6 | // Copyright © 2019 CocoaPods. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import nearclientios 11 | 12 | class SignerSpec: XCTestCase { 13 | func testNoKeyThrowsError() async throws { 14 | let signer = InMemorySigner(keyStore: InMemoryKeyStore()) 15 | await AssertThrowsError(try await signer.signMessage(message: "message".baseDecoded, accountId: "user", networkId: "network")) { error in 16 | XCTAssertTrue(error is InMemorySignerError) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Example/Tests/TestUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UtilsSpec.swift 3 | // nearclientios_Tests 4 | // 5 | // Created by Dmytro Kurochka on 28.11.2019. 6 | // Copyright © 2019 CocoaPods. All rights reserved. 7 | // 8 | 9 | @testable import nearclientios 10 | import XCTest 11 | 12 | let networkId = "unittest" 13 | let testAccountName = "test.near" 14 | 15 | let INITIAL_BALANCE = UInt128(stringLiteral: "500000000000000000000000000") 16 | let HELLO_WASM_BALANCE = UInt128(stringLiteral: "10000000000000000000000000") 17 | 18 | // Length of a random account. Set to 40 because in the protocol minimal allowed top-level account length should be at 19 | // least 32. 20 | let RANDOM_ACCOUNT_LENGTH = 40; 21 | 22 | enum TestUtils {} 23 | 24 | func unsafeWaitFor(_ f: @escaping () async -> ()) { 25 | let sema = DispatchSemaphore(value: 0) 26 | async { 27 | await f() 28 | sema.signal() 29 | } 30 | sema.wait() 31 | } 32 | 33 | extension TestUtils { 34 | 35 | static func setUpTestConnection() async throws -> Near { 36 | let keyStore = InMemoryKeyStore() 37 | let keyPair = try keyPairFromString(encodedKey: "ed25519:2wyRcSwSuHtRVmkMCGjPwnzZmQLeXLzLLyED1NDMt4BjnKgQL6tF85yBx6Jr26D2dUNeC716RBoTxntVHsegogYw") 38 | try! await(keyStore.setKey(networkId: networkId, accountId: testAccountName, keyPair: keyPair)) 39 | let environment = getConfig(env: .ci) 40 | let config = NearConfig(networkId: networkId, 41 | nodeUrl: environment.nodeUrl, 42 | masterAccount: nil, 43 | keyPath: nil, 44 | helperUrl: nil, 45 | initialBalance: nil, 46 | providerType: .jsonRPC(environment.nodeUrl), 47 | signerType: .inMemory(keyStore), 48 | keyStore: keyStore, 49 | contractName: "contractId", 50 | walletUrl: environment.nodeUrl.absoluteString) 51 | return try await connect(config: config) 52 | } 53 | 54 | // Generate some unique string with a given prefix using the alice nonce. 55 | static func generateUniqueString(prefix: String) -> String { 56 | var result = prefix + "-\(Int(Date().timeIntervalSince1970 * 1000))" + "-\(Int.random(in: 0..<1000000))" 57 | let add_symbols = max(RANDOM_ACCOUNT_LENGTH - result.count, 1) 58 | for _ in 0.. Account { 66 | try await masterAccount.fetchState() 67 | let newAccountName = generateUniqueString(prefix: "test") 68 | let newPublicKey = try await(masterAccount.connection.signer.createKey(accountId: newAccountName, 69 | networkId: networkId, curve: .ED25519)) 70 | _ = try await masterAccount.createAccount(newAccountId: newAccountName, publicKey: newPublicKey, amount: amount) 71 | return Account(connection: masterAccount.connection, accountId: newAccountName) 72 | } 73 | 74 | static func deployContract(workingAccount: Account, contractId: String, amount: UInt128 = HELLO_WASM_BALANCE) async throws -> Contract { 75 | let newPublicKey = try await workingAccount.connection.signer.createKey(accountId: contractId, networkId: networkId, curve: .ED25519) 76 | let data = Wasm().data 77 | _ = try await workingAccount.createAndDeployContract(contractId: contractId, 78 | publicKey: newPublicKey, 79 | data: data.bytes, 80 | amount: amount) 81 | let options = ContractOptions(viewMethods: [.getValue, .getLastResult], 82 | changeMethods: [.setValue, .callPromise], 83 | sender: nil) 84 | let contract = Contract(account: workingAccount, contractId: contractId, options: options) 85 | return contract 86 | } 87 | } 88 | 89 | extension XCTest { 90 | func AssertThrowsError( 91 | _ expression: @autoclosure () async throws -> Any, 92 | _ message: @autoclosure () -> String = "", 93 | file: StaticString = #filePath, 94 | line: UInt = #line, 95 | _ errorHandler: (_ error: Error) -> Void = { _ in } 96 | ) async { 97 | do { 98 | _ = try await expression() 99 | XCTFail(message(), file: file, line: line) 100 | } catch { 101 | errorHandler(error) 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Example/Tests/Transaction.json: -------------------------------------------------------------------------------- 1 | {"type": "Transaction", 2 | "data": "0000000000795cb7b5f57222e742d1759092f0e20071a0cd2bf30e1f681d800e67935e168801000000000000001000000073747564696f2d76776375396534316d4def837b838543990f3380af8e2a3817ddf70fe9960135b2add25a679b2a01ed01000000020a0000006164644d6573736167650b0000007b2274657874223a22227d80841e000000000000000000000000000000000000000000"} 3 | -------------------------------------------------------------------------------- /Example/Tests/WalletAccountSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WalletAccountSpec.swift 3 | // nearclientios_Tests 4 | // 5 | // Created by Dmytro Kurochka on 28.11.2019. 6 | // Copyright © 2019 CocoaPods. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import KeychainAccess 11 | @testable import nearclientios 12 | 13 | internal class MockAuthService: ExternalAuthService { 14 | var urls: [URL] = [] 15 | 16 | func openURL(_ url: URL, presentingViewController: UIViewController) -> Bool { 17 | urls.append(url) 18 | return true 19 | } 20 | } 21 | 22 | class WalletAccountSpec: XCTestCase { 23 | 24 | var walletAccount: WalletAccount! 25 | var keyStore: KeyStore! 26 | let walletUrl = "http://example.com/wallet" 27 | var authService: MockAuthService! 28 | var nearFake: Near! 29 | var testStorage: Keychain! 30 | 31 | override func setUp() { 32 | self.keyStore = InMemoryKeyStore() 33 | self.nearFake = try Near(config: NearConfig(networkId: "networkId", 34 | nodeUrl: URL(string: self.walletUrl)!, 35 | masterAccount: nil, 36 | keyPath: nil, 37 | helperUrl: nil, 38 | initialBalance: nil, 39 | providerType: .jsonRPC(URL(string: self.walletUrl)!), 40 | signerType: .inMemory(self.keyStore), 41 | keyStore: self.keyStore, 42 | contractName: "contractId", 43 | walletUrl: self.walletUrl)) 44 | self.testStorage = Keychain(service: "TEST_WALLET_STORAGE_SERVICE") 45 | self.authService = MockAuthService() 46 | self.walletAccount = try! WalletAccount(near: self.nearFake, 47 | authService: self.authService, 48 | storage: self.testStorage) 49 | } 50 | 51 | override func tearDown() { 52 | try! self.testStorage.removeAll() 53 | } 54 | 55 | func testNotSignedInByDefault() async { 56 | let signedIn = await walletAccount.isSignedIn() 57 | XCTAssertFalse(signedIn) 58 | } 59 | 60 | func testCanRequestSignIn() async throws { 61 | let viewController = UIViewController() 62 | try await self.walletAccount.requestSignIn(contractId: "signInContract", 63 | title: "signInTitle", presentingViewController: viewController) 64 | let accounts = try await self.keyStore.getAccounts(networkId: "networkId") 65 | XCTAssertEqual(accounts.count, 1) 66 | XCTAssertTrue(accounts[0].hasPrefix("pending_key")) 67 | XCTAssertEqual(self.authService.urls.count, 1) 68 | 69 | let newUrl = self.authService.urls.last! 70 | XCTAssertEqual(newUrl.scheme, "http") 71 | XCTAssertEqual(newUrl.host, "example.com") 72 | let params = newUrl.queryParameters! 73 | XCTAssertEqual(params["referrer"], "signInTitle") 74 | XCTAssertEqual(params["contract_id"], "signInContract") 75 | XCTAssertEqual(params["success_url"], "\(APP_SCHEME)://success") 76 | XCTAssertEqual(params["failure_url"], "\(APP_SCHEME)://fail") 77 | let keyPair = try await self.keyStore.getKey(networkId: "networkId", accountId: accounts[0]) 78 | XCTAssertEqual(params["public_key"], keyPair?.getPublicKey().toString()) 79 | } 80 | 81 | func testCompleteSignIn() async throws { 82 | let keyPair = try keyPairFromRandom() as! KeyPairEd25519 83 | try await self.keyStore.setKey(networkId: "networkId", 84 | accountId: "pending_key" + keyPair.getPublicKey().toString(), 85 | keyPair: keyPair) 86 | let public_key = keyPair.getPublicKey().toString() 87 | let url = URL(string: "\(APP_SCHEME)://success?account_id=near.account&public_key=\(public_key)")! 88 | try await self.walletAccount.completeSignIn(url: url) 89 | let testKeyPair = try await self.keyStore.getKey(networkId: "networkId", 90 | accountId: "near.account") 91 | XCTAssertEqual(testKeyPair as? KeyPairEd25519, keyPair) 92 | let signedIn = await self.walletAccount.isSignedIn() 93 | XCTAssertTrue(signedIn) 94 | let accountId = await self.walletAccount.getAccountId() 95 | XCTAssertEqual(accountId, "near.account") 96 | } 97 | 98 | } 99 | 100 | 101 | // override func spec() { 102 | // describe("WalletAccountSpec") { 103 | // 104 | // it("can complete sign in") { 105 | // do { 106 | // let keyPair = try keyPairFromRandom() as! KeyPairEd25519 107 | // try await(self.keyStore.setKey(networkId: "networkId", 108 | // accountId: "pending_key" + keyPair.getPublicKey().toString(), 109 | // keyPair: keyPair)) 110 | // let public_key = keyPair.getPublicKey().toString() 111 | // let url = URL(string: "customscheme://success?account_id=near.account&public_key=\(public_key)")! 112 | // try await(self.walletAccount.completeSignIn(UIApplication.shared, open: url)) 113 | // let testKeyPair = try await(self.keyStore.getKey(networkId: "networkId", 114 | // accountId: "near.account")) 115 | // expect(testKeyPair as? KeyPairEd25519).to(equal(keyPair)) 116 | // expect(self.walletAccount.isSignedIn()).to(beTrue()) 117 | // expect(self.walletAccount.getAccountId()).to(equal("near.account")) 118 | // } catch let error { 119 | // fail("\(error)") 120 | // } 121 | // } 122 | // } 123 | // } 124 | //} 125 | -------------------------------------------------------------------------------- /Example/Tests/Wasm.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Wasm.swift 3 | // nearclientios_Tests 4 | // 5 | // Created by Dmytro Kurochka on 04.12.2019. 6 | // Copyright © 2019 CocoaPods. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | internal class Wasm { 12 | lazy var data: Data = { 13 | let testBundle = Bundle(for: type(of: self)) 14 | guard let fileURL = testBundle.url(forResource: "main", withExtension: "wasm") else { fatalError() } 15 | return try! Data(contentsOf: fileURL) 16 | }() 17 | } 18 | -------------------------------------------------------------------------------- /Example/Tests/main.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/near/near-api-swift/95a9c118065f072aba2fa92c476a8cb53b1559a6/Example/Tests/main.wasm -------------------------------------------------------------------------------- /Example/nearclientios.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/nearclientios.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/nearclientios.xcodeproj/xcshareddata/xcschemes/nearclientios-Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 51 | 52 | 53 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 76 | 78 | 84 | 85 | 86 | 87 | 93 | 95 | 101 | 102 | 103 | 104 | 106 | 107 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /Example/nearclientios.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/nearclientios.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/nearclientios.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/nearclientios/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // nearclientios 4 | // 5 | // Created by dmitrykurochka on 10/28/2019. 6 | // Copyright (c) 2019 dmitrykurochka. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | func applicationWillResignActive(_ application: UIApplication) { 22 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 23 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 24 | } 25 | 26 | func applicationDidEnterBackground(_ application: UIApplication) { 27 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 28 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 29 | } 30 | 31 | func applicationWillEnterForeground(_ application: UIApplication) { 32 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 33 | } 34 | 35 | func applicationDidBecomeActive(_ application: UIApplication) { 36 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 37 | } 38 | 39 | func applicationWillTerminate(_ application: UIApplication) { 40 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 41 | } 42 | } 43 | 44 | extension UIApplication { 45 | static var name: String? { 46 | return Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String 47 | } 48 | } 49 | 50 | -------------------------------------------------------------------------------- /Example/nearclientios/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 25 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /Example/nearclientios/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 | 30 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /Example/nearclientios/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ios-marketing", 45 | "size" : "1024x1024", 46 | "scale" : "1x" 47 | } 48 | ], 49 | "info" : { 50 | "version" : 1, 51 | "author" : "xcode" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Example/nearclientios/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | NSAppTransportSecurity 26 | 27 | NSAllowsArbitraryLoads 28 | 29 | 30 | UILaunchStoryboardName 31 | LaunchScreen 32 | UIMainStoryboardFile 33 | Main 34 | UIRequiredDeviceCapabilities 35 | 36 | armv7 37 | 38 | UISupportedInterfaceOrientations 39 | 40 | UIInterfaceOrientationPortrait 41 | UIInterfaceOrientationLandscapeLeft 42 | 43 | NSFaceIDUsageDescription 44 | NEAR uses FaceID to protect your private keys. 45 | 46 | 47 | -------------------------------------------------------------------------------- /Example/nearclientios/WelcomeViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // nearclientios 4 | // 5 | // Created by dmitrykurochka on 10/28/2019. 6 | // Copyright (c) 2019 dmitrykurochka. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import nearclientios 11 | import LocalAuthentication 12 | 13 | class WelcomeViewController: UIViewController, WalletSignInDelegate { 14 | 15 | private var walletAccount: WalletAccount? 16 | private var near: Near? 17 | 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | // Do any additional setup after loading the view, typically from a nib. 21 | } 22 | 23 | override func viewDidAppear(_ animated: Bool) { 24 | super.viewDidAppear(animated) 25 | Task { 26 | walletAccount = await setupWallet() 27 | await setupUI(with: walletAccount!) 28 | } 29 | } 30 | 31 | override func didReceiveMemoryWarning() { 32 | super.didReceiveMemoryWarning() 33 | // Dispose of any resources that can be recreated. 34 | } 35 | 36 | private func setupWallet() async -> WalletAccount { 37 | let keyStore = SecureEnclaveKeyStore(keychain: .init(service: "example.keystore")) 38 | keyStore.context = LAContext() 39 | let config = NearConfig( 40 | networkId: "testnet", // "default" for mainnet 41 | nodeUrl: URL(string: "https://rpc.testnet.near.org")!, // "https://rpc.mainnet.near.org" for mainnet 42 | masterAccount: nil, 43 | keyPath: nil, 44 | helperUrl: nil, 45 | initialBalance: nil, 46 | providerType: .jsonRPC(URL(string: "https://rpc.testnet.near.org")!), // "https://rpc.mainnet.near.org" for mainnet 47 | signerType: .inMemory(keyStore), 48 | keyStore: keyStore, 49 | contractName: nil, 50 | walletUrl: "https://wallet.testnet.near.org" // "https://wallet.near.org" for mainnet 51 | ) 52 | near = Near(config: config) 53 | return try! WalletAccount(near: near!, authService: DefaultAuthService.shared) // a failed try here represents a configuration error, not a runtime error. It's safe to store a `WalletAccount!`. 54 | } 55 | 56 | private func setupUI(with wallet: WalletAccount) async { 57 | if await wallet.isSignedIn() { 58 | await MainActor.run { 59 | showAccountState(with: wallet) 60 | } 61 | } else { 62 | //Hide preloader 63 | } 64 | } 65 | 66 | private func showAccountState(with wallet: WalletAccount) { 67 | guard let accountVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "AccountViewController") as? AccountViewController else { 68 | return 69 | } 70 | accountVC.setup(near: near!, wallet: wallet) 71 | navigationController?.pushViewController(accountVC, animated: true) 72 | } 73 | 74 | @IBAction func tapShowAuthForm(_ sender: UIButton) { 75 | Task { 76 | let appName = UIApplication.name ?? "signInTitle" 77 | DefaultAuthService.shared.walletSignIn = self 78 | try! await walletAccount!.requestSignIn(contractId: nil, title: appName, presentingViewController: self) 79 | } 80 | } 81 | 82 | func completeSignIn(url: URL) async { 83 | do { 84 | try await walletAccount?.completeSignIn(url: url) 85 | } catch { 86 | await MainActor.run { 87 | let alert = UIAlertController(title: "Error", message: "\(error)", preferredStyle: .alert) 88 | alert.addAction(UIAlertAction(title: "Dismiss", style: .cancel, handler: { [weak self] _ in 89 | self?.dismiss(animated: true, completion: nil) 90 | })) 91 | present(alert, animated: true, completion: nil) 92 | } 93 | } 94 | await setupUI(with: walletAccount!) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 NEAR Inc 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.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "AnyCodable", 6 | "repositoryURL": "https://github.com/Flight-School/AnyCodable.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "876d162385e9862ae8b3c8d65dc301312b040005", 10 | "version": "0.6.0" 11 | } 12 | }, 13 | { 14 | "package": "Base58Swift", 15 | "repositoryURL": "https://github.com/keefertaylor/Base58Swift.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "1c13ea6b07f1584660526f8bde3d4cc150e91acd", 19 | "version": "2.1.14" 20 | } 21 | }, 22 | { 23 | "package": "BigInt", 24 | "repositoryURL": "https://github.com/attaswift/BigInt.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "0ed110f7555c34ff468e72e1686e59721f2b0da6", 28 | "version": "5.3.0" 29 | } 30 | }, 31 | { 32 | "package": "KeychainAccess", 33 | "repositoryURL": "https://github.com/kishikawakatsumi/KeychainAccess.git", 34 | "state": { 35 | "branch": null, 36 | "revision": "84e546727d66f1adc5439debad16270d0fdd04e7", 37 | "version": "4.2.2" 38 | } 39 | }, 40 | { 41 | "package": "secp256k1", 42 | "repositoryURL": "https://github.com/GigaBitcoin/secp256k1.swift.git", 43 | "state": { 44 | "branch": null, 45 | "revision": "1a796f738bdcd84b41d05f92593188b23163e60b", 46 | "version": "0.7.0" 47 | } 48 | }, 49 | { 50 | "package": "TweetNacl", 51 | "repositoryURL": "https://github.com/bitmark-inc/tweetnacl-swiftwrap.git", 52 | "state": { 53 | "branch": null, 54 | "revision": "f8fd111642bf2336b11ef9ea828510693106e954", 55 | "version": "1.1.0" 56 | } 57 | } 58 | ] 59 | }, 60 | "version": 1 61 | } 62 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | let package = Package( 6 | name: "nearclientios", 7 | platforms: [ 8 | .iOS(.v13), 9 | .macOS(.v12) 10 | ], 11 | products: [ 12 | // Products define the executables and libraries a package produces, and make them visible to other packages. 13 | .library( 14 | name: "nearclientios", 15 | targets: ["nearclientios"] 16 | ), 17 | ], 18 | dependencies: [ 19 | // Dependencies declare other packages that this package depends on. 20 | .package(url: "https://github.com/kishikawakatsumi/KeychainAccess.git", .exact("4.2.2")), 21 | .package(url: "https://github.com/bitmark-inc/tweetnacl-swiftwrap.git", .exact("1.1.0")), 22 | .package(url: "https://github.com/Flight-School/AnyCodable.git", .exact("0.6.0")), 23 | .package(url: "https://github.com/keefertaylor/Base58Swift.git", from: "2.1.0"), 24 | .package(url: "https://github.com/GigaBitcoin/secp256k1.swift.git", .exact("0.7.0")) 25 | ], 26 | targets: [ 27 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 28 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 29 | .target( 30 | name: "nearclientios", 31 | dependencies: [ 32 | "KeychainAccess", 33 | .product(name: "TweetNacl", package: "tweetnacl-swiftwrap"), 34 | "AnyCodable", 35 | "Base58Swift", 36 | .product(name: "secp256k1", package: "secp256k1.swift"), 37 | ], 38 | path: "./nearclientios/Sources" 39 | ), 40 | ] 41 | ) 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nearclientios 2 | 3 | [![Build Status](https://travis-ci.com/nearprotocol/near-client-ios.svg?branch=master)](https://travis-ci.com/nearprotocol/near-client-ios) 4 | [![Version](https://img.shields.io/cocoapods/v/nearclientios.svg?style=flat)](https://cocoapods.org/pods/nearclientios) 5 | [![License MIT](https://img.shields.io/github/license/nearprotocol/near-client-ios)](https://github.com/nearprotocol/near-client-ios/blob/master/LICENSE) 6 | [![Platform](https://img.shields.io/cocoapods/p/nearclientios.svg?style=flat)](https://cocoapods.org/pods/nearclientios) 7 | 8 | ## Example 9 | 10 | To run the example project, clone the repo, and run `pod install` from the Example directory first. 11 | 12 | # Usage 13 | 14 | ```swift 15 | import nearclientios 16 | class ViewController: UIViewController, WalletSignInDelegate { 17 | private var walletAccount: WalletAccount? 18 | private var near: Near? 19 | 20 | override func viewDidLoad() { 21 | super.viewDidLoad() 22 | let keyStore = KeychainKeyStore(keychain: .init(service: "example.keystore")) 23 | let config = NearConfig( 24 | networkId: "testnet", // "default" for mainnet 25 | nodeUrl: URL(string: "https://rpc.testnet.near.org")!, // "https://rpc.mainnet.near.org" for mainnet 26 | masterAccount: nil, 27 | keyPath: nil, 28 | helperUrl: nil, 29 | initialBalance: nil, 30 | providerType: .jsonRPC(URL(string: "https://rpc.testnet.near.org")!), // "https://rpc.mainnet.near.org" for mainnet 31 | signerType: .inMemory(keyStore), 32 | keyStore: keyStore, 33 | contractName: nil, 34 | walletUrl: "https://wallet.testnet.near.org" // "https://wallet.near.org" for mainnet 35 | ) 36 | near = try Near(config: config) 37 | walletAccount = try! WalletAccount(near: near!, authService: DefaultAuthService.shared) // a failed try here represents a configuration error, not a runtime error. It's safe to store a `WalletAccount!`. 38 | let appName = UIApplication.name ?? "signInTitle" 39 | DefaultAuthService.shared.walletSignIn = self 40 | try! await walletAccount!.requestSignIn(contractId: nil, title: appName, presentingViewController: self) 41 | } 42 | func completeSignIn(url: URL) async { 43 | try! await walletAccount?.completeSignIn(url: url) 44 | MainActor.run { 45 | //do any additional UI work on the main thread after sign in is complete 46 | } 47 | } 48 | } 49 | ``` 50 | 51 | ## Requirements 52 | 53 | nearclientios makes use of Swift's async/await and thus requires iOS 13. 54 | 55 | ## Installation 56 | 57 | ### CocoaPods 58 | nearclientios is available through [CocoaPods](https://cocoapods.org). To install 59 | it, simply add the following line to your Podfile: 60 | 61 | ```ruby 62 | pod 'nearclientios' 63 | ``` 64 | 65 | ### Swift Package Manager 66 | Once you have your Swift package set up, you can add nearclientios as a dependency in Package.swift. 67 | 68 | ```swift 69 | dependencies: [ 70 | .package(url: "https://github.com/near/near-api-swift", .upToNextMajor(from: "1.0.29")) 71 | ] 72 | 73 | ``` 74 | ## Examples 75 | 76 | * [Basic nearclientios function calls](https://github.com/LyubomyrBurday/near_basic) 77 | 78 | ## Author 79 | 80 | NEAR Inc 81 | 82 | ## License 83 | 84 | nearclientios is available under the MIT license. See the LICENSE file for more info. 85 | -------------------------------------------------------------------------------- /_Pods.xcodeproj: -------------------------------------------------------------------------------- 1 | Example/Pods/Pods.xcodeproj -------------------------------------------------------------------------------- /nearclientios.h: -------------------------------------------------------------------------------- 1 | // 2 | // nearclientios.h 3 | // nearclientios 4 | // 5 | // Created by Sam Ingle on 1/14/22. 6 | // 7 | 8 | #import 9 | 10 | //! Project version number for nearclientios. 11 | FOUNDATION_EXPORT double nearclientiosVersionNumber; 12 | 13 | //! Project version string for nearclientios. 14 | FOUNDATION_EXPORT const unsigned char nearclientiosVersionString[]; 15 | 16 | // In this header, you should import all the public headers of your framework using statements like #import 17 | 18 | 19 | -------------------------------------------------------------------------------- /nearclientios.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'nearclientios' 3 | s.version = '1.0.29' 4 | s.summary = 'Swift SDK to interact with NEAR Protocol' 5 | 6 | s.description = <<-DESC 7 | near-client-ios is a SWIFT library for development of DApps on NEAR platform. 8 | DESC 9 | s.homepage = 'https://github.com/nearprotocol/near-client-ios' 10 | s.license = { :type => 'MIT', :file => 'LICENSE' } 11 | s.author = { 'dmitrykurochka' => 'v.i.p.dimak@gmail.com' } 12 | s.source = { :git => 'https://github.com/nearprotocol/near-client-ios.git', :tag => s.version.to_s } 13 | s.social_media_url = 'https://twitter.com/nearprotocol' 14 | 15 | s.ios.deployment_target = '13.0' 16 | 17 | s.source_files = 'nearclientios/Sources/**/*' 18 | s.swift_versions = ["5.0"] 19 | 20 | s.dependency 'TweetNacl', '~> 1.0' 21 | s.dependency 'KeychainAccess', '~> 4.2.2' 22 | s.dependency 'Base58Swift', '~> 2.1.10' 23 | s.dependency 'secp256k1.swift' 24 | s.dependency 'AnyCodable-FlightSchool', '~> 0.6.0' 25 | end 26 | -------------------------------------------------------------------------------- /nearclientios.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /nearclientios.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /nearclientios/Assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/near/near-api-swift/95a9c118065f072aba2fa92c476a8cb53b1559a6/nearclientios/Assets/.gitkeep -------------------------------------------------------------------------------- /nearclientios/Sources/AccountCreator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccountCreator.swift 3 | // nearclientios 4 | // 5 | // Created by Dmitry Kurochka on 10/30/19. 6 | // Copyright © 2019 NEAR Protocol. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol AccountCreator { 12 | func createAccount(newAccountId: String, publicKey: PublicKey) async throws -> Void 13 | } 14 | 15 | public struct LocalAccountCreator { 16 | let masterAccount: Account 17 | let initialBalance: UInt128 18 | } 19 | 20 | extension LocalAccountCreator: AccountCreator { 21 | public func createAccount(newAccountId: String, publicKey: PublicKey) async throws -> Void { 22 | let _ = try await masterAccount.createAccount(newAccountId: newAccountId, publicKey: publicKey, amount: initialBalance) 23 | } 24 | } 25 | 26 | public struct UrlAccountCreator { 27 | let connection: Connection 28 | let helperConnection: ConnectionInfo 29 | } 30 | 31 | extension UrlAccountCreator { 32 | init(connection: Connection, helperUrl: URL) { 33 | self.init(connection: connection, helperConnection: ConnectionInfo(url: helperUrl)) 34 | } 35 | } 36 | 37 | extension UrlAccountCreator: AccountCreator { 38 | public func createAccount(newAccountId: String, publicKey: PublicKey) async throws -> Void { 39 | // no-op 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /nearclientios/Sources/Connection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Connection.swift 3 | // nearclientios 4 | // 5 | // Created by Dmitry Kurochka on 10/30/19. 6 | // Copyright © 2019 NEAR Protocol. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol ConnectionConfigProtocol { 12 | var networkId: String {get} 13 | var providerType: ProviderType {get} 14 | var signerType: SignerType {get} 15 | } 16 | 17 | public struct ConnectionConfig: ConnectionConfigProtocol { 18 | public let networkId: String 19 | public let providerType: ProviderType 20 | public let signerType: SignerType 21 | } 22 | 23 | public extension ConnectionConfigProtocol { 24 | func provider() -> Provider { 25 | switch providerType { 26 | case .jsonRPC(let url): return JSONRPCProvider(url: url) 27 | } 28 | } 29 | } 30 | 31 | public extension ConnectionConfigProtocol { 32 | func signer() -> Signer { 33 | switch signerType { 34 | case .inMemory(let keyStore): return InMemorySigner(keyStore: keyStore) 35 | } 36 | } 37 | } 38 | 39 | public struct Connection { 40 | public let networkId: String 41 | public let provider: Provider 42 | public let signer: Signer 43 | } 44 | 45 | public extension Connection { 46 | static func fromConfig(config: ConnectionConfigProtocol) -> Connection { 47 | let provider = config.provider() 48 | let signer = config.signer() 49 | return Connection(networkId: config.networkId, provider: provider, signer: signer) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /nearclientios/Sources/Contract.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Contract.swift 3 | // nearclientios 4 | // 5 | // Created by Dmitry Kurochka on 10/30/19. 6 | // Copyright © 2019 NEAR Protocol. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol ContractOptionsProtocol { 12 | var viewMethods: [ViewMethod] {get} 13 | var changeMethods: [ChangeMethod] {get} 14 | var sender: String? {get} 15 | } 16 | 17 | public typealias MethodName = String 18 | public typealias ViewMethod = MethodName 19 | public typealias ChangeMethod = MethodName 20 | 21 | public extension ViewMethod { 22 | static let getValue = "getValue" 23 | static let getLastResult = "getLastResult" 24 | static let hello = "hello" 25 | static let getAllKeys = "getAllKeys" 26 | static let returnHiWithLogs = "returnHiWithLogs" 27 | } 28 | 29 | public extension ChangeMethod { 30 | static let setValue = "setValue" 31 | static let callPromise = "callPromise" 32 | static let generateLogs = "generateLogs" 33 | static let triggerAssert = "triggerAssert" 34 | static let testSetRemove = "testSetRemove" 35 | } 36 | 37 | public struct ContractOptions: ContractOptionsProtocol { 38 | public let viewMethods: [ViewMethod] 39 | public let changeMethods: [ChangeMethod] 40 | public let sender: String? 41 | } 42 | 43 | public struct Contract { 44 | let account: Account 45 | let contractId: String 46 | let viewMethods: [ViewMethod] 47 | let changeMethods: [ChangeMethod] 48 | let sender: String? 49 | } 50 | 51 | public extension Contract { 52 | init(account: Account, contractId: String, options: ContractOptionsProtocol) { 53 | self.init(account: account, contractId: contractId, viewMethods: options.viewMethods, 54 | changeMethods: options.changeMethods, sender: nil) 55 | } 56 | } 57 | 58 | public extension Contract { 59 | func view(methodName: ChangeMethod, args: [String: Any] = [:]) async throws -> T { 60 | return try await account.viewFunction(contractId: contractId, methodName: methodName, args: args) 61 | } 62 | } 63 | 64 | public extension Contract { 65 | @discardableResult 66 | func change(methodName: ChangeMethod, args: [String: Any] = [:], 67 | gas: UInt64? = nil, amount: UInt128 = 0) async throws -> Any? { 68 | let rawResult = gas == nil ? try await account.functionCall( 69 | contractId: contractId, 70 | methodName: methodName, 71 | args: args, 72 | amount: amount 73 | ) : try await account.functionCall( 74 | contractId: contractId, 75 | methodName: methodName, 76 | args: args, 77 | gas: gas!, 78 | amount: amount 79 | ) 80 | return getTransactionLastResult(txResult: rawResult) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /nearclientios/Sources/DefaultAuthService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultAuthService.swift 3 | // nearclientios 4 | // 5 | // Created by Kevin McConnaughay on 2/17/22. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | import WebKit 11 | 12 | public class DefaultAuthService: NSObject, ExternalAuthService { 13 | public static let shared = DefaultAuthService() 14 | 15 | var navController: UINavigationController? 16 | public weak var walletSignIn: WalletSignInDelegate? 17 | 18 | public func openURL(_ url: URL, presentingViewController: UIViewController) -> Bool { 19 | let viewController = UIViewController() 20 | navController = UINavigationController(rootViewController: viewController) 21 | let closeButton = UIBarButtonItem(title: "Cancel", style: .plain, target: self, action: #selector(self.dismiss)) 22 | viewController.navigationItem.rightBarButtonItem = closeButton 23 | let webView = WKWebView() 24 | webView.navigationDelegate = self 25 | webView.translatesAutoresizingMaskIntoConstraints = false 26 | viewController.view.addSubview(webView) 27 | webView.leadingAnchor.constraint(equalTo: viewController.view.leadingAnchor).isActive = true 28 | webView.trailingAnchor.constraint(equalTo: viewController.view.trailingAnchor).isActive = true 29 | webView.topAnchor.constraint(equalTo: viewController.view.topAnchor).isActive = true 30 | webView.bottomAnchor.constraint(equalTo: viewController.view.bottomAnchor).isActive = true 31 | webView.load(URLRequest(url: url)) 32 | presentingViewController.present(navController!, animated: true, completion: nil) 33 | return true 34 | } 35 | 36 | @objc private func dismiss() { 37 | navController?.dismiss(animated: true, completion: { [weak self] in 38 | self?.navController = nil 39 | }) 40 | } 41 | } 42 | 43 | extension DefaultAuthService: WKNavigationDelegate { 44 | public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { 45 | defer { 46 | decisionHandler(.allow) 47 | } 48 | guard let url = navigationAction.request.url else { return } 49 | guard url.scheme == APP_SCHEME else { return } 50 | Task { 51 | await walletSignIn?.completeSignIn(url: url) 52 | } 53 | dismiss() 54 | } 55 | } 56 | 57 | public protocol WalletSignInDelegate: AnyObject { 58 | func completeSignIn(url: URL) async 59 | } 60 | -------------------------------------------------------------------------------- /nearclientios/Sources/KeyStores/FileSystemKeyStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccountInfo.swift 3 | // nearclientios 4 | // 5 | // Created by Dmitry Kurochka on 10/30/19. 6 | // Copyright © 2019 NEAR Protocol. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /** 12 | // Format of the account stored on disk. 13 | */ 14 | public protocol AccountInfoProtocol { 15 | var account_id: String {get} 16 | var private_key: String? {get} 17 | var secret_key: String? {get} 18 | } 19 | 20 | public struct AccountInfo: AccountInfoProtocol, Codable { 21 | public let account_id: String 22 | public let private_key: String? 23 | public let secret_key: String? 24 | } 25 | 26 | public struct UnencryptedFileSystemKeyStore { 27 | let keyDir: String 28 | let manager: FileManager 29 | 30 | public init(keyDir: String, manager: FileManager = .default) { 31 | self.keyDir = keyDir 32 | self.manager = manager 33 | } 34 | } 35 | 36 | public enum UnencryptedFileSystemKeyStoreError: Error { 37 | case noPrivateKey 38 | } 39 | 40 | extension UnencryptedFileSystemKeyStore: KeyStore { 41 | public func setKey(networkId: String, accountId: String, keyPair: KeyPair) async throws -> Void { 42 | let networkPath = "\(keyDir)/\(networkId)" 43 | let fullNetworkPath = manager.targetDirectory.appendingPathComponent(networkPath).path 44 | try manager.ensureDir(path: fullNetworkPath) 45 | let content = AccountInfo(account_id: accountId, private_key: keyPair.toString(), secret_key: nil) 46 | let encoded = try JSONEncoder().encode(content) 47 | let fileUrl = getKeyFileUrl(networkPath: networkPath, accountId: accountId) 48 | try encoded.write(to: fileUrl, options: [.atomic]) 49 | } 50 | 51 | /// Find key / account id. 52 | public func getKey(networkId: String, accountId: String) async throws -> KeyPair? { 53 | let networkPath = "\(keyDir)/\(networkId)" 54 | let path = getKeyFileUrl(networkPath: networkPath, accountId: accountId).path 55 | guard manager.fileExists(atPath: path) else {return nil} 56 | let accountKeyPair = try await UnencryptedFileSystemKeyStore.readKeyFile(path: path) 57 | return accountKeyPair.1 58 | } 59 | 60 | public func removeKey(networkId: String, accountId: String) async throws -> Void { 61 | let networkPath = "\(keyDir)/\(networkId)" 62 | let path = getKeyFileUrl(networkPath: networkPath, accountId: accountId).path 63 | guard manager.fileExists(atPath: path) else {return} 64 | try manager.removeItem(atPath: path) 65 | } 66 | 67 | public func clear() async throws -> Void { 68 | let networksPath = manager.targetDirectory.appendingPathComponent(keyDir).path 69 | try manager.removeItem(atPath: networksPath) 70 | } 71 | 72 | public func getNetworks() async throws -> [String] { 73 | let networksPath = manager.targetDirectory.appendingPathComponent(keyDir).path 74 | let files = try manager.contentsOfDirectory(atPath: networksPath) 75 | return files 76 | } 77 | 78 | public func getAccounts(networkId: String) async throws -> [String] { 79 | let networkPath = "\(keyDir)/\(networkId)" 80 | let fullNetworkPath = manager.targetDirectory.appendingPathComponent(networkPath).path 81 | guard manager.fileExists(atPath: fullNetworkPath) else {return []} 82 | let files = try manager.contentsOfDirectory(atPath: fullNetworkPath) 83 | return files.filter {$0.hasSuffix(".json")}.map {$0.replacingOccurrences(of: ".json", with: "")} 84 | } 85 | } 86 | 87 | extension UnencryptedFileSystemKeyStore { 88 | private func getKeyFileUrl(networkPath: String, accountId: String) -> URL { 89 | return manager.targetDirectory.appendingPathComponent("\(networkPath)/\(accountId).json") 90 | } 91 | 92 | private static func loadJsonFile(path: String) async throws -> AccountInfo { 93 | let content = try Data(contentsOf: URL(fileURLWithPath: path), options: []) 94 | let accountInfo = try JSONDecoder().decode(AccountInfo.self, from: content) 95 | return accountInfo 96 | } 97 | 98 | static func readKeyFile(path: String) async throws -> (String, KeyPair) { 99 | let accountInfo = try await loadJsonFile(path: path) 100 | // The private key might be in private_key or secret_key field. 101 | var privateKey = accountInfo.private_key 102 | if privateKey == nil, accountInfo.secret_key != nil { 103 | privateKey = accountInfo.secret_key 104 | } 105 | guard privateKey != nil else {throw UnencryptedFileSystemKeyStoreError.noPrivateKey} 106 | let keyPair = try keyPairFromString(encodedKey: privateKey!) 107 | return (accountInfo.account_id, keyPair) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /nearclientios/Sources/KeyStores/InMemoryKeyStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InMemoryKeyStore.swift 3 | // nearclientios 4 | // 5 | // Created by Dmitry Kurochka on 10/30/19. 6 | // Copyright © 2019 NEAR Protocol. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /** 12 | * Simple in-memory keystore for testing purposes. 13 | */ 14 | public class InMemoryKeyStore { 15 | private var keys: [String: String] 16 | 17 | public init(keys: [String: String] = [:]) { 18 | self.keys = keys 19 | } 20 | } 21 | 22 | extension InMemoryKeyStore: KeyStore { 23 | public func setKey(networkId: String, accountId: String, keyPair: KeyPair) async throws -> Void { 24 | keys["\(accountId):\(networkId)"] = keyPair.toString() 25 | } 26 | 27 | public func getKey(networkId: String, accountId: String) async throws -> KeyPair? { 28 | guard let value = keys["\(accountId):\(networkId)"] else {return nil} 29 | return try? keyPairFromString(encodedKey: value) 30 | } 31 | 32 | public func removeKey(networkId: String, accountId: String) async throws -> Void { 33 | keys.removeValue(forKey: "\(accountId):\(networkId)") 34 | } 35 | 36 | public func clear() async throws -> Void { 37 | keys = [:] 38 | } 39 | 40 | public func getNetworks() async throws -> [String] { 41 | var result = Set() 42 | keys.keys.forEach {key in 43 | let parts = key.split(separator: ":") 44 | result.insert(String(parts[1])) 45 | } 46 | return Array(result) 47 | } 48 | 49 | public func getAccounts(networkId: String) async throws -> [String] { 50 | var result = [String]() 51 | keys.keys.forEach {key in 52 | let parts = key.split(separator: ":").map {String($0)} 53 | if parts[parts.count - 1] == networkId { 54 | result.append(parts.dropLast().joined(separator: ":")) 55 | } 56 | } 57 | return result 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /nearclientios/Sources/KeyStores/KeyStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyStore.swift 3 | // nearclientios 4 | // 5 | // Created by Dmitry Kurochka on 10/30/19. 6 | // Copyright © 2019 NEAR Protocol. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /** 12 | * Key store interface for `InMemorySigner`. 13 | */ 14 | public protocol KeyStore { 15 | func setKey(networkId: String, accountId: String, keyPair: KeyPair) async throws -> Void 16 | func getKey(networkId: String, accountId: String) async throws -> KeyPair? 17 | func removeKey(networkId: String, accountId: String) async throws -> Void 18 | func clear() async throws -> Void 19 | func getNetworks() async throws -> [String] 20 | func getAccounts(networkId: String) async throws -> [String] 21 | } 22 | -------------------------------------------------------------------------------- /nearclientios/Sources/KeyStores/KeychainKeyStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeychainKeyStore.swift 3 | // nearclientios 4 | // 5 | // Created by Dmytro Kurochka on 08.11.2019. 6 | // 7 | 8 | import Foundation 9 | import KeychainAccess 10 | 11 | public let NEAR_KEYCHAIN_STORAGE_SERVICE = "near.keystore" 12 | 13 | public struct KeychainKeyStore { 14 | private let keychain: Keychain 15 | 16 | public init(keychain: Keychain = .init(service: NEAR_KEYCHAIN_STORAGE_SERVICE)) { 17 | self.keychain = keychain 18 | } 19 | } 20 | 21 | extension KeychainKeyStore: KeyStore { 22 | public func setKey(networkId: String, accountId: String, keyPair: KeyPair) async throws -> Void { 23 | keychain[storageKeyForSecretKey(networkId: networkId, accountId: accountId)] = keyPair.toString() 24 | } 25 | 26 | public func setKey(networkId: String, accountId: String, withKeyPairAsData keyPair: Data) async throws -> Void { 27 | keychain[data: storageKeyForSecretKey(networkId: networkId, accountId: accountId)] = keyPair 28 | } 29 | 30 | public func getKey(networkId: String, accountId: String) async throws -> KeyPair? { 31 | guard let value = keychain[storageKeyForSecretKey(networkId: networkId, accountId: accountId)] else { 32 | return nil 33 | } 34 | return try? keyPairFromString(encodedKey: value) 35 | } 36 | 37 | public func getEncryptedKey(networkId: String, accountId: String) async throws -> Data? { 38 | guard let value = keychain[data: storageKeyForSecretKey(networkId: networkId, accountId: accountId)] else { 39 | return nil 40 | } 41 | return value 42 | } 43 | 44 | public func removeKey(networkId: String, accountId: String) async throws -> Void { 45 | keychain[storageKeyForSecretKey(networkId: networkId, accountId: accountId)] = nil 46 | } 47 | 48 | public func clear() async throws -> Void { 49 | try keychain.removeAll() 50 | } 51 | 52 | public func getNetworks() async throws -> [String] { 53 | var result = Set() 54 | for key in storageKeys() { 55 | if let networkId = key.components(separatedBy: ":").last { 56 | result.insert(networkId) 57 | } 58 | } 59 | return Array(result) 60 | } 61 | 62 | public func getAccounts(networkId: String) async throws -> [String] { 63 | var result = [String]() 64 | for key in storageKeys() { 65 | let components = key.components(separatedBy: ":") 66 | let accountId = components[components.count - 2] 67 | if let keychainNetworkId = components.last, keychainNetworkId == networkId, accountId != networkId { 68 | result.append(accountId) 69 | } 70 | } 71 | return result 72 | } 73 | } 74 | 75 | extension KeychainKeyStore { 76 | private func storageKeyForSecretKey(networkId: String, accountId: String) -> String { 77 | return "\(accountId):\(networkId)" 78 | } 79 | 80 | private func storageKeys() -> [String] { 81 | return keychain.allKeys() 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /nearclientios/Sources/KeyStores/MergeKeyStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MergeKeyStore.swift 3 | // nearclientios 4 | // 5 | // Created by Dmitry Kurochka on 10/30/19. 6 | // Copyright © 2019 NEAR Protocol. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /** 12 | * Keystore which can be used to merge multiple key stores into one virtual key store. 13 | */ 14 | public struct MergeKeyStore { 15 | /// First keystore gets all write calls, read calls are attempted from start to end of array 16 | private(set) var keyStores: [KeyStore] 17 | 18 | public init(keyStores: [KeyStore] = []) { 19 | self.keyStores = keyStores 20 | } 21 | } 22 | 23 | extension MergeKeyStore: KeyStore { 24 | public func setKey(networkId: String, accountId: String, keyPair: KeyPair) async throws -> Void { 25 | return try await keyStores[0].setKey(networkId: networkId, accountId: accountId, keyPair: keyPair) 26 | } 27 | 28 | public func getKey(networkId: String, accountId: String) async throws -> KeyPair? { 29 | for keyStore in keyStores { 30 | if let keyPair = try await keyStore.getKey(networkId: networkId, accountId: accountId) { 31 | return keyPair 32 | } 33 | } 34 | return nil 35 | } 36 | 37 | public func removeKey(networkId: String, accountId: String) async throws -> Void { 38 | for keyStore in keyStores { 39 | try await keyStore.removeKey(networkId: networkId, accountId: accountId) 40 | } 41 | } 42 | 43 | public func clear() async throws -> Void { 44 | for keyStore in keyStores { 45 | try await keyStore.clear() 46 | } 47 | } 48 | 49 | public func getNetworks() async throws -> [String] { 50 | var result = Set() 51 | for keyStore in keyStores { 52 | let networks = try await keyStore.getNetworks() 53 | for network in networks { 54 | result.insert(network) 55 | } 56 | } 57 | return Array(result) 58 | } 59 | 60 | public func getAccounts(networkId: String) async throws -> [String] { 61 | var result = Set() 62 | for keyStore in keyStores { 63 | let accounts = try await keyStore.getAccounts(networkId: networkId) 64 | for account in accounts { 65 | result.insert(account) 66 | } 67 | } 68 | return Array(result) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /nearclientios/Sources/KeyStores/SecureEnclaveKeyStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SecureEnclaveKeyStore.swift 3 | // nearclientios 4 | // 5 | // Created by Kevin McConnaughay on 4/1/22. 6 | // 7 | 8 | import Foundation 9 | import CryptoKit 10 | import LocalAuthentication 11 | import KeychainAccess 12 | 13 | private let algorithm = SecKeyAlgorithm.eciesEncryptionCofactorVariableIVX963SHA256AESGCM 14 | 15 | private enum SecureEnclaveKeyStoreError: Error { 16 | case cannotAccessPrivateKey(osStatus: OSStatus) 17 | case cannotAccessPublicKey 18 | case encryptionNotSupported(algorithm: SecKeyAlgorithm) 19 | case decryptionNotSupported(algorithm: SecKeyAlgorithm) 20 | case cannotDeleteKeys(osStatus: OSStatus) 21 | case unexpected(description: String) 22 | 23 | var errorDescription: String { 24 | switch self { 25 | case .cannotAccessPrivateKey(let osStatus): 26 | return "Cannot access private key: \(osStatus)" 27 | case .cannotAccessPublicKey: 28 | return "Cannot access public key" 29 | case .encryptionNotSupported(let algorithm): 30 | return "Encryption not supported: \(algorithm)" 31 | case .decryptionNotSupported(let algorithm): 32 | return "Decryption not supported: \(algorithm)" 33 | case .cannotDeleteKeys(let osStatus): 34 | return "Cannot delete key: \(osStatus)" 35 | case .unexpected(let description): 36 | return description 37 | } 38 | } 39 | } 40 | 41 | public class SecureEnclaveKeyStore { 42 | public let keychain: Keychain 43 | public let keychainKeyStore: KeychainKeyStore 44 | /// Ignored and forced to false on simulators. 45 | let requireUserPresence: Bool 46 | public var context: LAContext? 47 | 48 | public init(keychain: Keychain = .init(service: NEAR_KEYCHAIN_STORAGE_SERVICE), requireUserPresence: Bool = true) { 49 | var userPresence = requireUserPresence 50 | #if targetEnvironment(simulator) 51 | userPresence = false 52 | #endif 53 | self.keychain = keychain 54 | self.keychainKeyStore = .init(keychain: keychain) 55 | self.requireUserPresence = userPresence 56 | } 57 | } 58 | 59 | extension SecureEnclaveKeyStore: KeyStore { 60 | public func setKey(networkId: String, accountId: String, keyPair: KeyPair) async throws -> Void { 61 | let storageKey = storageKeyForSecretKey(networkId: networkId, accountId: accountId) 62 | let keyPairData = keyPair.toString().data(using: .utf8)! 63 | let encrypted = try encrypt(plainTextData: keyPairData, withPublicKeyFromStorageKey: storageKey) 64 | try await keychainKeyStore.setKey(networkId: networkId, accountId: accountId, withKeyPairAsData: encrypted) 65 | } 66 | 67 | public func getKey(networkId: String, accountId: String) async throws -> KeyPair? { 68 | let storageKey = storageKeyForSecretKey(networkId: networkId, accountId: accountId) 69 | guard let encryptedKey = try await keychainKeyStore.getEncryptedKey(networkId: networkId, accountId: accountId) else { 70 | return nil 71 | } 72 | 73 | let decrypted = try decrypt(cipherText: encryptedKey, withPrivateKeyFromStorageKey: storageKey) 74 | return try? keyPairFromString(encodedKey: String(decoding: decrypted, as: UTF8.self)) 75 | } 76 | 77 | public func removeKey(networkId: String, accountId: String) async throws -> Void { 78 | let storageKey = storageKeyForSecretKey(networkId: networkId, accountId: accountId) 79 | try delete(storageKey: storageKey) 80 | try await keychainKeyStore.removeKey(networkId: networkId, accountId: accountId) 81 | } 82 | 83 | public func clear() async throws -> Void { 84 | try delete(storageKey: nil) 85 | try await keychainKeyStore.clear() 86 | } 87 | 88 | private func delete(storageKey: String?) throws -> Void { 89 | var params: [String: Any] = [ 90 | kSecClass as String: kSecClassKey, 91 | kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, 92 | ] 93 | if storageKey != nil { 94 | params[kSecAttrApplicationTag as String] = storageKey! as CFString 95 | } 96 | let status = SecItemDelete(params as CFDictionary) 97 | if status != errSecSuccess && status != errSecItemNotFound { 98 | throw SecureEnclaveKeyStoreError.cannotDeleteKeys(osStatus: status) 99 | } 100 | } 101 | 102 | public func getNetworks() async throws -> [String] { 103 | return try await keychainKeyStore.getNetworks() 104 | } 105 | 106 | public func getAccounts(networkId: String) async throws -> [String] { 107 | return try await keychainKeyStore.getAccounts(networkId: networkId) 108 | } 109 | 110 | private func createPrivateKey(withStorageKey storageKey: String) throws -> SecKey { 111 | let flags: SecAccessControlCreateFlags = requireUserPresence ? [.privateKeyUsage, .userPresence] : .privateKeyUsage 112 | 113 | let access = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, flags, nil)! 114 | var attributes: [String: Any] = [ 115 | kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, 116 | kSecAttrKeySizeInBits as String: 256, 117 | kSecPrivateKeyAttrs as String: [ 118 | kSecAttrIsPermanent as String: true, 119 | kSecAttrApplicationTag as String: storageKey as CFString, 120 | kSecAttrAccessControl as String: access 121 | ] 122 | ] 123 | 124 | if SecureEnclave.isAvailable { 125 | attributes[kSecAttrTokenID as String] = kSecAttrTokenIDSecureEnclave 126 | } 127 | 128 | var error: Unmanaged? 129 | guard let privateKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error) else { 130 | throw error!.takeRetainedValue() as Swift.Error 131 | } 132 | 133 | return privateKey 134 | } 135 | 136 | private func getPrivateKey(withStorageKey storageKey: String) throws -> SecKey { 137 | var params: [String: Any] = [ 138 | kSecClass as String: kSecClassKey, 139 | kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, 140 | kSecAttrApplicationTag as String: storageKey as CFString, 141 | kSecReturnRef as String: true, 142 | ] 143 | if context != nil { 144 | params[kSecUseAuthenticationContext as String] = context 145 | } 146 | var raw: CFTypeRef? 147 | let status = SecItemCopyMatching(params as CFDictionary, &raw) 148 | guard status == errSecSuccess, let result = raw else { 149 | throw SecureEnclaveKeyStoreError.cannotAccessPrivateKey(osStatus: status) 150 | } 151 | 152 | return result as! SecKey 153 | } 154 | 155 | private func encrypt(plainTextData: Data, withPublicKey publicKey: SecKey) throws -> Data { 156 | guard SecKeyIsAlgorithmSupported(publicKey, .encrypt, algorithm) else { 157 | throw SecureEnclaveKeyStoreError.encryptionNotSupported(algorithm: algorithm) 158 | } 159 | var error: Unmanaged? 160 | guard let cipherTextData = SecKeyCreateEncryptedData(publicKey, algorithm, plainTextData as CFData, &error) as Data? else { 161 | throw error!.takeRetainedValue() as Swift.Error 162 | } 163 | return cipherTextData 164 | } 165 | 166 | func encrypt(plainTextData: Data, withPublicKeyFromStorageKey storageKey: String) throws -> Data { 167 | let privateKey: SecKey 168 | do { 169 | privateKey = try getPrivateKey(withStorageKey: storageKey) 170 | } catch { 171 | privateKey = try createPrivateKey(withStorageKey: storageKey) 172 | } 173 | 174 | guard let publicKey = SecKeyCopyPublicKey(privateKey) else { throw SecureEnclaveKeyStoreError.cannotAccessPublicKey } 175 | return try encrypt(plainTextData: plainTextData, withPublicKey: publicKey) 176 | } 177 | 178 | private func decrypt(cipherText: Data, privateKey: SecKey) throws -> Data { 179 | guard SecKeyIsAlgorithmSupported(privateKey, .decrypt, algorithm) else { 180 | throw SecureEnclaveKeyStoreError.decryptionNotSupported(algorithm: algorithm) 181 | } 182 | 183 | var error: Unmanaged? 184 | guard let plainTextData = SecKeyCreateDecryptedData(privateKey, algorithm, cipherText as CFData, &error) as Data? else { 185 | throw error!.takeRetainedValue() as Swift.Error 186 | } 187 | 188 | return plainTextData 189 | } 190 | 191 | func decrypt(cipherText: Data, withPrivateKeyFromStorageKey storageKey: String) throws -> Data { 192 | let privateKey = try getPrivateKey(withStorageKey: storageKey) 193 | return try decrypt(cipherText: cipherText, privateKey: privateKey) 194 | } 195 | 196 | private func storageKeyForSecretKey(networkId: String, accountId: String) -> String { 197 | return "enc:\(accountId):\(networkId)" 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /nearclientios/Sources/Near.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Near.swift 3 | // nearclientios 4 | // 5 | // Created by Dmitry Kurochka on 10/30/19. 6 | // Copyright © 2019 NEAR Protocol. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol NearConfigProtocol: ConnectionConfigProtocol { 12 | var networkId: String {get} 13 | var nodeUrl: URL {get} 14 | var masterAccount: String? {get set} 15 | var keyPath: String? {get} 16 | var helperUrl: URL? {get} 17 | var initialBalance: UInt128? {get} 18 | var keyStore: KeyStore? {get set} 19 | var contractName: String? {get} 20 | var walletUrl: String {get} 21 | } 22 | 23 | public struct NearConfig: NearConfigProtocol { 24 | public let networkId: String 25 | public let nodeUrl: URL 26 | public var masterAccount: String? 27 | public let keyPath: String? 28 | public let helperUrl: URL? 29 | public let initialBalance: UInt128? 30 | public let providerType: ProviderType 31 | public let signerType: SignerType 32 | public var keyStore: KeyStore? 33 | public let contractName: String? 34 | public let walletUrl: String 35 | 36 | public init(networkId: String, nodeUrl: URL, masterAccount: String?, keyPath: String?, helperUrl: URL?, initialBalance: UInt128?, providerType: ProviderType, signerType: SignerType, keyStore: KeyStore?, contractName: String?, walletUrl: String) { 37 | self.networkId = networkId 38 | self.nodeUrl = nodeUrl 39 | self.masterAccount = masterAccount 40 | self.keyPath = keyPath 41 | self.helperUrl = helperUrl 42 | self.initialBalance = initialBalance 43 | self.providerType = providerType 44 | self.signerType = signerType 45 | self.keyStore = keyStore 46 | self.contractName = contractName 47 | self.walletUrl = walletUrl 48 | } 49 | } 50 | 51 | public struct Near { 52 | public let config: NearConfigProtocol 53 | public let connection: Connection 54 | private let accountCreator: AccountCreator? 55 | } 56 | 57 | public enum NearError: Error { 58 | case noAccountCreator(String) 59 | case noAccountId 60 | } 61 | 62 | extension Near { 63 | 64 | public init(config: NearConfigProtocol) { 65 | let connection = Connection.fromConfig(config: config) 66 | var accountCreator: AccountCreator? 67 | if let masterAccount = config.masterAccount { 68 | // TODO: figure out better way of specifiying initial balance. 69 | let initialBalance = config.initialBalance ?? UInt128(1000000000000) 70 | let masterAccount = Account(connection: connection, accountId: masterAccount) 71 | accountCreator = LocalAccountCreator(masterAccount: masterAccount, initialBalance: initialBalance) 72 | } else if let url = config.helperUrl { 73 | accountCreator = UrlAccountCreator(connection: connection, helperUrl: url) 74 | } 75 | self.init(config: config, connection: connection, accountCreator: accountCreator) 76 | } 77 | 78 | // expose global functions as static functions in the Near namespace 79 | public static func keyPairFromString(_ string: String) throws -> KeyPair { 80 | return try nearclientios.keyPairFromString(encodedKey: string) 81 | } 82 | 83 | // expose global functions as static functions in the Near namespace 84 | public static func keyPairFromRandom(curve: KeyType) throws -> KeyPair { 85 | return try nearclientios.keyPairFromRandom(curve: curve) 86 | } 87 | 88 | } 89 | 90 | public extension Near { 91 | func account(accountId: String) async throws -> Account { 92 | let account = Account(connection: connection, accountId: accountId) 93 | try await account.ready() 94 | return account 95 | } 96 | 97 | private func createAccount(accountId: String, publicKey: PublicKey) async throws -> Account { 98 | guard let accountCreator = accountCreator else { 99 | throw NearError.noAccountCreator("Must specify account creator, either via masterAccount or helperUrl configuration settings.") 100 | } 101 | try await accountCreator.createAccount(newAccountId: accountId, publicKey: publicKey) 102 | return Account(connection: connection, accountId: accountId) 103 | } 104 | 105 | /** 106 | - Parameters: 107 | - contractId: contractId 108 | - options: options 109 | - Returns: promise with contract. 110 | */ 111 | @available(*, deprecated, renamed: "Contract.init", message: "Backwards compatibility method. Use contract constructor instead") 112 | private func loadContract(contractId: String, options: ContractOptionsProtocol) async throws -> Contract { 113 | print("near.loadContract is deprecated. Use `Contract.init` instead.") 114 | guard let accountId = options.sender else { throw NearError.noAccountId } 115 | let account = Account(connection: connection, accountId: accountId) 116 | let contract = Contract(account: account, contractId: contractId, viewMethods: options.viewMethods, 117 | changeMethods: options.changeMethods, sender: accountId) 118 | return contract 119 | } 120 | 121 | /** 122 | - Parameters: 123 | - amount: amount 124 | - originator: originator 125 | - receiver: receiver 126 | */ 127 | @available(*, deprecated, renamed: "yourAccount.sendMoney", message: "Backwards compatibility method. Use `yourAccount.sendMoney` instead") 128 | func sendTokens(amount: UInt128, originator: String, receiver: String) async throws -> String { 129 | print("near.sendTokens is deprecated. Use `yourAccount.sendMoney` instead.") 130 | let account = Account(connection: connection, accountId: originator) 131 | let result = try await account.sendMoney(receiverId: receiver, amount: amount) 132 | return result.transactionOutcome.id 133 | } 134 | } 135 | 136 | func connect(config: NearConfigProtocol) async throws -> Near { 137 | // Try to find extra key in `KeyPath` if provided.let 138 | var configuration = config 139 | if let keyPath = configuration.keyPath, let keyStore = configuration.keyStore { 140 | do { 141 | let (accountId, keyPair) = try await UnencryptedFileSystemKeyStore.readKeyFile(path: keyPath) 142 | // TODO: Only load key if network ID matches 143 | let keyPathStore = InMemoryKeyStore() 144 | try await keyPathStore.setKey(networkId: configuration.networkId, accountId: accountId, keyPair: keyPair) 145 | if configuration.masterAccount == nil { 146 | configuration.masterAccount = accountId 147 | } 148 | configuration.keyStore = MergeKeyStore(keyStores: [keyStore, keyPathStore]) 149 | print("Loaded master account \(accountId) key from \(keyPath) with public key = \(keyPair.getPublicKey())") 150 | } catch let error { 151 | print("Failed to load master account key from \(keyPath): \(error)") 152 | } 153 | } 154 | let near = Near(config: configuration) 155 | return near 156 | } 157 | -------------------------------------------------------------------------------- /nearclientios/Sources/Providers/JSONRPCProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSONRPCProvider.swift 3 | // nearclientios 4 | // 5 | // Created by Dmitry Kurochka on 10/30/19. 6 | // Copyright © 2019 NEAR Protocol. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import AnyCodable 11 | 12 | public enum TypedError: Error { 13 | case error(type: String = "UntypedError", message: String?) 14 | } 15 | 16 | public enum Finality: String, Codable { 17 | case final 18 | case optimistic 19 | } 20 | 21 | public enum SyncCheckpoint: String, Codable { 22 | case genesis = "genesis" 23 | case earliestAvailable = "earliest_available" 24 | } 25 | 26 | public final class JSONRPCProvider { 27 | /// Keep ids unique across all connections 28 | private var _nextId = 123 29 | 30 | private let connection: ConnectionInfo 31 | 32 | init(url: URL, network: Network? = nil) { 33 | self.connection = ConnectionInfo(url: url) 34 | } 35 | } 36 | 37 | extension JSONRPCProvider { 38 | private func getId() -> Int { 39 | _nextId += 1 40 | return _nextId 41 | } 42 | 43 | private func sendJsonRpc(method: String, params: [Any?]) async throws -> T { 44 | let request: [String: Any] = ["method": method, 45 | "params": params, 46 | "id": getId(), 47 | "jsonrpc": "2.0"] 48 | let json = try await fetchJson(connection: connection, json: request) 49 | return try await processJsonRpc(request: request, json: json) 50 | } 51 | 52 | private func sendJsonRpc(method: String, paramsDict: [String: Any]) async throws -> T { 53 | let request: [String: Any] = ["method": method, 54 | "params": paramsDict, 55 | "id": getId(), 56 | "jsonrpc": "2.0"] 57 | let json = try await fetchJson(connection: connection, json: request) 58 | return try await processJsonRpc(request: request, json: json) 59 | } 60 | 61 | func processJsonRpc(request: [String: Any], json: Any) async throws -> T { 62 | let data = try JSONSerialization.data(withJSONObject: json, options: []) 63 | do { 64 | let decoder = JSONDecoder() 65 | decoder.keyDecodingStrategy = .convertFromSnakeCase 66 | let decoded = try decoder.decode(T.self, from: data) 67 | return decoded 68 | } catch let originalError { 69 | // see if this error can be parsed into a standard RPCError, and return that if possible 70 | do { 71 | let decoder = JSONDecoder() 72 | decoder.keyDecodingStrategy = .convertFromSnakeCase 73 | let decodedError = try decoder.decode(RPCError.self, from: data) 74 | throw decodedError 75 | } catch { 76 | print(originalError) 77 | print(String(decoding: try! JSONSerialization.data(withJSONObject: request, options: []), as: UTF8.self)) 78 | print(String(decoding: data, as: UTF8.self)) 79 | print(T.self) 80 | throw originalError 81 | } 82 | } 83 | } 84 | } 85 | 86 | extension JSONRPCProvider: Provider { 87 | public func getNetwork() async -> Network { 88 | let result: Network = Network(name: "test", chainId: "test") 89 | return result 90 | } 91 | 92 | public func status() async throws -> NodeStatusResult { 93 | return try await sendJsonRpc(method: "status", params: []) 94 | } 95 | 96 | public func networkInfo() async throws -> NetworkInfoResult { 97 | return try await sendJsonRpc(method: "network_info", params: []) 98 | } 99 | 100 | public func sendTransaction(signedTransaction: SignedTransaction) async throws -> FinalExecutionOutcome { 101 | let data = try BorshEncoder().encode(signedTransaction) 102 | let params = [data.base64EncodedString()] 103 | // debugPrint("params \(params)") 104 | return try await sendJsonRpc(method: "broadcast_tx_commit", params: params) 105 | } 106 | 107 | public func sendTransactionAsync(signedTransaction: SignedTransaction) async throws -> SimpleRPCResult { 108 | let data = try BorshEncoder().encode(signedTransaction) 109 | let params = [data.base64EncodedString()] 110 | // debugPrint("params \(params)") 111 | return try await sendJsonRpc(method: "broadcast_tx_async", params: params) 112 | } 113 | 114 | public func txStatus(txHash: [UInt8], accountId: String) async throws -> FinalExecutionOutcome { 115 | let params = [txHash.baseEncoded, accountId] 116 | return try await sendJsonRpc(method: "tx", params: params) 117 | } 118 | 119 | public func experimentalTxStatusWithReceipts(txHash: [UInt8], accountId: String) async throws -> FinalExecutionOutcome { 120 | let params = [txHash.baseEncoded, accountId] 121 | return try await sendJsonRpc(method: "EXPERIMENTAL_tx_status", params: params) 122 | } 123 | 124 | public func query(params: [String: Any]) async throws -> T { 125 | return try await sendJsonRpc(method: "query", paramsDict: params) 126 | } 127 | 128 | public func block(blockQuery: BlockReference) async throws -> BlockResult { 129 | let params: [String: Any] = typeEraseBlockReferenceParams(blockQuery: blockQuery) 130 | return try await sendJsonRpc(method: "block", paramsDict: params) 131 | } 132 | 133 | public func blockChanges(blockQuery: BlockReference) async throws -> BlockChangeResult { 134 | let params: [String: Any] = typeEraseBlockReferenceParams(blockQuery: blockQuery) 135 | return try await sendJsonRpc(method: "EXPERIMENTAL_changes_in_block", paramsDict: params) 136 | } 137 | 138 | public func chunk(chunkId: ChunkId) async throws -> ChunkResult { 139 | var params: [String: Any] = [:] 140 | switch chunkId { 141 | case .chunkHash(let chunkHash): 142 | params["chunk_id"] = chunkHash 143 | case .blockShardId(let blockShardId): 144 | params["block_id"] = typeEraseBlockId(blockId: blockShardId.blockId) 145 | params["shard_id"] = blockShardId.shardId 146 | } 147 | return try await sendJsonRpc(method: "chunk", paramsDict: params) 148 | } 149 | 150 | public func gasPrice(blockId: NullableBlockId) async throws -> GasPrice { 151 | let params: Any? = typeEraseNullableBlockId(blockId: blockId) 152 | return try await sendJsonRpc(method: "gas_price", params: [params]) 153 | } 154 | 155 | public func experimentalGenesisConfig() async throws -> ExperimentalNearProtocolConfig { 156 | return try await sendJsonRpc(method: "EXPERIMENTAL_genesis_config", params: []) 157 | } 158 | 159 | public func experimentalProtocolConfig(blockQuery: BlockReference) async throws -> ExperimentalNearProtocolConfig { 160 | let params: [String: Any] = typeEraseBlockReferenceParams(blockQuery: blockQuery) 161 | return try await sendJsonRpc(method: "EXPERIMENTAL_protocol_config", paramsDict: params) 162 | } 163 | 164 | public func validators(blockId: NullableBlockId) async throws -> EpochValidatorInfo { 165 | let params: Any? = typeEraseNullableBlockId(blockId: blockId) 166 | return try await sendJsonRpc(method: "validators", params: [params]) 167 | } 168 | 169 | public func accessKeyChanges(accountIdArray: [String], blockQuery: BlockReference) async throws -> ChangeResult { 170 | var params: [String: Any] = typeEraseBlockReferenceParams(blockQuery: blockQuery) 171 | params["changes_type"] = "all_access_key_changes" 172 | params["account_ids"] = accountIdArray 173 | 174 | return try await sendJsonRpc(method: "EXPERIMENTAL_changes", paramsDict: params) 175 | } 176 | 177 | public func singleAccessKeyChanges(accessKeyArray: [AccessKeyWithPublicKey], blockQuery: BlockReference) async throws -> ChangeResult { 178 | var params: [String: Any] = typeEraseBlockReferenceParams(blockQuery: blockQuery) 179 | params["changes_type"] = "single_access_key_changes" 180 | params["keys"] = accessKeyArray.map { value in 181 | return [ 182 | "account_id": value.accountId, 183 | "public_key": value.publicKey 184 | ] 185 | } 186 | 187 | return try await sendJsonRpc(method: "EXPERIMENTAL_changes", paramsDict: params) 188 | } 189 | public func accountChanges(accountIdArray: [String], blockQuery: BlockReference) async throws -> ChangeResult { 190 | var params: [String: Any] = typeEraseBlockReferenceParams(blockQuery: blockQuery) 191 | params["changes_type"] = "account_changes" 192 | params["account_ids"] = accountIdArray 193 | 194 | return try await sendJsonRpc(method: "EXPERIMENTAL_changes", paramsDict: params) 195 | } 196 | 197 | public func contractStateChanges(accountIdArray: [String], blockQuery: BlockReference, keyPrefix: String?) async throws -> ChangeResult { 198 | var params: [String: Any] = typeEraseBlockReferenceParams(blockQuery: blockQuery) 199 | params["changes_type"] = "data_changes" 200 | params["account_ids"] = accountIdArray 201 | params["key_prefix_base64"] = keyPrefix ?? "" 202 | 203 | return try await sendJsonRpc(method: "EXPERIMENTAL_changes", paramsDict: params) 204 | } 205 | 206 | public func contractCodeChanges(accountIdArray: [String], blockQuery: BlockReference) async throws -> ChangeResult { 207 | var params: [String: Any] = typeEraseBlockReferenceParams(blockQuery: blockQuery) 208 | params["changes_type"] = "contract_code_changes" 209 | params["account_ids"] = accountIdArray 210 | 211 | return try await sendJsonRpc(method: "EXPERIMENTAL_changes", paramsDict: params) 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /nearclientios/Sources/Providers/Provider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Provider.swift 3 | // nearclientios 4 | // 5 | // Created by Dmitry Kurochka on 10/30/19. 6 | // Copyright © 2019 NEAR Protocol. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import AnyCodable 11 | 12 | public struct SyncInfo: Codable { 13 | let latestBlockHash: String 14 | let latestBlockHeight: Int 15 | let latestBlockTime: String 16 | let latestStateRoot: String 17 | let syncing: Bool 18 | } 19 | 20 | public struct Validator: Codable {} 21 | 22 | public struct NodeStatusResult: Codable { 23 | let chainId: String 24 | let rpcAddr: String 25 | let syncInfo: SyncInfo 26 | let validators: [Validator] 27 | } 28 | 29 | public struct NetworkInfoResult: Decodable { 30 | let peerMaxCount: Int 31 | } 32 | 33 | public struct SimpleRPCResult: Decodable { 34 | public let id: String 35 | public let jsonrpc: String 36 | private let result: String 37 | 38 | public var hash: String { 39 | return result 40 | } 41 | 42 | } 43 | 44 | public typealias BlockHash = String 45 | public typealias BlockHeight = Int 46 | public enum BlockId { 47 | case blockHash(String) 48 | case blockHeight(Int) 49 | } 50 | public enum NullableBlockId { 51 | case blockHash(String) 52 | case blockHeight(Int) 53 | case null 54 | } 55 | 56 | public func typeEraseNullableBlockId(blockId: NullableBlockId) -> Any? { 57 | switch blockId { 58 | case .blockHeight(let height): 59 | return height 60 | case .blockHash(let hash): 61 | return hash 62 | case .null: 63 | return nil 64 | } 65 | } 66 | 67 | public enum BlockReference { 68 | case blockId(BlockId) 69 | case finality(Finality) 70 | } 71 | 72 | public func typeEraseBlockId(blockId: BlockId) -> Any { 73 | switch blockId { 74 | case .blockHeight(let height): 75 | return height 76 | case .blockHash(let hash): 77 | return hash 78 | } 79 | } 80 | 81 | public func typeEraseBlockReferenceParams(blockQuery: BlockReference) -> [String: Any] { 82 | var params: [String: Any] = [:] 83 | switch blockQuery { 84 | case .blockId(let blockId): 85 | params["block_id"] = typeEraseBlockId(blockId: blockId) 86 | case .finality(let finality): 87 | params["finality"] = finality.rawValue 88 | } 89 | 90 | return params 91 | } 92 | 93 | public struct AccessKeyWithPublicKey: Codable { 94 | let accountId: String 95 | let publicKey: String 96 | } 97 | 98 | public enum ExecutionStatusBasic: String, Decodable { 99 | case unknown = "Unknown" 100 | case pending = "Pending" 101 | case failure = "Failure" 102 | } 103 | 104 | public enum ExecutionStatus: Decodable, Equatable { 105 | case successValue(String) 106 | case basic(ExecutionStatusBasic) 107 | case successReceiptId(String) 108 | case failure(ExecutionError) 109 | 110 | private enum CodingKeys: String, CodingKey { 111 | case successValue = "SuccessValue" 112 | case failure = "Failure" 113 | case successReceiptId = "SuccessReceiptId" 114 | } 115 | 116 | public init(from decoder: Decoder) throws { 117 | if let container = try? decoder.singleValueContainer(), let status = try? container.decode(ExecutionStatusBasic.self) { 118 | self = .basic(status) 119 | return 120 | } 121 | let container = try? decoder.container(keyedBy: CodingKeys.self) 122 | if let value = try? container?.decode(String.self, forKey: .successValue) { 123 | self = .successValue(value) 124 | return 125 | } 126 | if let value = try? container?.decode(String.self, forKey: .successReceiptId) { 127 | self = .successReceiptId(value) 128 | return 129 | } 130 | if let value = try? container?.decode(ExecutionError.self, forKey: .failure) { 131 | self = .failure(value) 132 | return 133 | } 134 | throw NEARDecodingError.notExpected 135 | } 136 | } 137 | 138 | public enum FinalExecutionStatusBasic: String, Codable { 139 | case notStarted = "NotStarted" 140 | case started = "Started" 141 | case failure = "Failure" 142 | } 143 | 144 | public struct ExecutionError: Codable, Equatable{ 145 | let errorMessage: String? 146 | let errorType: String? 147 | 148 | init(errorMessage: String? = nil, errorType: String? = nil) { 149 | self.errorMessage = errorMessage 150 | self.errorType = errorType 151 | } 152 | } 153 | 154 | public enum FinalExecutionStatus: Decodable, Equatable { 155 | case successValue(String) 156 | case basic(ExecutionStatusBasic) 157 | case failure(ExecutionError) 158 | 159 | private enum CodingKeys: String, CodingKey { 160 | case successValue = "SuccessValue" 161 | case failure = "Failure" 162 | } 163 | 164 | public init(from decoder: Decoder) throws { 165 | if let container = try? decoder.singleValueContainer(), let status = try? container.decode(ExecutionStatusBasic.self) { 166 | self = .basic(status) 167 | return 168 | } 169 | let container = try? decoder.container(keyedBy: CodingKeys.self) 170 | if let value = try? container?.decode(String.self, forKey: .successValue) { 171 | self = .successValue(value) 172 | return 173 | } 174 | if let value = try? container?.decode(ExecutionError.self, forKey: .failure) { 175 | self = .failure(value) 176 | return 177 | } 178 | throw NEARDecodingError.notExpected 179 | } 180 | } 181 | 182 | public struct ExecutionOutcomeWithId: Decodable, Equatable { 183 | public let id: String 184 | public let outcome: ExecutionOutcome 185 | } 186 | 187 | public struct ExecutionOutcome: Decodable, Equatable { 188 | public let status: ExecutionStatus 189 | public let logs: [String] 190 | public let receiptIds: [String] 191 | public let gasBurnt: Int 192 | } 193 | 194 | public struct FinalExecutionOutcome: Decodable, Equatable { 195 | public let transaction: Transaction 196 | public let status: FinalExecutionStatus 197 | public let transactionOutcome: ExecutionOutcomeWithId 198 | public let receiptsOutcome: [ExecutionOutcomeWithId] 199 | public let receipts: AnyDecodable? 200 | } 201 | 202 | public struct TotalWeight: Codable { 203 | let num: Int 204 | } 205 | 206 | public struct BlockHeader: Codable { 207 | let height: Int 208 | let epochId: String 209 | let nextEpochId: String 210 | let hash: String 211 | let prevHash: String 212 | let prevStateRoot: String 213 | let chunkReceiptsRoot: String 214 | let chunkHeadersRoot: String 215 | let chunkTxRoot: String 216 | let outcomeRoot: String 217 | let chunksIncluded: Int 218 | let challengesRoot: String 219 | let timestamp: Int 220 | let timestampNanosec: String 221 | let randomValue: String 222 | let validatorProposals: [ValidatorProposal] 223 | let chunkMask: [Bool] 224 | let gasPrice: String 225 | let rentPaid: String 226 | let validatorReward: String 227 | let totalSupply: String 228 | //let challenges_result: [Any] 229 | let lastFinalBlock: String 230 | let lastDsFinalBlock: String 231 | let nextBpHash: String 232 | let blockMerkleRoot: String 233 | } 234 | 235 | public typealias ChunkHash = String 236 | public typealias ShardId = Int 237 | public struct BlockShardId { 238 | let blockId: BlockId 239 | let shardId: ShardId 240 | } 241 | 242 | public enum ChunkId { 243 | case chunkHash(ChunkHash) 244 | case blockShardId(BlockShardId) 245 | } 246 | 247 | public struct ValidatorProposal: Codable {} 248 | 249 | public struct ChunkHeader: Codable { 250 | let chunkHash: ChunkHash 251 | let prevBlockHash: String 252 | let outcomeRoot: String 253 | let prevStateRoot: String 254 | let encodedMerkleRoot: String 255 | let encodedLength: Int 256 | let heightCreated: Int 257 | let heightIncluded: Int 258 | let shardId: ShardId 259 | let gasUsed: Int 260 | let gasLimit: Int 261 | let rentPaid: String 262 | let validatorReward: String 263 | let balanceBurnt: String 264 | let outgoingReceiptsRoot: String 265 | let txRoot: String 266 | let validatorProposals: [ValidatorProposal] 267 | let signature: String 268 | } 269 | 270 | public struct Receipt: Codable {} 271 | 272 | public struct ChunkResult: Codable { 273 | let header: ChunkHeader 274 | let receipts: [Receipt] 275 | let transactions: [Transaction] 276 | } 277 | 278 | public struct Transaction: Codable, Equatable { 279 | public let hash: String 280 | public let publicKey: String 281 | public let signature: String 282 | } 283 | 284 | public struct BlockResult: Codable { 285 | let header: BlockHeader 286 | let transactions: [Transaction]? 287 | } 288 | 289 | public struct BlockChange: Codable { 290 | let type: String 291 | let accountId: String 292 | } 293 | 294 | public struct BlockChangeResult: Codable { 295 | let blockHash: String 296 | let changes: [BlockChange] 297 | } 298 | 299 | public struct ChangeResult: Decodable { 300 | let blockHash: String 301 | let changes: [AnyDecodable] 302 | } 303 | 304 | public struct ExperimentalNearProtocolConfig: Decodable { 305 | let chainId: String 306 | let genesisHeight: Int 307 | let runtimeConfig: ExperimentalNearProtocolRuntimeConfig? 308 | } 309 | 310 | public struct ExperimentalNearProtocolRuntimeConfig: Decodable { 311 | let storageAmountPerByte: String 312 | } 313 | 314 | public struct GasPrice: Codable { 315 | let gasPrice: String 316 | } 317 | 318 | public struct EpochValidatorInfo: Decodable { 319 | // Validators for the current epoch. 320 | let nextValidators: [NextEpochValidatorInfo] 321 | // Validators for the next epoch. 322 | let currentValidators: [CurrentEpochValidatorInfo] 323 | // Fishermen for the current epoch. 324 | let nextFishermen: [ValidatorStakeView] 325 | // Fishermen for the next epoch. 326 | let currentFishermen: [ValidatorStakeView] 327 | // Proposals in the current epoch. 328 | let currentProposals: [ValidatorStakeView] 329 | // Kickout in the previous epoch. 330 | let prevEpochKickout: [ValidatorStakeView] 331 | // Epoch start height. 332 | let epochStartHeight: Int 333 | } 334 | 335 | public struct CurrentEpochValidatorInfo: Decodable { 336 | let accountId: String 337 | let publicKey: String 338 | let isSlashed: Bool 339 | let stake: String 340 | let shards: [Int] 341 | let numProducedBlocks: Int 342 | let numExpectedBlocks: Int 343 | } 344 | 345 | public struct NextEpochValidatorInfo: Decodable { 346 | let accountId: String 347 | let publicKey: String 348 | let stake: String 349 | let shards: [Int] 350 | } 351 | 352 | public struct ValidatorStakeView: Decodable { 353 | let accountId: String 354 | let publicKey: String 355 | let stake: String 356 | } 357 | 358 | public enum ProviderType { 359 | case jsonRPC(URL) 360 | } 361 | 362 | public protocol Provider { 363 | func getNetwork() async throws -> Network 364 | func status() async throws -> NodeStatusResult 365 | func networkInfo() async throws -> NetworkInfoResult 366 | func sendTransaction(signedTransaction: SignedTransaction) async throws -> FinalExecutionOutcome 367 | func sendTransactionAsync(signedTransaction: SignedTransaction) async throws -> SimpleRPCResult 368 | func txStatus(txHash: [UInt8], accountId: String) async throws -> FinalExecutionOutcome 369 | func experimentalTxStatusWithReceipts(txHash: [UInt8], accountId: String) async throws -> FinalExecutionOutcome 370 | func query(params: [String: Any]) async throws -> T 371 | func block(blockQuery: BlockReference) async throws -> BlockResult 372 | func blockChanges(blockQuery: BlockReference) async throws -> BlockChangeResult 373 | func chunk(chunkId: ChunkId) async throws -> ChunkResult 374 | func gasPrice(blockId: NullableBlockId) async throws -> GasPrice 375 | func experimentalGenesisConfig() async throws -> ExperimentalNearProtocolConfig 376 | func experimentalProtocolConfig(blockQuery: BlockReference) async throws -> ExperimentalNearProtocolConfig 377 | func validators(blockId: NullableBlockId) async throws -> EpochValidatorInfo 378 | func accessKeyChanges(accountIdArray: [String], blockQuery: BlockReference) async throws -> ChangeResult 379 | func singleAccessKeyChanges(accessKeyArray: [AccessKeyWithPublicKey], blockQuery: BlockReference) async throws -> ChangeResult 380 | func accountChanges(accountIdArray: [String], blockQuery: BlockReference) async throws -> ChangeResult 381 | func contractStateChanges(accountIdArray: [String], blockQuery: BlockReference, keyPrefix: String?) async throws -> ChangeResult 382 | func contractCodeChanges(accountIdArray: [String], blockQuery: BlockReference) async throws -> ChangeResult 383 | } 384 | 385 | public func getTransactionLastResult(txResult: FinalExecutionOutcome) -> Any? { 386 | if case .successValue(let value) = txResult.status, let data = Data(base64Encoded: value) { 387 | do { 388 | return try JSONSerialization.jsonObject(with: data, options: []) 389 | } catch { 390 | return String(data: data, 391 | encoding: .utf8)?.trimmingCharacters(in: CharacterSet(charactersIn: "\"")) 392 | } 393 | } 394 | return nil 395 | } 396 | -------------------------------------------------------------------------------- /nearclientios/Sources/Signer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Signer.swift 3 | // nearclientios 4 | // 5 | // Created by Dmitry Kurochka on 10/30/19. 6 | // Copyright © 2019 NEAR Protocol. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum SignerType { 12 | case inMemory(KeyStore) 13 | } 14 | 15 | /** 16 | General signing interface, can be used for in memory signing, RPC singing, external wallet, HSM, etc. 17 | */ 18 | public protocol Signer { 19 | 20 | /** 21 | Creates new key and returns public key. 22 | - Parameters: 23 | - accountId: accountId to retrieve from. 24 | - networkId: network for this accountId. 25 | */ 26 | func createKey(accountId: String, networkId: String, curve: KeyType) async throws -> PublicKey 27 | 28 | /** 29 | - Parameters: 30 | - accountId: accountId to retrieve from. 31 | - networkId: network for this accountId. 32 | - Returns: public key for given account / network. 33 | */ 34 | func getPublicKey(accountId: String, networkId: String) async throws -> PublicKey? 35 | 36 | /** 37 | Signs given hash. 38 | - Parameters: 39 | - hash: hash to sign. 40 | - accountId: accountId to use for signing. 41 | - networkId: network for this accontId. 42 | */ 43 | func signHash(hash: [UInt8], accountId: String, networkId: String) async throws -> SignatureProtocol 44 | 45 | /** 46 | Signs given message, by first hashing with sha256. 47 | - Parameters: 48 | - message: message to sign. 49 | - accountId: accountId to use for signing. 50 | - networkId: network for this accontId. 51 | */ 52 | func signMessage(message: [UInt8], accountId: String, networkId: String) async throws -> SignatureProtocol 53 | } 54 | 55 | extension Signer { 56 | public func signMessage(message: [UInt8], accountId: String, networkId: String) async throws -> SignatureProtocol { 57 | return try await signHash(hash: message.digest, accountId: accountId, networkId: networkId) 58 | } 59 | } 60 | 61 | /** 62 | * Signs using in memory key store. 63 | */ 64 | public struct InMemorySigner { 65 | let keyStore: KeyStore 66 | 67 | public init(keyStore: KeyStore) { 68 | self.keyStore = keyStore 69 | } 70 | } 71 | 72 | public enum InMemorySignerError: Error { 73 | case notFound(String) 74 | } 75 | 76 | extension InMemorySigner: Signer { 77 | public func createKey(accountId: String, networkId: String, curve: KeyType = .ED25519) async throws -> PublicKey { 78 | let keyPair = try keyPairFromRandom(curve: curve) 79 | try await keyStore.setKey(networkId: networkId, accountId: accountId, keyPair: keyPair) 80 | return keyPair.getPublicKey() 81 | } 82 | 83 | public func getPublicKey(accountId: String, networkId: String) async throws -> PublicKey? { 84 | let keyPair = try await keyStore.getKey(networkId: networkId, accountId: accountId) 85 | return keyPair?.getPublicKey() 86 | } 87 | 88 | public func signHash(hash: [UInt8], accountId: String, networkId: String) async throws -> SignatureProtocol { 89 | guard let keyPair = try await keyStore.getKey(networkId: networkId, accountId: accountId) else { 90 | throw InMemorySignerError.notFound("Key for \(accountId) not found in \(networkId)") 91 | } 92 | let signature = try keyPair.sign(message: hash) 93 | return signature 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /nearclientios/Sources/Utils/AppInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppInfo.swift 3 | // nearclientios 4 | // 5 | // Created by Dmytro Kurochka on 10.12.2019. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | extension UIApplication { 12 | static var urlSchemes: [String]? { 13 | return (Bundle.main.object(forInfoDictionaryKey: "CFBundleURLTypes") as? [[String: Any]])?.first?["CFBundleURLSchemes"] as? [String] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /nearclientios/Sources/Utils/Borsh/BinaryReader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BinaryReader.swift 3 | // nearclientios 4 | // 5 | // Created by Dmytro Kurochka on 22.11.2019. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct BinaryReader { 11 | private var cursor: Int 12 | private let bytes: [UInt8] 13 | 14 | init(bytes: [UInt8]) { 15 | self.cursor = 0 16 | self.bytes = bytes 17 | } 18 | } 19 | 20 | extension BinaryReader { 21 | mutating func read(count: UInt32) -> [UInt8] { 22 | let newPosition = cursor + Int(count) 23 | let result = bytes[cursor..(_ type: T.Type, from data: Data) throws -> T where T : BorshDeserializable { 13 | var reader = BinaryReader(bytes: [UInt8](data)) 14 | return try T.init(from: &reader) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /nearclientios/Sources/Utils/Borsh/BorshDeserialize.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BorshDeserialize.swift 3 | // nearclientios 4 | // 5 | // Created by Dmytro Kurochka on 21.11.2019. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol BorshDeserializable { 11 | init(from reader: inout BinaryReader) throws 12 | } 13 | 14 | enum DeserializationError: Error { 15 | case noData 16 | } 17 | 18 | public extension FixedWidthInteger { 19 | init(from reader: inout BinaryReader) throws { 20 | var value: Self = .zero 21 | let bytes = reader.read(count: UInt32(MemoryLayout.size)) 22 | let size = withUnsafeMutableBytes(of: &value, { bytes.copyBytes(to: $0) } ) 23 | assert(size == MemoryLayout.size) 24 | self = Self(littleEndian: value) 25 | } 26 | } 27 | 28 | extension UInt8: BorshDeserializable {} 29 | extension UInt16: BorshDeserializable {} 30 | extension UInt32: BorshDeserializable {} 31 | extension UInt64: BorshDeserializable {} 32 | extension UInt128: BorshDeserializable {} 33 | extension Int8: BorshDeserializable {} 34 | extension Int16: BorshDeserializable {} 35 | extension Int32: BorshDeserializable {} 36 | extension Int64: BorshDeserializable {} 37 | extension Int128: BorshDeserializable {} 38 | 39 | extension Float32: BorshDeserializable { 40 | public init(from reader: inout BinaryReader) throws { 41 | var value: Self = .zero 42 | let bytes = reader.read(count: UInt32(MemoryLayout.size)) 43 | let size = withUnsafeMutableBytes(of: &value, { bytes.copyBytes(to: $0) } ) 44 | assert(size == MemoryLayout.size) 45 | assert(!value.isNaN, "For portability reasons we do not allow to deserialize NaNs.") 46 | self = value 47 | } 48 | } 49 | 50 | extension Float64: BorshDeserializable { 51 | public init(from reader: inout BinaryReader) throws { 52 | var value: Self = .zero 53 | let bytes = reader.read(count: UInt32(MemoryLayout.size)) 54 | let size = withUnsafeMutableBytes(of: &value, { bytes.copyBytes(to: $0) } ) 55 | assert(size == MemoryLayout.size) 56 | assert(!value.isNaN, "For portability reasons we do not allow to deserialize NaNs.") 57 | self = value 58 | } 59 | } 60 | 61 | extension Bool: BorshDeserializable { 62 | public init(from reader: inout BinaryReader) throws { 63 | var value: Self = false 64 | let bytes = reader.read(count: UInt32(MemoryLayout.size)) 65 | let size = withUnsafeMutableBytes(of: &value, { bytes.copyBytes(to: $0) } ) 66 | assert(size == MemoryLayout.size) 67 | self = value 68 | } 69 | } 70 | 71 | extension Optional where Wrapped: BorshDeserializable { 72 | init(from reader: inout BinaryReader) throws { 73 | let isSomeValue: UInt8 = try .init(from: &reader) 74 | switch isSomeValue { 75 | case 1: self = try Wrapped.init(from: &reader) 76 | default: self = .none 77 | } 78 | } 79 | } 80 | 81 | extension String: BorshDeserializable { 82 | public init(from reader: inout BinaryReader) throws { 83 | let count: UInt32 = try .init(from: &reader) 84 | let bytes = reader.read(count: count) 85 | guard let value = String(bytes: bytes, encoding: .utf8) else {throw DeserializationError.noData} 86 | self = value 87 | } 88 | } 89 | 90 | extension Array: BorshDeserializable where Element: BorshDeserializable { 91 | public init(from reader: inout BinaryReader) throws { 92 | let count: UInt32 = try .init(from: &reader) 93 | self = try Array(0...init(from: &reader)) 100 | } 101 | } 102 | 103 | extension Dictionary: BorshDeserializable where Key: BorshDeserializable & Equatable, Value: BorshDeserializable { 104 | public init(from reader: inout BinaryReader) throws { 105 | let count: UInt32 = try .init(from: &reader) 106 | let keyValuePairs = try Array(0..(_ value: T) throws -> Data where T : BorshSerializable { 13 | var writer = Data() 14 | try value.serialize(to: &writer) 15 | return writer 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /nearclientios/Sources/Utils/Borsh/BorshSerialize.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BorshSerialize.swift 3 | // nearclientios 4 | // 5 | // Created by Dmytro Kurochka on 21.11.2019. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol BorshSerializable { 11 | func serialize(to writer: inout Data) throws 12 | } 13 | 14 | extension UInt8: BorshSerializable {} 15 | extension UInt16: BorshSerializable {} 16 | extension UInt32: BorshSerializable {} 17 | extension UInt64: BorshSerializable {} 18 | extension UInt128: BorshSerializable {} 19 | extension Int8: BorshSerializable {} 20 | extension Int16: BorshSerializable {} 21 | extension Int32: BorshSerializable {} 22 | extension Int64: BorshSerializable {} 23 | extension Int128: BorshSerializable {} 24 | 25 | public extension FixedWidthInteger { 26 | func serialize(to writer: inout Data) throws { 27 | writer.append(contentsOf: withUnsafeBytes(of: self.littleEndian) { Array($0) }) 28 | } 29 | } 30 | 31 | extension Float32: BorshSerializable { 32 | public func serialize(to writer: inout Data) throws { 33 | assert(!self.isNaN, "For portability reasons we do not allow to serialize NaNs.") 34 | var start = bitPattern.littleEndian 35 | writer.append(Data(buffer: UnsafeBufferPointer(start: &start, count: 1))) 36 | } 37 | } 38 | 39 | extension Float64: BorshSerializable { 40 | public func serialize(to writer: inout Data) throws { 41 | assert(!self.isNaN, "For portability reasons we do not allow to serialize NaNs.") 42 | var start = bitPattern.littleEndian 43 | writer.append(Data(buffer: UnsafeBufferPointer(start: &start, count: 1))) 44 | } 45 | } 46 | 47 | extension Bool: BorshSerializable { 48 | public func serialize(to writer: inout Data) throws { 49 | let intRepresentation: UInt8 = self ? 1 : 0 50 | try intRepresentation.serialize(to: &writer) 51 | } 52 | } 53 | 54 | extension Optional where Wrapped: BorshSerializable { 55 | func serialize(to writer: inout Data) throws { 56 | switch self { 57 | case .some(let value): 58 | try UInt8(1).serialize(to: &writer) 59 | try value.serialize(to: &writer) 60 | case .none: 61 | try UInt8(0).serialize(to: &writer) 62 | } 63 | } 64 | } 65 | 66 | extension String: BorshSerializable { 67 | public func serialize(to writer: inout Data) throws { 68 | let data = Data(utf8) 69 | try UInt32(data.count).serialize(to: &writer) 70 | writer.append(data) 71 | } 72 | } 73 | 74 | extension Array: BorshSerializable where Element: BorshSerializable { 75 | public func serialize(to writer: inout Data) throws { 76 | try UInt32(count).serialize(to: &writer) 77 | try forEach { try $0.serialize(to: &writer) } 78 | } 79 | } 80 | 81 | extension Set: BorshSerializable where Element: BorshSerializable & Comparable { 82 | public func serialize(to writer: inout Data) throws { 83 | try sorted().serialize(to: &writer) 84 | } 85 | } 86 | 87 | extension Dictionary: BorshSerializable where Key: BorshSerializable & Comparable, Value: BorshSerializable { 88 | public func serialize(to writer: inout Data) throws { 89 | let sortedByKeys = sorted(by: {$0.key < $1.key}) 90 | try UInt32(sortedByKeys.count).serialize(to: &writer) 91 | try sortedByKeys.forEach { key, value in 92 | try key.serialize(to: &writer) 93 | try value.serialize(to: &writer) 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /nearclientios/Sources/Utils/FileManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileManager.swift 3 | // nearclientios 4 | // 5 | // Created by Dmytro Kurochka on 08.11.2019. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension FileManager { 11 | func ensureDir(path: String) throws -> Void { 12 | try createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil) 13 | } 14 | 15 | var targetDirectory: URL { 16 | let paths = urls(for: .documentDirectory, in: .userDomainMask) 17 | return paths[0] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /nearclientios/Sources/Utils/FixedLengthByteArray.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FixedLengthByteArray.swift 3 | // nearclientios 4 | // 5 | // Created by Dmytro Kurochka on 25.11.2019. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol FixedLengthByteArray { 11 | static var fixedLength: UInt32 {get} 12 | var bytes: [UInt8] {get} 13 | init(bytes: [UInt8]) throws 14 | } 15 | 16 | extension FixedLengthByteArray { 17 | public func serialize(to writer: inout Data) throws { 18 | writer.append(bytes, count: Int(Self.fixedLength)) 19 | } 20 | 21 | public init(from reader: inout BinaryReader) throws { 22 | try self.init(bytes: reader.read(count: Self.fixedLength)) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /nearclientios/Sources/Utils/Format.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Format.swift 3 | // nearclientios 4 | // 5 | // Created by Kevin McConnaughay on 3/17/22. 6 | // 7 | 8 | import Foundation 9 | 10 | // Exponent for calculating how many indivisible units are there in one NEAR. 11 | public let NEAR_NOMINATION_EXP = 24 12 | 13 | extension UInt128 { 14 | 15 | // Pre-calculate offsets used for rounding to different number of digits 16 | private static let roundingOffsets: [UInt128] = { 17 | var offsets: [UInt128] = [] 18 | let multiplier = UInt128(10) 19 | var offset = UInt128(5) 20 | for _ in 0.. (String, String) { 29 | var balance = self 30 | if fracDigits != exponent { 31 | // Adjust balance for rounding at given number of digits 32 | let roundingExp = exponent - fracDigits - 1 33 | if roundingExp > 0 { 34 | balance += UInt128.roundingOffsets[roundingExp] 35 | } 36 | } 37 | 38 | let balanceString = balance.toString() 39 | let digitOffset = balanceString.count - exponent 40 | 41 | let wholeStr = String(digitOffset > 0 ? balanceString.prefix(digitOffset) : "0") 42 | var fractionStr = String(balanceString.suffix(digitOffset > 0 ? exponent : balanceString.count)) 43 | 44 | fractionStr = String(String(fractionStr.reversed()).padding(toLength: exponent, withPad: "0", startingAt: 0).reversed()) 45 | fractionStr = String(fractionStr.prefix(fracDigits)) 46 | return (wholeStr, fractionStr) 47 | } 48 | 49 | /// Convert account balance value from internal indivisible units to NEAR. 50 | /// - Parameter fracDigits: number of fractional digits to preserve in formatted string. Balance is rounded to match given number of digits. 51 | /// - Returns: Value in Ⓝ 52 | public func toNearAmount(fracDigits: Int = NEAR_NOMINATION_EXP, withExponent exponent: Int = NEAR_NOMINATION_EXP) -> String { 53 | let (wholeStr, fractionStr) = nearComponents(fracDigits: fracDigits, exponent: exponent) 54 | return trimTrailingZeroes(value: "\(formatWithCommas(value: wholeStr)).\(fractionStr)") 55 | } 56 | 57 | /// Convert account balance value from internal indivisible units to NEAR 58 | /// - Parameter fracDigits: number of fractional digits to preserve in formatted string. Balance is rounded to match given number of digits. 59 | /// - Returns: Value as double in Ⓝ 60 | public func toNearDouble(fracDigits: Int = NEAR_NOMINATION_EXP, withExponent exponent: Int = NEAR_NOMINATION_EXP) -> Double { 61 | let (wholeStr, fractionStr) = nearComponents(fracDigits: fracDigits, exponent: exponent) 62 | return Double("\(wholeStr).\(fractionStr)")! 63 | } 64 | 65 | } 66 | 67 | struct ConversionError: Error { 68 | let message = "Could not convert string to yoctoNEAR." 69 | } 70 | 71 | extension String { 72 | 73 | /// Convert human readable NEAR amount to internal indivisible units. 74 | /// Custom exponent here could be used for Fungible Tokens where the number of decimals differs from NEAR. 75 | /// - Returns: The parsed yoctoⓃ amount 76 | public func toYoctoNearString(withExponent exponent: Int = NEAR_NOMINATION_EXP) throws -> String { 77 | var parsed = self 78 | // Edge cases not covered by our numberyPattern regex. 79 | if (parsed == "" || parsed == ".") { 80 | throw ConversionError() 81 | } 82 | parsed = parsed.replacingOccurrences(of: ",", with: "").trimmingCharacters(in: NSCharacterSet.whitespacesAndNewlines) 83 | let numberyPattern = "^\\d*\\.?\\d*$" 84 | let numberyRegex = try! NSRegularExpression(pattern: numberyPattern) 85 | if numberyRegex.matches(in: parsed, options: [], range: NSRange(location: 0, length: parsed.utf16.count)).count != 1 { 86 | throw ConversionError() 87 | } 88 | parsed = parsed.prefix(1) == "." ? "0\(parsed)" : parsed 89 | let split = parsed.split(separator: ".") 90 | let wholePart = split[0] 91 | let fractionPart = split.indices.contains(1) ? split[1] : "" 92 | if split.count > 2 || fractionPart.count > exponent { 93 | throw ConversionError() 94 | } 95 | 96 | return trimLeadingZeroes(value: "\(wholePart)\(fractionPart.padding(toLength: exponent, withPad: "0", startingAt: 0))") 97 | } 98 | 99 | /// Convert human readable NEAR amount to internal indivisible units. 100 | /// Custom exponent here could be used for Fungible Tokens where the number of decimals differs from NEAR. 101 | /// - Returns: The parsed yoctoⓃ amount 102 | public func toYoctoNear(withExponent exponent: Int = NEAR_NOMINATION_EXP) throws -> UInt128 { 103 | return UInt128(stringLiteral: try self.toYoctoNearString(withExponent: exponent)) 104 | } 105 | 106 | /// Convert account balance value from internal indivisible units to NEAR. 107 | /// - Parameter fracDigits: number of fractional digits to preserve in formatted string. Balance is rounded to match given number of digits. 108 | /// - Returns: Value in Ⓝ 109 | public func toNearAmount(fracDigits: Int = NEAR_NOMINATION_EXP) -> String { 110 | return UInt128(stringLiteral: self).toNearAmount(fracDigits: fracDigits) 111 | } 112 | 113 | } 114 | 115 | /// Returns a human-readable value with commas 116 | /// - Parameter value: A value that may not contain commas 117 | /// - Returns: A value with commas 118 | private func formatWithCommas(value: String) -> String { 119 | var formatted = value 120 | let pattern = "(-?\\d+)(\\d{3})" 121 | let regex = try! NSRegularExpression(pattern: pattern) 122 | while regex.matches(in: formatted, options: [], range: NSRange(location: 0, length: formatted.utf16.count)).count > 0 { 123 | formatted = regex.stringByReplacingMatches(in: formatted, options: [], range: NSRange(0.. String { 133 | var formatted = value 134 | let pattern = "^0+" 135 | let regex = try! NSRegularExpression(pattern: pattern) 136 | formatted = regex.stringByReplacingMatches(in: formatted, options: [], range: NSRange(0.. String { 149 | var formatted = value 150 | let pattern = "\\.?0*$" 151 | let regex = try! NSRegularExpression(pattern: pattern) 152 | formatted = regex.stringByReplacingMatches(in: formatted, options: [], range: NSRange(0..: Hashable, Codable { 7 | public typealias IntegerLiteralType = UInt64 8 | public typealias Magnitude = UInt2X 9 | public typealias Words = [Word.Words.Element] 10 | public typealias Stride = Int 11 | public var rawValue:Magnitude = 0 12 | public init(rawValue:Magnitude){ self.rawValue = rawValue } 13 | public init(_ source:Int2X) { self.rawValue = source.rawValue } 14 | public init() {} 15 | } 16 | // Swift bug? 17 | // auto-generated == fatalError()'s 18 | // UInt2X(hi:nonzero, lo:0) == 0 19 | extension Int2X { 20 | public static func == (_ lhs: Int2X, _ rhs: Int2X)->Bool { 21 | return lhs.rawValue == rhs.rawValue 22 | } 23 | } 24 | extension Int2X : ExpressibleByIntegerLiteral { 25 | public static var isSigned: Bool { return true } 26 | public static var bitWidth: Int { return Magnitude.bitWidth } 27 | public static var max:Int2X { return Int2X(rawValue:(Magnitude.max >> 1)) } 28 | public static var min:Int2X { return Int2X(rawValue:(Magnitude.max >> 1) &+ 1) } 29 | public init?(exactly source: T) where T : BinaryInteger { 30 | guard source.bitWidth <= Int2X.bitWidth || source.magnitude <= T(Int2X.max.rawValue) else { 31 | return nil 32 | } 33 | if !T.isSigned && source & (1 << (source.bitWidth - 1)) != 0 { 34 | return nil 35 | } 36 | self.init(source) 37 | } 38 | public init(_ source: T) where T : BinaryInteger { 39 | if !T.isSigned && Word.bitWidth * 2 <= source.bitWidth && source & (1 << (source.bitWidth - 1)) != 0 { 40 | fatalError("Not enough bits to represent a signed value") 41 | } 42 | self.rawValue = Magnitude(source.magnitude) 43 | if T.isSigned && source & (1 << (source.bitWidth - 1)) != 0 { 44 | self.rawValue = -self.rawValue 45 | } 46 | } 47 | public init?(exactly source: T) where T : BinaryFloatingPoint { 48 | guard let rv = Magnitude(exactly: source.sign == .minus ? -source : +source) else { return nil } 49 | self = Int2X(rawValue:rv) 50 | guard !self.isNegative else { return nil } 51 | if source.sign == .minus { self = -self } 52 | } 53 | public init(_ source: T) where T : BinaryFloatingPoint { 54 | guard let result = Int2X(exactly: source) else { 55 | fatalError("Not enough bits to represent a signed value") 56 | } 57 | self = result 58 | } 59 | // alway succeeds 60 | public init(truncatingIfNeeded source: T) { 61 | self.rawValue = Magnitude(truncatingIfNeeded:source.magnitude) 62 | if T.isSigned && source < 0 { 63 | self.rawValue = -self.rawValue 64 | } 65 | } 66 | // alway succeeds 67 | public init(clamping source: T) { 68 | self = Int2X(exactly: source) ?? Int2X.max 69 | } 70 | public init(integerLiteral value: IntegerLiteralType) { 71 | self.init(value) 72 | } 73 | } 74 | extension Int2X : Comparable { 75 | internal var isNegative:Bool { 76 | return Int2X.max.rawValue < self.rawValue 77 | } 78 | public var magnitude:Magnitude { 79 | return isNegative ? rawValue == Int2X.min.rawValue ? rawValue : -rawValue : +rawValue 80 | } 81 | public static func < (lhs: Int2X, rhs: Int2X) -> Bool { 82 | return Int2X.max.rawValue < lhs.rawValue &- rhs.rawValue 83 | } 84 | } 85 | extension Int2X : Numeric { 86 | // unary operators 87 | public static prefix func ~(_ value:Int2X)->Int2X { 88 | return Int2X(rawValue:~(value.rawValue)) 89 | } 90 | public static prefix func +(_ value:Int2X)->Int2X { 91 | return value 92 | } 93 | public static prefix func -(_ value:Int2X)->Int2X { 94 | return Int2X(rawValue:-(value.rawValue)) 95 | } 96 | // additions 97 | public func addingReportingOverflow(_ other: Int2X) -> (partialValue: Int2X, overflow: Bool) { 98 | let (pv, of) = self.rawValue.addingReportingOverflow(other.rawValue) 99 | // For any given int the only possible case that overflows is I.min - I.min 100 | // in which case overflow is true and partialValue is 0 101 | return (Int2X(rawValue:pv), of && pv == 0) 102 | 103 | } 104 | public static func &+(_ lhs:Int2X, _ rhs:Int2X)->Int2X { 105 | return lhs.addingReportingOverflow(rhs).partialValue 106 | } 107 | public static func +(_ lhs:Int2X, _ rhs:Int2X)->Int2X { 108 | let (pv, of) = lhs.addingReportingOverflow(rhs) 109 | precondition(!of, "\(lhs) + \(rhs): Addition overflow!") 110 | return pv 111 | } 112 | public static func += (lhs: inout Int2X, rhs: Int2X) { 113 | lhs = lhs + rhs 114 | } 115 | // subtraction 116 | public func subtractingReportingOverflow(_ other: Int2X) -> (partialValue: Int2X, overflow: Bool) { 117 | let (pv, of) = self.rawValue.subtractingReportingOverflow(other.rawValue) 118 | return (Int2X(rawValue:pv), of && pv == 0) 119 | } 120 | public static func &-(_ lhs:Int2X, _ rhs:Int2X)->Int2X { 121 | return lhs.subtractingReportingOverflow(rhs).partialValue 122 | } 123 | public static func -(_ lhs:Int2X, _ rhs:Int2X)->Int2X { 124 | let (pv, of) = lhs.subtractingReportingOverflow(rhs) 125 | precondition(!of, "\(lhs) - \(rhs): Subtruction overflow!") 126 | return pv 127 | } 128 | public static func -= (lhs: inout Int2X, rhs: Int2X) { 129 | lhs = lhs - rhs 130 | } 131 | // multiplication 132 | public func multipliedFullWidth(by other: Int2X) -> (high: Int2X, low: Magnitude) { 133 | let (h, l) = self.rawValue.multipliedFullWidth(by:other.rawValue) 134 | return (Int2X(h), l) 135 | 136 | } 137 | public func multipliedReportingOverflow(by other: Int2X) -> (partialValue: Int2X, overflow: Bool) { 138 | let hv = self.magnitude.multipliedFullWidth(by: other.magnitude) 139 | return (self.isNegative != other.isNegative ? -Int2X(rawValue:hv.low) : +Int2X(rawValue:hv.low), 0 < hv.high) 140 | } 141 | public static func &*(lhs: Int2X, rhs: Int2X) -> Int2X { 142 | return lhs.multipliedReportingOverflow(by: rhs).partialValue 143 | } 144 | public static func *(lhs: Int2X, rhs: Int2X) -> Int2X { 145 | let result = lhs.multipliedReportingOverflow(by: rhs) 146 | precondition(!result.overflow, "Multiplication overflow!") 147 | return result.partialValue 148 | } 149 | public static func *= (lhs: inout Int2X, rhs: Int2X) { 150 | lhs = lhs * rhs 151 | } 152 | } 153 | // bitshifts 154 | extension Int2X { 155 | public static func &>>(_ lhs:Int2X, _ rhs:Int2X)->Int2X { 156 | if rhs.isNegative { return lhs &<< -rhs } 157 | if Int2X.bitWidth <= rhs { return 0 } 158 | let rv = lhs.magnitude &>> rhs.magnitude 159 | return lhs.isNegative ? -Int2X(rawValue:rv) : +Int2X(rawValue:rv) 160 | } 161 | public static func &<<(_ lhs:Int2X, _ rhs:Int2X)->Int2X { 162 | if rhs.isNegative { return lhs &>> -rhs } 163 | if Int2X.bitWidth <= rhs { return 0 } 164 | let rv = lhs.magnitude &<< rhs.magnitude 165 | return lhs.isNegative ? -Int2X(rawValue:rv) : +Int2X(rawValue:rv) 166 | } 167 | public static func &>>=(_ lhs:inout Int2X, _ rhs:Int2X) { 168 | return lhs = lhs &>> rhs 169 | } 170 | public static func &<<=(_ lhs:inout Int2X, _ rhs:Int2X) { 171 | return lhs = lhs &<< rhs 172 | } 173 | } 174 | // division 175 | extension Int2X { 176 | public func quotientAndRemainder(dividingBy other: Int2X) -> (quotient: Int2X, remainder: Int2X) { 177 | let qv = self.magnitude.quotientAndRemainder(dividingBy:other.magnitude) 178 | let q = self.isNegative != other.isNegative ? -qv.quotient : +qv.quotient 179 | let r = self.isNegative ? -qv.remainder : +qv.remainder 180 | return (Int2X(rawValue:q), Int2X(rawValue:r)) 181 | } 182 | public static func / (_ lhs:Int2X, rhs:Int2X)->Int2X { 183 | return lhs.quotientAndRemainder(dividingBy: rhs).quotient 184 | } 185 | public static func /= (_ lhs:inout Int2X, rhs:Int2X) { 186 | lhs = lhs / rhs 187 | } 188 | public static func % (_ lhs:Int2X, rhs:Int2X)->Int2X { 189 | return lhs.quotientAndRemainder(dividingBy: rhs).remainder 190 | } 191 | public static func %= (_ lhs:inout Int2X, rhs:Int2X) { 192 | lhs = lhs % rhs 193 | } 194 | public func dividedReportingOverflow(by other :Int2X) -> (partialValue: Int2X, overflow:Bool) { 195 | return (self / other, false) 196 | } 197 | public func remainderReportingOverflow(dividingBy other :Int2X) -> (partialValue: Int2X, overflow:Bool) { 198 | return (self % other, false) 199 | } 200 | public func dividingFullWidth(_ dividend: (high: Int2X, low: Magnitude)) -> (quotient: Int2X, remainder: Int2X) { 201 | let qv = self.magnitude.dividingFullWidth((high: dividend.high.magnitude, low: dividend.low)) 202 | let q = self.isNegative != dividend.high.isNegative ? -qv.quotient : +qv.quotient 203 | let r = self.isNegative ? -qv.remainder : +qv.remainder 204 | return (Int2X(rawValue:q), Int2X(rawValue:r)) 205 | } 206 | } 207 | // UInt2X -> String 208 | extension Int2X : CustomStringConvertible, CustomDebugStringConvertible { 209 | public func toString(radix:Int=10, uppercase:Bool=false) -> String { 210 | return (self.isNegative ? "-" : "") + self.magnitude.toString(radix:radix, uppercase:uppercase) 211 | } 212 | public var description:String { 213 | return toString() 214 | } 215 | public var debugDescription:String { 216 | return (self.isNegative ? "-" : "+") + "0x" + self.magnitude.toString(radix:16) 217 | } 218 | } 219 | extension StringProtocol { 220 | public init?(_ source:Int2X, radix:Int=10, uppercase:Bool=false) { 221 | self.init(source.toString(radix:radix, uppercase:uppercase)) 222 | } 223 | } 224 | // String <- UInt2X 225 | extension Int2X : ExpressibleByStringLiteral { 226 | public init(stringLiteral value: StringLiteralType) { 227 | self.init() 228 | if let result = Int2X.fromString(value) { 229 | self = result 230 | } 231 | } 232 | internal static func fromString(_ value: String) -> Int2X? { 233 | var source = value 234 | var sign = "+" 235 | if source.first == "-" || source.first == "+" { 236 | sign = String(source.first!) 237 | source.removeFirst() 238 | } 239 | guard let magnitude = Magnitude.fromString(source) else { return nil } 240 | return sign == "-" ? -Int2X(rawValue: magnitude) : +Int2X(rawValue: magnitude) 241 | } 242 | } 243 | // Int -> Int2X 244 | extension Int { 245 | public init(_ source:Int2X) { 246 | let a = Int(bitPattern: UInt(source.magnitude)) 247 | self.init(source.isNegative ? -a : +a) 248 | } 249 | } 250 | // Strideable 251 | extension Int2X: Strideable { 252 | public func distance(to other: Int2X) -> Int { 253 | return Int(other) - Int(self) 254 | } 255 | public func advanced(by n: Int) -> Int2X { 256 | return self + Int2X(n) 257 | } 258 | } 259 | // BinaryInteger 260 | extension Int2X: BinaryInteger { 261 | public var bitWidth: Int { 262 | return rawValue.bitWidth 263 | } 264 | public var words: Words { 265 | return rawValue.words 266 | } 267 | public var trailingZeroBitCount: Int { 268 | return rawValue.trailingZeroBitCount 269 | } 270 | public static func &= (lhs: inout Int2X, rhs: Int2X) { 271 | lhs.rawValue &= rhs.rawValue 272 | } 273 | public static func |= (lhs: inout Int2X, rhs: Int2X) { 274 | lhs.rawValue |= rhs.rawValue 275 | } 276 | public static func ^= (lhs: inout Int2X, rhs: Int2X) { 277 | lhs.rawValue ^= rhs.rawValue 278 | } 279 | public static func <<= (lhs: inout Int2X, rhs: RHS) where RHS : BinaryInteger { 280 | lhs.rawValue <<= rhs 281 | } 282 | public static func >>= (lhs: inout Int2X, rhs: RHS) where RHS : BinaryInteger { 283 | lhs.rawValue >>= rhs 284 | } 285 | } 286 | // FixedWidthInteger 287 | extension Int2X: FixedWidthInteger { 288 | public init(_truncatingBits bits: UInt) { 289 | fatalError() 290 | } 291 | public var nonzeroBitCount: Int { 292 | return self.rawValue.nonzeroBitCount 293 | } 294 | public var leadingZeroBitCount: Int { 295 | return self.rawValue.leadingZeroBitCount 296 | } 297 | public var byteSwapped: Int2X { 298 | return Int2X(rawValue:rawValue.byteSwapped) 299 | } 300 | } 301 | // SignedInteger 302 | extension Int2X: SignedInteger {} 303 | 304 | public typealias Int128 = Int2X 305 | public typealias Int256 = Int2X 306 | public typealias Int512 = Int2X 307 | public typealias Int1024 = Int2X 308 | 309 | -------------------------------------------------------------------------------- /nearclientios/Sources/Utils/Network.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Network.swift 3 | // nearclientios 4 | // 5 | // Created by Dmitry Kurochka on 10/30/19. 6 | // Copyright © 2019 NEAR Protocol. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol NetworkProtocol { 12 | var name: String {get} 13 | var chainId: String {get} 14 | var _defaultProvider: ((_ providers: Any) -> Any)? {get} 15 | } 16 | 17 | public struct Network: NetworkProtocol { 18 | public let name: String 19 | public let chainId: String 20 | public var _defaultProvider: ((_ providers: Any) -> Any)? 21 | } 22 | -------------------------------------------------------------------------------- /nearclientios/Sources/Utils/SHA256.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SHA256.swift 3 | // nearclientios 4 | // 5 | // Created by Dmitry Kurochka on 10/30/19. 6 | // Copyright © 2019 NEAR Protocol. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CommonCrypto 11 | 12 | public extension Collection where Element == UInt8 { 13 | var digest: [UInt8] { 14 | var result = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) 15 | data.withUnsafeBytes { 16 | _ = CC_SHA256($0.baseAddress, CC_LONG(count), &result) 17 | } 18 | return result 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /nearclientios/Sources/Utils/Serialize.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Serialize.swift 3 | // nearclientios 4 | // 5 | // Created by Dmitry Kurochka on 10/30/19. 6 | // Copyright © 2019 NEAR Protocol. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Base58Swift 11 | 12 | public extension String { 13 | var baseDecoded: [UInt8] { 14 | return Base58.base58Decode(self) ?? [] 15 | } 16 | } 17 | 18 | public extension Data { 19 | var baseEncoded: String { 20 | return Base58.base58Encode(bytes) 21 | } 22 | 23 | var bytes: [UInt8] { 24 | return [UInt8](self) 25 | } 26 | } 27 | 28 | public extension Data { 29 | var hexString: String { 30 | return map { String(format: "%02x", UInt8($0)) }.joined() 31 | } 32 | } 33 | 34 | public extension Data { 35 | var json: [String: Any]? { 36 | return try? toDictionary() 37 | } 38 | 39 | init(json: [String: Any]) { 40 | let data = try? json.toData() 41 | self.init(bytes: data?.bytes ?? [], count: data?.bytes.count ?? 0) 42 | } 43 | } 44 | 45 | public extension Sequence where Element == UInt8 { 46 | var baseEncoded: String { 47 | return data.baseEncoded 48 | } 49 | 50 | var data: Data { 51 | return Data(self) 52 | } 53 | } 54 | 55 | public struct CastingError: Error { 56 | let fromType: Any.Type 57 | let toType: Any.Type 58 | init(fromType: FromType.Type, toType: ToType.Type) { 59 | self.fromType = fromType 60 | self.toType = toType 61 | } 62 | } 63 | 64 | extension CastingError: LocalizedError { 65 | var localizedDescription: String { return "Can not cast from \(fromType) to \(toType)" } 66 | } 67 | 68 | extension CastingError: CustomStringConvertible { public var description: String { return localizedDescription } } 69 | 70 | public extension Data { 71 | func toDictionary(options: JSONSerialization.ReadingOptions = []) throws -> [String: Any] { 72 | return try to(type: [String: Any].self, options: options) 73 | } 74 | 75 | func to(type: T.Type, options: JSONSerialization.ReadingOptions = []) throws -> T { 76 | guard let result = try JSONSerialization.jsonObject(with: self, options: options) as? T else { 77 | throw CastingError(fromType: type, toType: T.self) 78 | } 79 | return result 80 | } 81 | } 82 | 83 | public extension String { 84 | func asJSON(to type: T.Type, using encoding: String.Encoding = .utf8) throws -> T { 85 | guard let data = data(using: encoding) else { throw CastingError(fromType: type, toType: T.self) } 86 | return try data.to(type: T.self) 87 | } 88 | 89 | func asJSONToDictionary(using encoding: String.Encoding = .utf8) throws -> [String: Any] { 90 | return try asJSON(to: [String: Any].self, using: encoding) 91 | } 92 | } 93 | 94 | internal extension Dictionary { 95 | func toData(options: JSONSerialization.WritingOptions = []) throws -> Data { 96 | return try JSONSerialization.data(withJSONObject: self, options: options) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /nearclientios/Sources/Utils/URL+Params.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URL+Params.swift 3 | // nearclientios 4 | // 5 | // Created by Dmytro Kurochka on 10.12.2019. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension URL { 11 | var queryParameters: [String: String]? { 12 | guard let components = URLComponents(url: self, resolvingAgainstBaseURL: true), 13 | let queryItems = components.queryItems else { return nil } 14 | return queryItems.reduce(into: [String: String]()) { result, item in 15 | result[item.name] = item.value 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /nearclientios/Sources/Utils/Web.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Web.swift 3 | // nearclientios 4 | // 5 | // Created by Dmitry Kurochka on 10/30/19. 6 | // Copyright © 2019 NEAR Protocol. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct ConnectionInfo { 12 | let url: URL 13 | let user: String? = nil 14 | let password: String? = nil 15 | let allowInsecure: Bool? = nil 16 | let timeout: TimeInterval? = nil 17 | let headers: [String: Any]? = nil 18 | } 19 | 20 | enum HTTPError: Error { 21 | case unknown 22 | case error(status: Int, message: String?) 23 | } 24 | 25 | private func fetch(url: URL, params: [String: Any]?) async throws-> Any { 26 | let session = URLSession.shared 27 | var request = URLRequest(url: url) 28 | request.httpMethod = params.flatMap {_ in "POST"} ?? "GET" 29 | request.addValue("application/json", forHTTPHeaderField: "Content-Type") 30 | request.httpBody = params.flatMap { try? $0.toData() } 31 | return try await withCheckedThrowingContinuation({ (continuation: CheckedContinuation) in 32 | let task = session.dataTask(with: request) { data, response, error in 33 | if let error = error { return continuation.resume(throwing: error) } 34 | let result = data.flatMap {try? $0.toDictionary()} 35 | if let json = result?["result"] { 36 | continuation.resume(returning: json) 37 | } else if let httpResponse = response as? HTTPURLResponse { 38 | do { 39 | let decoder = JSONDecoder() 40 | decoder.keyDecodingStrategy = .convertFromSnakeCase 41 | let decodedError = try decoder.decode(TransactionError.self, from: data ?? Data()) 42 | continuation.resume(throwing: decodedError) 43 | } catch { 44 | if let result = result, let json = try? JSONSerialization.data(withJSONObject: result, options: []) { 45 | debugPrint("=====================") 46 | print(String(decoding: json, as: UTF8.self)) 47 | debugPrint("=====================") 48 | } 49 | let error = HTTPError.error(status: httpResponse.statusCode, 50 | message: data.flatMap({ String(data: $0, encoding: .utf8) })) 51 | continuation.resume(throwing: error) 52 | } 53 | } else { 54 | continuation.resume(throwing: HTTPError.unknown) 55 | } 56 | } 57 | task.resume() 58 | }) 59 | } 60 | 61 | public func fetchJson(connection: ConnectionInfo, json: [String: Any]?) async throws -> Any { 62 | let url = connection.url 63 | return try await fetch(url: url, params: json) 64 | } 65 | 66 | func fetchJson(url: URL, json: [String: Any]?) async throws -> Any { 67 | return try await fetch(url: url, params: json) 68 | } 69 | -------------------------------------------------------------------------------- /nearclientios/Sources/WalletAccount.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WalletAccount.swift 3 | // nearclientios 4 | // 5 | // Created by Dmitry Kurochka on 10/30/19. 6 | // Copyright © 2019 NEAR Protocol. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | import KeychainAccess 12 | 13 | public let APP_SCHEME = "x-nearclientios" 14 | 15 | let LOGIN_WALLET_URL_SUFFIX = "/login/" 16 | 17 | let LOCAL_STORAGE_KEY_SUFFIX = "_wallet_auth_key" 18 | 19 | /// storage key for a pending access key (i.e. key has been generated but we are not sure it was added yet) 20 | let PENDING_ACCESS_KEY_PREFIX = "pending_key" 21 | 22 | public protocol AuthDataProtocol { 23 | var accountId: String? {get} 24 | } 25 | 26 | public struct AuthData: AuthDataProtocol { 27 | public let accountId: String? 28 | } 29 | 30 | public enum WalletAccountError: Error { 31 | case noKeyStore 32 | case noKeyPair 33 | case noRegisteredURLSchemes 34 | case successUrlWrongScheme 35 | case failureUrlWrongScheme 36 | case callbackUrlParamsNotValid 37 | } 38 | 39 | public protocol WalletStorage: AnyObject { 40 | subscript(key: String) -> String? {get set} 41 | } 42 | 43 | public let WALLET_STORAGE_SERVICE = "nearlib.wallet" 44 | 45 | extension Keychain: WalletStorage {} 46 | 47 | public protocol ExternalAuthService { 48 | func openURL(_ url: URL, presentingViewController: UIViewController) -> Bool 49 | } 50 | 51 | public actor WalletAccount { 52 | private let _walletBaseUrl: String 53 | private let _authDataKey: String 54 | private let _keyStore: KeyStore 55 | private var _authData: AuthDataProtocol 56 | private let _networkId: String 57 | private let storage: WalletStorage 58 | private let authService: ExternalAuthService 59 | 60 | public init(near: Near, authService: ExternalAuthService, appKeyPrefix: String? = nil, 61 | storage: WalletStorage = Keychain(service: WALLET_STORAGE_SERVICE)) throws { 62 | let keyPrefix = appKeyPrefix ?? (near.config.contractName ?? "default") 63 | let authDataKey = keyPrefix + LOCAL_STORAGE_KEY_SUFFIX 64 | guard let keyStore = (near.connection.signer as? InMemorySigner)?.keyStore else {throw WalletAccountError.noKeyStore} 65 | let authData = AuthData(accountId: storage[authDataKey]) 66 | _walletBaseUrl = near.config.walletUrl 67 | _authDataKey = authDataKey 68 | _keyStore = keyStore 69 | _authData = authData 70 | _networkId = near.config.networkId 71 | self.storage = storage 72 | self.authService = authService 73 | } 74 | } 75 | 76 | extension WalletAccount { 77 | /** 78 | - Example: 79 | walletAccount.isSignedIn() 80 | - Returns: Returns true, if this WalletAccount is authorized with the wallet. 81 | */ 82 | public func isSignedIn() -> Bool { 83 | return _authData.accountId != nil 84 | } 85 | 86 | /** 87 | - Example: 88 | walletAccount.getAccountId() 89 | - Returns: Authorized Account ID. 90 | */ 91 | public func getAccountId() -> String { 92 | return _authData.accountId ?? "" 93 | } 94 | 95 | /** 96 | Redirects current page to the wallet authentication page. 97 | - Parameters: 98 | - contractId: contractId contract ID of the application 99 | - accountId: title name of the application 100 | - networkId: successUrl url to redirect on success 101 | - failureUrl: failureUrl url to redirect on failure 102 | */ 103 | @discardableResult 104 | public func requestSignIn(contractId: String?, title: String, presentingViewController: UIViewController, successUrl: URL = URL(string: APP_SCHEME + "://success")!, failureUrl: URL = URL(string: APP_SCHEME + "://fail")!, appUrl: URL = URL(string: APP_SCHEME + "://")!, curve: KeyType = .ED25519) async throws -> Bool { 105 | guard getAccountId().isEmpty else {return true} 106 | guard try await _keyStore.getKey(networkId: _networkId, accountId: getAccountId()) == nil else {return true} 107 | 108 | var newUrlComponents = URLComponents(string: _walletBaseUrl + LOGIN_WALLET_URL_SUFFIX) 109 | let title = URLQueryItem(name: "referrer", value: title) 110 | let contract_id = URLQueryItem(name: "contract_id", value: contractId) 111 | let success_url = URLQueryItem(name: "success_url", value: successUrl.absoluteString) 112 | let failure_url = URLQueryItem(name: "failure_url", value: failureUrl.absoluteString) 113 | let app_url = URLQueryItem(name: "app_url", value: appUrl.absoluteString) 114 | let accessKey = try keyPairFromRandom(curve: curve) 115 | let public_key = URLQueryItem(name: "public_key", value: accessKey.getPublicKey().toString()) 116 | 117 | newUrlComponents?.queryItems = [title, contract_id, success_url, failure_url, app_url, public_key] 118 | let accountId = PENDING_ACCESS_KEY_PREFIX + accessKey.getPublicKey().toString() 119 | try await _keyStore.setKey(networkId: _networkId, accountId: accountId, keyPair: accessKey) 120 | if let openUrl = newUrlComponents?.url { 121 | return await MainActor.run { 122 | authService.openURL(openUrl, presentingViewController: presentingViewController) 123 | } 124 | } 125 | return false 126 | } 127 | 128 | /** 129 | Complete sign in for a given account id and public key. To be invoked by the app when getting a callback from the wallet. 130 | */ 131 | public func completeSignIn(url: URL) async throws -> Void { 132 | guard let params = url.queryParameters else {throw WalletAccountError.callbackUrlParamsNotValid} 133 | if let publicKey = params["public_key"], let accountId = params["account_id"] { 134 | _authData = AuthData(accountId: accountId) 135 | storage[_authDataKey] = accountId 136 | try await _moveKeyFromTempToPermanent(accountId: accountId, publicKey: publicKey) 137 | } 138 | } 139 | 140 | public func completeSignIn(withKeyPair keyPair: KeyPair, accountId: String) async throws -> Void { 141 | _authData = AuthData(accountId: accountId) 142 | storage[_authDataKey] = accountId 143 | try await _keyStore.setKey(networkId: _networkId, accountId: accountId, keyPair: keyPair) 144 | } 145 | 146 | private func _moveKeyFromTempToPermanent(accountId: String, publicKey: String) async throws -> Void { 147 | let pendingAccountId = PENDING_ACCESS_KEY_PREFIX + publicKey 148 | guard let keyPair = try await (_keyStore.getKey(networkId: _networkId, 149 | accountId: pendingAccountId)) else {throw WalletAccountError.noKeyPair} 150 | try await _keyStore.setKey(networkId: _networkId, accountId: accountId, keyPair: keyPair) 151 | try await _keyStore.removeKey(networkId: _networkId, accountId: PENDING_ACCESS_KEY_PREFIX + publicKey) 152 | } 153 | 154 | /** 155 | Sign out from the current account 156 | */ 157 | public func signOut() { 158 | _authData = AuthData(accountId: nil) 159 | storage[_authDataKey] = nil 160 | } 161 | } 162 | --------------------------------------------------------------------------------