Skip to content
This repository has been archived by the owner on Oct 26, 2021. It is now read-only.

Commit

Permalink
Create CoinbaseApi struct with static API functions #17
Browse files Browse the repository at this point in the history
Coinbase client side authentication flow #15
Create CoinbaseAccount object which parses the response #18
Create function to create native Accounts from CoinbaseAccounts #19
  • Loading branch information
einsteinx2 committed Jun 15, 2017
1 parent 3576777 commit c570807
Show file tree
Hide file tree
Showing 8 changed files with 306 additions and 48 deletions.
4 changes: 4 additions & 0 deletions BalanceForBlockchain.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@
53946FA01EEB10AA00E921C3 /* KeychainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53946F9E1EEB10AA00E921C3 /* KeychainManager.swift */; };
53946FA31EEF0B0B00E921C3 /* CoinbaseAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53946FA21EEF0B0B00E921C3 /* CoinbaseAccount.swift */; };
53946FA51EF0035200E921C3 /* CoinbaseApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53946FA41EF0035200E921C3 /* CoinbaseApi.swift */; };
53F8E2EA1EF1B6E90071FC73 /* Currency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53F8E2E91EF1B6E90071FC73 /* Currency.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -284,6 +285,7 @@
53946F9E1EEB10AA00E921C3 /* KeychainManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainManager.swift; sourceTree = "<group>"; };
53946FA21EEF0B0B00E921C3 /* CoinbaseAccount.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinbaseAccount.swift; sourceTree = "<group>"; };
53946FA41EF0035200E921C3 /* CoinbaseApi.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinbaseApi.swift; sourceTree = "<group>"; };
53F8E2E91EF1B6E90071FC73 /* Currency.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Currency.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -568,6 +570,7 @@
children = (
532122681EEAD7BD00F4812A /* Account.swift */,
5321227C1EEAD8DA00F4812A /* AccountType.swift */,
53F8E2E91EF1B6E90071FC73 /* Currency.swift */,
532122691EEAD7BD00F4812A /* Institution.swift */,
5321227A1EEAD8D000F4812A /* Source.swift */,
532122151EEAC8F800F4812A /* Syncer.swift */,
Expand Down Expand Up @@ -792,6 +795,7 @@
5321229E1EEAE00600F4812A /* SyncButton.swift in Sources */,
532122301EEAD1BB00F4812A /* PopoverViewController.swift in Sources */,
53946FA51EF0035200E921C3 /* CoinbaseApi.swift in Sources */,
53F8E2EA1EF1B6E90071FC73 /* Currency.swift in Sources */,
532122CD1EEAFF6300F4812A /* NSWindow.swift in Sources */,
532122AC1EEAE56400F4812A /* Shortcut.swift in Sources */,
5321221C1EEAC90C00F4812A /* DefaultsStorage.swift in Sources */,
Expand Down
36 changes: 22 additions & 14 deletions BalanceForBlockchain/Account.swift
Original file line number Diff line number Diff line change
Expand Up @@ -255,17 +255,21 @@ class Account: Equatable {
}
}

static func allAccounts() -> [Account] {
static func allAccounts(includeHidden: Bool = false) -> [Account] {
var accounts = [Account]()

let hiddenAccountIds = defaults.hiddenAccountIds

database.readDbPool.inDatabase { db in
do {
let statement = "SELECT * FROM accounts ORDER BY institutionId, name"
let result = try db.executeQuery(statement)
while result.next() {
let resultArray = arrayFromResult(result)
let account = Account(resultArray)
accounts.append(account)
if includeHidden || !hiddenAccountIds.contains(account.accountId) {
accounts.append(Account(resultArray))
}
}
result.close()
} catch {
Expand All @@ -276,9 +280,11 @@ class Account: Equatable {
return accounts
}

static func accountsByInstitution() -> OrderedDictionary<Institution, [Account]> {
static func accountsByInstitution(includeHidden: Bool = false) -> OrderedDictionary<Institution, [Account]> {
var accountsByInstitutionId = [Int: [Account]]()

let hiddenAccountIds = defaults.hiddenAccountIds

database.readDbPool.inDatabase { db in
do {
let statement = "SELECT * FROM accounts ORDER BY institutionId, name"
Expand All @@ -287,11 +293,13 @@ class Account: Equatable {
while result.next() {
let resultArray = arrayFromResult(result)
let account = Account(resultArray)
if var accounts = accountsByInstitutionId[account.institutionId] {
accounts.append(account)
accountsByInstitutionId[account.institutionId] = accounts
} else {
accountsByInstitutionId[account.institutionId] = [account]
if includeHidden || !hiddenAccountIds.contains(account.accountId) {
if var accounts = accountsByInstitutionId[account.institutionId] {
accounts.append(account)
accountsByInstitutionId[account.institutionId] = accounts
} else {
accountsByInstitutionId[account.institutionId] = [account]
}
}
}
result.close()
Expand Down Expand Up @@ -367,17 +375,21 @@ class Account: Equatable {
}
}

static func accountsForInstitution(institutionId: Int) -> [Account] {
static func accountsForInstitution(institutionId: Int, includeHidden: Bool = false) -> [Account] {
var accounts = [Account]()

let hiddenAccountIds = defaults.hiddenAccountIds

database.readDbPool.inDatabase { db in
do {
let statement = "SELECT * FROM accounts WHERE institutionId = ? ORDER BY name"
let result = try db.executeQuery(statement, institutionId)
while result.next() {
let resultArray = arrayFromResult(result)
let account = Account(resultArray)
accounts.append(account)
if includeHidden || !hiddenAccountIds.contains(account.accountId) {
accounts.append(Account(resultArray))
}
}
result.close()
} catch {
Expand All @@ -395,10 +407,6 @@ class Account: Equatable {
// Begin transaction
try db.executeUpdate("BEGIN")

// Delete transaction records
let statement1 = "DELETE FROM transactions WHERE accountId = ?"
try db.executeUpdate(statement1, accountId)

// Delete account records
let statement2 = "DELETE FROM accounts WHERE accountId = ?"
try db.executeUpdate(statement2, accountId)
Expand Down
49 changes: 49 additions & 0 deletions BalanceForBlockchain/CoinbaseAccount.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,55 @@

import Foundation

fileprivate var decimalFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.generatesDecimalNumbers = true
formatter.numberStyle = .decimal
return formatter
}()

fileprivate var jsonDateFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
dateFormatter.timeZone = TimeZone(identifier: "UTC")
return dateFormatter
}()

struct CoinbaseAccount {
let id: String
let name: String
let primary: Bool
let type: String

let currency: String
let balance: Decimal
let nativeCurrency: String
let nativeBalance: Decimal

// let createdAt: Date
// let updatedAt: Date

init(account: [String: AnyObject]) throws {
self.id = try checkType(account, name: "id")
self.name = try checkType(account, name: "name")
self.primary = try checkType(account, name: "primary")
self.type = try checkType(account, name: "type")

let balanceDict: [String: AnyObject] = try checkType(account, name: "balance")
self.currency = try checkType(balanceDict, name: "currency")
let balanceAmount: String = try checkType(balanceDict, name: "amount")
let balanceAmountDecimal = decimalFormatter.number(from: balanceAmount) as? Decimal
self.balance = try checkType(balanceAmountDecimal, name: "balanceAmountDecimal")

let nativeBalanceDict: [String: AnyObject] = try checkType(account, name: "native_balance")
self.nativeCurrency = try checkType(nativeBalanceDict, name: "currency")
let nativeBalanceAmount: String = try checkType(nativeBalanceDict, name: "amount")
let nativeBalanceAmountDecimal = decimalFormatter.number(from: nativeBalanceAmount) as? Decimal
self.nativeBalance = try checkType(nativeBalanceAmountDecimal, name: "balanceAmountDecimal")

// TODO: Finish this
// let createdAtString: String = try checkType(account, name: "created_at")
// self.createdAt = jsonDateFormatter.date(from: createdAtString) ?? throw "
// self.updatedAt = try checkType(account, name: "updated_at")
}
}
132 changes: 128 additions & 4 deletions BalanceForBlockchain/CoinbaseApi.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
import AppKit
import Locksmith

typealias SuccessErrorBlock = (_ success: Bool, _ error: Error) -> Void
typealias SuccessErrorBlock = (_ success: Bool, _ error: Error?) -> Void

fileprivate let connectionTimeout = 30.0
fileprivate let baseUrl = "http://localhost:8080/"
fileprivate let subServerUrl = "http://localhost:8080/"
fileprivate let clientId = "e47cf82db1ab3497eb06f96bcac0dde027c90c24a977c0b965416e7351b0af9f"

// Save random state for current authentication request
Expand Down Expand Up @@ -51,7 +51,7 @@ struct CoinbaseApi {
}

lastState = nil
let urlString = "\(baseUrl)coinbase/convertCode"
let urlString = "\(subServerUrl)coinbase/convertCode"
let url = URL(string: urlString)!
var request = URLRequest(url: url)
request.timeoutInterval = connectionTimeout
Expand All @@ -78,7 +78,7 @@ struct CoinbaseApi {
institution?.refreshToken = refreshToken
institution?.tokenExpireDate = Date().addingTimeInterval(expiresIn - 10.0)
DispatchQueue.main.async {
completion(false, "state does not match saved state")
completion(true, nil)
}
} catch {
DispatchQueue.main.async {
Expand All @@ -89,6 +89,130 @@ struct CoinbaseApi {

task.resume()
}

static func refreshAccessToken(institution: Institution, completion: @escaping SuccessErrorBlock) {
guard let refreshToken = institution.refreshToken else {
completion(false, "missing refreshToken")
return
}

let urlString = "\(subServerUrl)coinbase/refreshToken"
let url = URL(string: urlString)!
var request = URLRequest(url: url)
request.timeoutInterval = connectionTimeout
request.cachePolicy = .reloadIgnoringLocalCacheData
request.httpMethod = "POST"
let parameters = "{\"refreshToken\":\"\(refreshToken)\"}"
request.httpBody = parameters.data(using: .utf8)

// TODO: Create enum types for each error
let task = session.dataTask(with: request, completionHandler: { (maybeData, maybeResponse, maybeError) in
do {
// Make sure there's data
guard let data = maybeData, maybeError == nil else {
throw "No data"
}

// Try to parse the JSON
guard let JSONResult = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: AnyObject], let accessToken = JSONResult["accessToken"] as? String, accessToken.length > 0, let refreshToken = JSONResult["refreshToken"] as? String, refreshToken.length > 0, let expiresIn = JSONResult["expiresIn"] as? TimeInterval else {
throw "JSON decoding failed"
}

// Update the model
institution.accessToken = accessToken
institution.refreshToken = refreshToken
institution.tokenExpireDate = Date().addingTimeInterval(expiresIn - 10.0)
DispatchQueue.main.async {
completion(true, nil)
}
} catch {
DispatchQueue.main.async {
completion(false, error)
}
}
})

task.resume()
}

static func updateAccounts(institution: Institution, completion: @escaping SuccessErrorBlock) {
guard let accessToken = institution.accessToken else {
completion(false, "missing access token")
return
}

let urlString = "https://api.coinbase.com/v2/accounts"
let url = URL(string: urlString)!
var request = URLRequest(url: url)
request.timeoutInterval = connectionTimeout
request.cachePolicy = .reloadIgnoringLocalCacheData
request.httpMethod = "GET"
request.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization")
request.setValue("2017-06-14", forHTTPHeaderField: "CB-VERSION")

// TODO: Create enum types for each error
let task = session.dataTask(with: request, completionHandler: { (maybeData, maybeResponse, maybeError) in
do {
// Make sure there's data
guard let data = maybeData, maybeError == nil else {
throw "No data"
}

// Try to parse the JSON
guard let JSONResult = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: AnyObject], let accountDicts = JSONResult["data"] as? [[String: AnyObject]] else {
throw "JSON decoding failed"
}

// Create the CoinbaseAccount objects
var coinbaseAccounts = [CoinbaseAccount]()
for accountDict in accountDicts {
do {
let coinbaseAccount = try CoinbaseAccount(account: accountDict)
coinbaseAccounts.append(coinbaseAccount)
} catch {
log.error("Failed to parse account data: \(error)")
}
}

// Create native Account objects and update them
self.processCoinbaseAccounts(coinbaseAccounts, institution: institution)

DispatchQueue.main.async {
completion(true, nil)
}
} catch {
DispatchQueue.main.async {
completion(false, error)
}
}
})

task.resume()
}

static func processCoinbaseAccounts(_ coinbaseAccounts: [CoinbaseAccount], institution: Institution) {
// Add/update accounts
for ca in coinbaseAccounts {
// Initialize an Account object to insert the record
// TODO: Look into how to handle source institution ids
// TODO: Add support for the native balance stuff
// TODO!: Add currency support to accounts
_ = Account(institutionId: institution.institutionId, sourceId: institution.sourceId, sourceAccountId: ca.id, sourceInstitutionId: "", accountTypeId: AccountType.depository, accountSubTypeId: nil, name: ca.name, currentBalance: 0, availableBalance: nil, number: nil)
}

// Remove accounts that no longer exist
// TODO: In the future, when we have metadata associated with accounts / transactions, we'll need to
// migrate that metadata to a new account if it is a replacement for an old one. In my case, my Provident
// Credit Union at some point returned new accounts with new source account ids with better formatted names.
let accounts = Account.accountsForInstitution(institutionId: institution.institutionId)
for account in accounts {
let index = coinbaseAccounts.index(where: {$0.id == account.sourceAccountId})
if index == nil {
// This account doesn't exist in the coinbase response, so remove it
Account.removeAccount(accountId: account.accountId)
}
}
}
}

extension Institution {
Expand Down
21 changes: 21 additions & 0 deletions BalanceForBlockchain/Currency.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//
// Currency.swift
// BalanceForBlockchain
//
// Created by Benjamin Baron on 6/14/17.
// Copyright © 2017 Balanced Software, Inc. All rights reserved.
//

import Foundation

enum Currency: String {
// Fiat
case usd = "USD"
case eur = "EUR"
case gbp = "GBP"

// Crypto
case BTC = "BTC"
case LTC = "LTC"
case ETH = "ETH"
}
Loading

0 comments on commit c570807

Please sign in to comment.