BTCMarkets / API

API
120 stars 30 forks source link

Authentication issue with Swift #172

Closed mitch-1211 closed 4 years ago

mitch-1211 commented 4 years ago

Hi team,

I am having an issue authenticating a request for list of trades. I am aware that the v3 API has been released and includes updated functionality, however I need to get this code working with v2 of the API for now.

The below code has previously been working fine. Testing tonight (7/10/19) and am receiving {"success":false,"errorCode":1,"errorMessage":"Authentication failed."}

I have tested my code with the sample API key provided in the v2 API docs and I am able to recreate the expected signature.

Code is as follows:

getHoldingsData(Coin: "BTC", CoinURL: "/v2/order/trade/history/BTC/AUD")

    func getHoldingsData(Coin: String, CoinURL: String){

        var transactionArray = [Transaction]()
        transactionArray.removeAll()

        let privateAPIKey = savedPrivateKey!
        let publicAPIKey = savedPublicKey!

        guard let payload = Data(base64Encoded: privateAPIKey) else {errorEncountered()
            return}

        let decodedPrivateAPIKey = convert64EncodedToHex(payload)
        print("holdings secret key: \(decodedPrivateAPIKey)")
        let timestamp = "\(Int64(Date().timeIntervalSince1970*1000))"
        let stringToSign = CoinURL + "\n" + timestamp + "\n"
        let decodedPrivateAPIKeyByteArray = decodedPrivateAPIKey.hexa2Bytes
        let stringToSignData: [UInt8] = Array(stringToSign.utf8)
        var signature = ""
        do {
            let signatureArray = try HMAC(key: decodedPrivateAPIKeyByteArray, variant: .sha512).authenticate(stringToSignData)
            if let conversion = signatureArray.toBase64(){
                signature = conversion
            }

        }catch{
            print(error)
        }

        let jsonUrlString = "https://api.btcmarkets.net" + CoinURL
        let url = URL(string: jsonUrlString)!
        let session = URLSession.shared

        var request = URLRequest(url: url)
        request.httpMethod = "GET"

        request.addValue("application/json", forHTTPHeaderField: "Accept")
        request.addValue("UTF-8", forHTTPHeaderField: "Accept-Charset")
        request.addValue("application/json", forHTTPHeaderField: "Content-Type")

        request.addValue(publicAPIKey, forHTTPHeaderField: "apikey")
        request.addValue(timestamp, forHTTPHeaderField: "timestamp")
        request.addValue(signature, forHTTPHeaderField: "signature")

        session.dataTask(with: request as URLRequest) { (data, response, err) in
            if err != nil {
                print(err)
            }else {
                print("holdings: have received website response")
                let dataAsString = String(data: data!, encoding: .utf8)
                print("Webisite Response: " + dataAsString!)
                guard let data = data, let jsonObj = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) else { self.errorEncountered()
                    self.URLdispatchGroup.leave()
                    return }
                guard let dict = jsonObj as? NSDictionary else {self.errorEncountered()
                    self.URLdispatchGroup.leave()
                    return }
                guard let dataArray = dict.value(forKey: "trades") as? [NSDictionary] else {self.errorEncountered()
                    //self.URLdispatchGroup.leave()
                    return }

                for object in dataArray {

                    //print("\(object.value(forKey: "price")) \(object.value(forKey: "volume"))")
                    let type = object.value(forKey: "side") as! String
                    if type == "Bid" {

                        var price = object.value(forKey: "price") as! Double
                        var volume = object.value(forKey: "volume") as! Double
                        let date = object.value(forKey: "creationTime") as! Double

                        price = price/1e8
                        volume = volume/1e8

                        var icon = UIImage(named: "bitcoin")
                        if Coin == "BTC" {
                            icon = UIImage(named: "bitcoin")
                        }else if Coin == "BCHSV" {
                            icon = UIImage(named: "bitcoincash")
                        }else if Coin == "BCHABC" {
                            icon = UIImage(named: "bitcoincash")
                        }else if Coin == "ETH" {
                            icon = UIImage(named: "ethereum")
                        }else if Coin == "ETC" {
                            icon = UIImage(named: "ethclassic")
                        }else if Coin == "LTC" {
                            icon = UIImage(named: "litecoin")
                        }else if Coin == "XRP" {
                            icon = UIImage(named: "ripple")
                        }else if Coin == "OMG" {
                            icon = UIImage(named: "OMG")
                        }else if Coin == "POWR" {
                            icon = UIImage(named: "POWR")
                        }else if Coin == "BAT" {
                            icon = UIImage(named: "BAT")
                        }else if Coin == "XLM" {
                            icon = UIImage(named: "XLM")
                        }else if Coin == "GNT" {
                            icon = UIImage(named: "GNT")
                        }

                        let transaction = Transaction(crypto: Coin, amount: volume, buyPrice: price, icon: icon, date: date)
                        print(transaction!)
                        transactionArray.append(transaction!)
                    }

                }
                print("count1:  \(transactionArray.count)")
                self.transactions += transactionArray
                print("count2:  \(self.transactions.count)")
                self.URLdispatchGroup.leave()

            }
            }.resume()

    }
mitch-1211 commented 4 years ago

Hi,

So I have also tried using v3 of the API in Swift as follows:

func getHoldingsData(Coin: String, CoinURL: String, data: String){

    var transactionArray = [Transaction]()
    transactionArray.removeAll()

   let privateAPIKey = savedPrivateKey!
  let publicAPIKey = savedPublicKey!

    guard let payload = Data(base64Encoded: privateAPIKey) else {errorEncountered()
        return}

    let decodedPrivateAPIKey = convert64EncodedToHex(payload)
    let timestamp = "\(Int64(Date().timeIntervalSince1970*1000))"
   let CoinURL = "/v3/trades"
    let stringToSign = "GET" + CoinURL + timestamp

    print("string to sign: \(stringToSign)")

    let decodedPrivateAPIKeyByteArray = decodedPrivateAPIKey.hexa2Bytes
    let stringToSignData: [UInt8] = Array(stringToSign.utf8)
    var signature = ""
    do {
        let signatureArray = try HMAC(key: decodedPrivateAPIKeyByteArray, variant: .sha512).authenticate(stringToSignData)
        if let conversion = signatureArray.toBase64(){
            signature = conversion
        }

    }catch{
        print(error)
    }

    let jsonUrlString = "https://api.btcmarkets.net" + CoinURL
    let url = URL(string: jsonUrlString)!
    let session = URLSession.shared

    var request = URLRequest(url: url)
    request.httpMethod = "GET"

    request.setValue("application/json", forHTTPHeaderField: "Accept")
    request.setValue("UTF-8", forHTTPHeaderField: "Accept-Charset")
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    request.setValue(publicAPIKey, forHTTPHeaderField: "BM-AUTH-APIKEY")
     request.setValue(timestamp, forHTTPHeaderField: "BM-AUTH-TIMESTAMP")
     request.setValue(signature, forHTTPHeaderField: "BM-AUTH-SIGNATURE")

    print("holdings: signature \(signature)")
    print("holdings: apikey \(publicAPIKey)")
    print("holdings: timestamp \(timestamp)")

    session.dataTask(with: request as URLRequest) { (data, response, err) in
        if err != nil {
            print(err)
        }else {
            print("holdings: have received website response")
            let dataAsString = String(data: data!, encoding: .utf8)
            print("Website Response: " + dataAsString!)
            guard let data = data, let jsonObj = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) else { self.errorEncountered()
                self.URLdispatchGroup.leave()
                return }
            guard let dict = jsonObj as? NSDictionary else {self.errorEncountered()
                self.URLdispatchGroup.leave()
                return }
            guard let dataArray = dict.value(forKey: "trades") as? [NSDictionary] else {self.errorEncountered()
                //self.URLdispatchGroup.leave()
                return }

            for object in dataArray {

                //print("\(object.value(forKey: "price")) \(object.value(forKey: "volume"))")
                let type = object.value(forKey: "side") as! String
                if type == "Bid" {

                    var price = object.value(forKey: "price") as! Double
                    var volume = object.value(forKey: "volume") as! Double
                    let date = object.value(forKey: "creationTime") as! Double

                    price = price/1e8
                    volume = volume/1e8

                    var icon = UIImage(named: "bitcoin")
                    if Coin == "BTC" {
                        icon = UIImage(named: "bitcoin")
                    }else if Coin == "BCHSV" {
                        icon = UIImage(named: "bitcoincash")
                    }else if Coin == "BCHABC" {
                        icon = UIImage(named: "bitcoincash")
                    }else if Coin == "ETH" {
                        icon = UIImage(named: "ethereum")
                    }else if Coin == "ETC" {
                        icon = UIImage(named: "ethclassic")
                    }else if Coin == "LTC" {
                        icon = UIImage(named: "litecoin")
                    }else if Coin == "XRP" {
                        icon = UIImage(named: "ripple")
                    }else if Coin == "OMG" {
                        icon = UIImage(named: "OMG")
                    }else if Coin == "POWR" {
                        icon = UIImage(named: "POWR")
                    }else if Coin == "BAT" {
                        icon = UIImage(named: "BAT")
                    }else if Coin == "XLM" {
                        icon = UIImage(named: "XLM")
                    }else if Coin == "GNT" {
                        icon = UIImage(named: "GNT")
                    }

                    let transaction = Transaction(crypto: Coin, amount: volume, buyPrice: price, icon: icon, date: date)
                    print(transaction!)
                    transactionArray.append(transaction!)
                }

            }
            print("count1:  \(transactionArray.count)")
            self.transactions += transactionArray
            print("count2:  \(self.transactions.count)")
            self.URLdispatchGroup.leave()

        }
        }.resume()

}

I get the following message returned from the API:

Website Response: {"code":"InvalidAuthSignature","message":"invalid signature"}

I have previously validated the signing using the example in v1/v2 of the API and able to recreate the example signature. Am I missing something in my implementation?

martin-nginio commented 4 years ago

Hi @dev-duzza

Thanks for your feedback.

Authentication between v3 and v2 are very similar (except for how the message is created to be signed).

Our team will try to provide a working sample of authentication using Swift language in the coming days and I'm hoping that helps with your project.

In the meantime, please note we have sample working code (covers authentication and http handling ) in few programming languages including Java, Javascript, .NET, Python, Php, etc so please feel free to take a look as they might be helpful for Swift as well. https://github.com/BTCMarkets

Thanks.

Regards, Martin

mitch-1211 commented 4 years ago

Hi @martin-nginio

Thanks for getting back to me. A working example for Swift would be amazing. If possible can it be for a GET request and POST request to see the difference between?

I’ve had a browse of the other example code but am struggling to replicate in Swift.

Thanks, Mitch

martin-nginio commented 4 years ago

In other languages we have provided GET, POST and DELETE and I'm hoping we can provide the same for Swift as well.

Thanks.

mitch-1211 commented 4 years ago

Thanks Martin, much appreciated.

By the way, I am using cryptoSwift library for the HMAC signing. It can be installed using cocoaPods

martin-nginio commented 4 years ago

Thanks @dev-duzza. I will let our team know.

mitch-1211 commented 4 years ago

Just to add a little more information, this issue seems to only occur when using Xcode 11 and iOS13

martin-nginio commented 4 years ago

Hi @dev-duzza

The following code (main body of a Swift command line application using CryptoSwift library) was provided by a team member. It command line app works for http get and post. Please feel free to check this out. Later on we will add this to the repo with the rest of the Swift project files (e.g. main.swft, etc).

import Foundation
import SystemConfiguration

let apiKey = "";
let privateKey = "";
let baseUrl = "";

public class CredentialsHTTP : NSObject, URLSessionDelegate
{
    static let sharedInstance : CredentialsHTTP = {
        let instance = CredentialsHTTP()

        return instance
    }()

    override init() {
        super.init()
    }

    func processSessionLoad(_ data: Data) -> Array<Any>
    {
        let jsonResults = try? JSONSerialization.jsonObject(with: data, options: []) as? Array<Any>
        return jsonResults!
    }

    func get_order_request()
    {
        let method = "GET"
        let path = "/v3/orders"
        let dataObj = "?status=open"

        let timestamp = Date().currentTimeMillis()
        let message = method + path + "\(timestamp)";

        let configuration = URLSessionConfiguration.ephemeral
        configuration.timeoutIntervalForRequest = 30

        let localOperationQueue = OperationQueue.main
        localOperationQueue.maxConcurrentOperationCount = 20

        let session = Foundation.URLSession(configuration: configuration, delegate: self, delegateQueue: localOperationQueue)

        let request = self.buildAuthHeaders(method: method, path: path, message: message, timestamp: "\(timestamp)", dataObj: dataObj, jsonObj: nil)

        session.dataTask(with: request, completionHandler:
        {
            (data, response, error) -> Void in

            guard error == nil else {
                print(error)
                return
            }

            if let data = data
            {
                let responsePrint = response as? HTTPURLResponse
                session.finishTasksAndInvalidate()

                if (responsePrint?.statusCode)! == 200
                {
                    print("--------- Printing Get Order Success ---------")
                    print(self.processSessionLoad(data))
                    print("--------- End Printing Get Order Success ---------")
                } else {
                    let payloadJSONHttpCode = self.errorJson(data)
                    print("--------- Printing Get Order Fail ---------")
                    print(payloadJSONHttpCode["message"])
                    print("--------- End Printing Get Order Fail ---------")
                }
                exit(EXIT_SUCCESS)
            }
        }).resume()

        dispatchMain()
    }

    func place_order_request()
    {
        let method = "POST"
        let path = "/v3/orders"
        let dataObj = ""
        let jsonObj: [String:String] = ["marketId": "XRP-AUD", "price": "0.1", "amount": "0.1", "side": "Bid", "type": "Limit"]

        let timestamp = Date().currentTimeMillis()
        let message = method + path + "\(timestamp)" + self.jsonToString(json: jsonObj as AnyObject);

        let configuration = URLSessionConfiguration.ephemeral
        configuration.timeoutIntervalForRequest = 30

        let localOperationQueue = OperationQueue.main
        localOperationQueue.maxConcurrentOperationCount = 20

        let session = Foundation.URLSession(configuration: configuration, delegate: self, delegateQueue: localOperationQueue)

        let request = self.buildAuthHeaders(method: method, path: path, message: message, timestamp: "\(timestamp)", dataObj: dataObj, jsonObj: jsonObj as AnyObject)

        let datatask = session.dataTask(with: request, completionHandler:
        {
            (data, response, error) -> Void in
            guard error == nil else {
                return
            }

            if let data = data
            {
                let responsePrint = response as? HTTPURLResponse
                session.finishTasksAndInvalidate()

                if (responsePrint?.statusCode)! == 200
                {
                    print("--------- Printing Place Order Success ---------")
                    let jsonResults = try? JSONSerialization.jsonObject(with: data, options: []) as? Dictionary<String, Any>
                    print(jsonResults)
                    print("--------- End Printing Place Order Success ---------")
                } else {
                    let payloadJSONHttpCode = self.errorJson(data)
                    print("--------- Printing Place Order Fail ---------")
                    print(payloadJSONHttpCode["message"])
                    print("--------- End Printing Place Order Fail ---------")
                }
                exit(EXIT_SUCCESS)
            }
        })
        datatask.resume()
        dispatchMain()
    }

    func cancel_order_request()
    {
        let method = "DELETE"
        let path = "/v3/orders/1228743"
        let dataObj = ""

        let timestamp = Date().currentTimeMillis()
        let message = method + path + "\(timestamp)";

        let configuration = URLSessionConfiguration.ephemeral
        configuration.timeoutIntervalForRequest = 30

        let localOperationQueue = OperationQueue.main
        localOperationQueue.maxConcurrentOperationCount = 20

        let session = Foundation.URLSession(configuration: configuration, delegate: self, delegateQueue: localOperationQueue)

        let request = self.buildAuthHeaders(method: method, path: path, message: message, timestamp: "\(timestamp)", dataObj: dataObj, jsonObj: nil)

        let datatask = session.dataTask(with: request, completionHandler:
        {
            (data, response, error) -> Void in
            guard error == nil else {
                return
            }

            if let data = data
            {
                let responsePrint = response as? HTTPURLResponse
                session.finishTasksAndInvalidate()

                if (responsePrint?.statusCode)! == 200
                {
                    print("--------- Printing Cancel Order Success ---------")
                    let jsonResults = try? JSONSerialization.jsonObject(with: data, options: []) as? Dictionary<String, Any>
                    print(jsonResults)
                    print("--------- End Printing Cancel Order Success ---------")
                } else {
                    let payloadJSONHttpCode = self.errorJson(data)
                    print("--------- Printing Cancel Order Fail ---------")
                    print(payloadJSONHttpCode["message"])
                    print("--------- End Printing Cancel Order Fail ---------")
                }
                exit(EXIT_SUCCESS)
            }
        })
        datatask.resume()
        dispatchMain()
    }

    func buildAuthHeaders(method: String, path: String, message: String, timestamp: String, dataObj: String, jsonObj: AnyObject?) -> URLRequest {
        let preppedUrlString = String(format: "%@", baseUrl + path + dataObj)

        let url: URL = URL(string: preppedUrlString)!

        var request = URLRequest(url: url)

        request.setValue("application/json", forHTTPHeaderField: "Accept")
        request.setValue("UTF-8", forHTTPHeaderField: "Accept-Charset")
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue(apiKey, forHTTPHeaderField: "BM-AUTH-APIKEY")
        request.setValue("\(timestamp)", forHTTPHeaderField: "BM-AUTH-TIMESTAMP")
        request.setValue(signMessage(message: message), forHTTPHeaderField: "BM-AUTH-SIGNATURE")
        request.httpMethod = method
        request.cachePolicy = NSURLRequest.CachePolicy.reloadIgnoringCacheData

        if let actualJsonObj = jsonObj {
            if (JSONSerialization.isValidJSONObject(jsonObj)) {
                do {
                    request.httpBody = try JSONSerialization.data(withJSONObject: jsonObj, options: .prettyPrinted)
                } catch {
                    return request
                }
            }

        }
        return request
    }

    func signMessage(message: String) -> String {
        guard let payload = Data(base64Encoded: privateKey) else {
            return ""}
        let decodedPrivateAPIKey = convert64EncodedToHex(payload)
        let decodedPrivateAPIKeyByteArray = decodedPrivateAPIKey.hexa
        let stringToSignData: [UInt8] = Array(message.utf8)
        var signature = ""
        do {
            let signatureArray = try HMAC(key: decodedPrivateAPIKeyByteArray, variant: .sha512).authenticate(stringToSignData)
            if let conversion = signatureArray.toBase64(){
                signature = conversion
            }
        } catch {
            print(error)
        }
        return signature
    }

    func convert64EncodedToHex(_ data:Data) -> String {
        return data.map{ String(format: "%02x", $0) }.joined()
    }

    func errorJson(_ data: Data)->Dictionary<String, Any> {
        let jsonResults = try? JSONSerialization.jsonObject(with: data, options: []) as! Dictionary<String, Any>
        return jsonResults!
    }

    func jsonToString(json: AnyObject) -> String {
        do {
            let data1 =  try JSONSerialization.data(withJSONObject: json, options: JSONSerialization.WritingOptions.prettyPrinted) // first of all convert json to the data
            let convertedString = String(data: data1, encoding: String.Encoding.utf8) // the data will be converted to the string
            return convertedString!
        } catch let myJSONError {
            print(myJSONError)
            return ""
        }
    }
}

extension Date {
    func currentTimeMillis() -> Int64 {
        return Int64(self.timeIntervalSince1970 * 1000)
    }
}

extension String {

    func fromBase64() -> String? {
        guard let data = Data(base64Encoded: self) else {
            return nil
        }

        return String(data: data, encoding: .utf8)
    }

    func toBase64() -> String {
        return Data(self.utf8).base64EncodedString()
    }
}

extension StringProtocol {
    var hexa: [UInt8] {
        var startIndex = self.startIndex
        return stride(from: 0, to: count, by: 2).compactMap { _ in
            let endIndex = index(startIndex, offsetBy: 2, limitedBy: self.endIndex) ?? self.endIndex
            defer { startIndex = endIndex }
            return UInt8(self[startIndex..<endIndex], radix: 16)
        }
    }
}

Regards, Martin

mitch-1211 commented 4 years ago

Hi martin,

Thanks very much for that! Can you please confirm the correct value of baseUrl as it never seems to be define. Is it just https://api.btcmarkets.net

martin-nginio commented 4 years ago

Hi @dev-duzza

Yes. that's the correct baseUrl for the Swift sample code.

Regards, Martin

martin-nginio commented 4 years ago

closing the issue and the sample client can be found here: https://github.com/BTCMarkets/api-v3-client-swift

Thanks.

Regards, Martin