Bitstamp API Authentication - ruby

Im a ruby fan and i am also buying/selling bitcoins on the bitstamp exchange. I have used the bitstamp api successfully in the past with python and i also want to try ruby for that task. I have not been successfull with that though.
Here is a python code that works
import hashlib
import hmac
import time
import requests
import uuid
import sys
api_key = 'some api key'
API_SECRET = b'some secret api key'
timestamp = str(int(round(time.time() * 1000)))
nonce = str(uuid.uuid4())
content_type = 'application/x-www-form-urlencoded'
payload = {'offset': '1'}
if sys.version_info.major >= 3:
from urllib.parse import urlencode
else:
from urllib import urlencode
payload_string = urlencode(payload)
# '' (empty string) in message represents any query parameters or an empty string in case there are none
message = 'BITSTAMP ' + api_key + \
'POST' + \
'www.bitstamp.net' + \
'/api/v2/user_transactions/' + \
'' + \
content_type + \
nonce + \
timestamp + \
'v2' + \
payload_string
message = message.encode('utf-8')
signature = hmac.new(API_SECRET, msg=message, digestmod=hashlib.sha256).hexdigest()
headers = {
'X-Auth': 'BITSTAMP ' + api_key,
'X-Auth-Signature': signature,
'X-Auth-Nonce': nonce,
'X-Auth-Timestamp': timestamp,
'X-Auth-Version': 'v2',
'Content-Type': content_type
}
r = requests.post(
'https://www.bitstamp.net/api/v2/user_transactions/',
headers=headers,
data=payload_string
)
if not r.status_code == 200:
raise Exception('Status code not 200')
string_to_sign = (nonce + timestamp + r.headers.get('Content-Type')).encode('utf-8') + r.content
signature_check = hmac.new(API_SECRET, msg=string_to_sign, digestmod=hashlib.sha256).hexdigest()
if not r.headers.get('X-Server-Auth-Signature') == signature_check:
raise Exception('Signatures do not match')
print(r.content)
And here is my Ruby code
require "awesome_print"
require "json"
require "securerandom"
require "openssl"
require "uri"
require "net/http"
api_key = 'Some api key'
API_SECRET = 'Some secret api key'
nonce = SecureRandom.uuid
timestamp = Time.new.strftime('%s%L')
content_type = 'application-x-www-form-urlencoded'
data= {'offset' => '1'}
data_string = URI.encode_www_form(data)
message = 'BITSTAMP' + '' + api_key + 'POST' + 'www.bitstamp.net' + 'api/v2/balance' + '' +
content_type + nonce + timestamp + 'v2' + data_string
message.encode("UTF-8")
signature = OpenSSL::HMAC.hexdigest("SHA256", API_SECRET, message)
headers = { 'X-Auth' => 'BITSTAMP ' + api_key,
'X-Auth-Signature' => signature,
'X-Auth-Nonce' => nonce,
'X-Auth-Timestamp' => timestamp,
'X-Auth-Version' => 'V2',
'Content-Type' => content_type
}
uri = URI("https://www.bitstamp.net/api/v2/balance/")
res = Net::HTTP.post_form(uri, {headers => headers, data => data_string})
puts res.body
The api gives me back an error. The error is
{“status”: “error”, “reason”: “Missing key, signature and nonce parameters”, “code”: “API0000”}
Im a novice programmer so i don’t have deep skills in this. If anyone has an idea to get this ruby code to work that would
be highly appreciated. Thanx for reading.
Regards.
Primtala

Related

Coinbase API Request in Ruby always returns Invalid API Key

I am trying to learn how to make simple requests to the Coinbase API in Ruby. This is mostly for my own entertainment and education. The Ruby GEM is so out of date I thought I'd start working on my own system instead of relying on it. To that end I wanted to get the basics working before I tried to turn it into a gem.
Here is the beginnings of my module.
And no matter what, when I try to get /accounts I get a 401 response.
#status=401 #reason_phrase="Unauthorized" #response_body="{\"message\":\"Invalid API Key\"}
# Simple Coinbase Requests
require 'base64'
require 'openssl'
require 'json'
require 'faraday'
module Coinbase
module Request
class API
def initialize key = '', secret = '', passphrase = '', sandbox = false
#url = sandbox ? 'https://api-public.sandbox.pro.coinbase.com' : 'https://api-public.pro.coinbase.com'
#key = key
#secret = secret
#passphrase = passphrase
#content_type = 'application/json'
end
# Get Accounts
def accounts
self.get '/accounts'
end
# Do the work of a HTTP Get
def get path
timestamp = Time.now.to_i.to_s
headers = auth_headers path, '', 'GET', timestamp
connection = Faraday.new(url: 'https://api-public.sandbox.pro.coinbase.com', headers: headers, ssl: { :verify => false })
# puts connection.inspect
connection.get path
end
# Auth Headers
# CB-ACCESS-KEY The api key as a string.
# CB-ACCESS-SIGN The base64-encoded signature (see Signing a Message).
# CB-ACCESS-TIMESTAMP A timestamp for your request.
# CB-ACCESS-PASSPHRASE The passphrase you specified when
def auth_headers path = '', body = '', method = 'GET', timestamp = nil
{
'Content-Type': 'Application/JSON',
'CB-ACCESS-SIGN': self.signature(path, body, method, timestamp),
'CB-ACCESS-TIMESTAMP': timestamp,
'CB-ACCESS-KEY': #key,
'CB-ACCESS-PASSPHRASE': #passphrase
}
end
# Generate a signature
def signature path = '', body = '', method = 'GET', timestamp = nil
body = body.to_json if body.is_a?(Hash)
secret = Base64.decode64 #secret
hash = OpenSSL::HMAC.digest 'sha256', secret, "#{timestamp}#{method}#{path}#{body}"
Base64.strict_encode64 hash
end
end
end
end
I'm calling it using the following (THE KEY/SECRET/PASSPHRASE ARE FAKE for this example)
coinbase = Coinbase::Request::API.new('123426bc3a583fb8393141fb7777fake',
'+FAKEbGoG1eT1WVFWNJxFtTE/y4kIYq2Lbf6FAKEw5j2756GXgaqg5iXTsqPJXKkStZ7nPoTT2RGKwiJfRFAKE==',
'FAKEPASSPHRASE',
true)
puts coinbase.accounts.inspect
The signature "what" (as defined as "#{timestamp}#{method}#{path}#{body}" in your docs) for this simple request would be something like 1624063589GET/accounts
The headers come out to {:"Content-Type"=>"Application/JSON", :"CB-ACCESS-SIGN"=>"rs29GSZuRspthioywb5IkaHQmPIwH5DRDW5LHoYUvw8=", :"CB-ACCESS-TIMESTAMP"=>"1624063726", :"CB-ACCESS-KEY"=>"123426bc3a583fb8393141fb22328113", :"CB-ACCESS-PASSPHRASE"=>"FAKEPASSPHRASE"}
Once connected the headers come out as seen below in the faraday response object.
This is the full response object output which is what I would expect to get with fake keys as used but I get the same thing when I use the keys I generated here: https://pro.coinbase.com/profile/ap
#<Faraday::Response:0x00000001621a8e58 #on_complete_callbacks=[], #env=#<Faraday::Env #method=:get #url=#<URI::HTTPS https://api-public.sandbox.pro.coinbase.com/accounts> #request=#<Faraday::RequestOptions (empty)> #request_headers={"Content-type"=>"Application/JSON", "Cb-access-sign"=>"vEfjUnFy+3qQqRa2lxvEC5O32xOa6t7NgGAxO8OYrpo=", "Cb-access-timestamp"=>"1624063280", "Cb-access-key"=>"123426bc3a583fb8393141fb22328113", "Cb-access-passphrase"=>"FAKEPASSPHRASE", "User-Agent"=>"Faraday v1.4.2"} #ssl=#<Faraday::SSLOptions (empty)> #response=#<Faraday::Response:0x00000001621a8e58 ...> #response_headers={"date"=>"Sat, 19 Jun 2021 00:41:21 GMT", "content-type"=>"application/json; charset=utf-8", "content-length"=>"29", "connection"=>"keep-alive", "access-control-allow-headers"=>"Content-Type, Accept, cb-session, cb-fp, cb-form-factor", "access-control-allow-methods"=>"GET,POST,DELETE,PUT", "access-control-allow-origin"=>"*", "access-control-expose-headers"=>"cb-before, cb-after, cb-gdpr", "access-control-max-age"=>"7200", "cache-control"=>"no-store", "etag"=>"W/\"1d-mmRSeO9uba2rhQtGfy4YjixIkt4\"", "strict-transport-security"=>"max-age=15552000; includeSubDomains", "x-content-type-options"=>"nosniff", "x-dns-prefetch-control"=>"off", "x-download-options"=>"noopen", "x-frame-options"=>"SAMEORIGIN", "x-xss-protection"=>"1; mode=block", "cf-cache-status"=>"MISS", "cf-request-id"=>"0ac3501f6300005ae1c99c3000000001", "expect-ct"=>"max-age=604800, report-uri=\"https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct\"", "vary"=>"Accept-Encoding", "server"=>"cloudflare", "cf-ray"=>"6618b6123ba75ae1-IAD"} #status=401 #reason_phrase="Unauthorized" #response_body="{\"message\":\"Invalid API Key\"}">>
I've been plugging away at this for days and I just can't seem to get it to not return Invalid API Key. I feel like I'm so very close.. but no cigar..
Thoughts?
NOTE: On a whim before submitting this I tried /currencies and it worked fine.
It took me some trial and error with this too, but I was hitting invalid signature errors rather than invalid API key errors. Maybe this will help anyway:
Working example:
Gemfile
source 'https://rubygems.org'
ruby '2.6.7'
gem 'httparty'
coinbase.rb
require 'base64'
require 'httparty'
API_KEY = ''
API_PASSPHRASE = ''
API_SECRET = ''
key = Base64.decode64(API_SECRET)
url = "https://api.exchange.coinbase.com"
requestPath = "/accounts"
method = "GET"
body = ""
timestamp = Time.now.to_i.to_s
message = timestamp + method + requestPath + body
hmac = OpenSSL::HMAC.digest('sha256', key, message)
signature = Base64.strict_encode64(hmac)
headers = {
'Content-Type': 'application/json',
'CB-ACCESS-SIGN': signature,
'CB-ACCESS-TIMESTAMP': timestamp,
'CB-ACCESS-KEY': API_KEY,
'CB-ACCESS-PASSPHRASE': API_PASSPHRASE
}
result = HTTParty.get(url+requestPath, headers: headers)
puts result.response.body
Run:
bundle install
bundle exec coinbase.rb

How can I past a variable in request with HTTParty?

I need to generate a value using Post and pass this value in the query and delete. How to do this?
Is it possible to pass the value of a variable directly in the def retrieve method of request get or delete?
I want to use the same value generated in the var that stores the faker gem and pass both get and delete.
require 'HTTParty'
require 'httparty/request'
require 'httparty/response/headers'
class Crud
include HTTParty
def create
##codigo = Faker::Number.number(digits: 5)
#nome = Faker::Name.first_name
#salario = Faker::Number.decimal(l_digits: 4, r_digits: 2)
#idade = Faker::Number.number(digits: 2)
#base_url = 'http://dummy.restapiexample.com/api/v1/create'
#body = {
"id":##codigo,
"name":#nome,
"salary":#salario,
"age":#idade
}.to_json
#headers = {
"Accept": 'application/vnd.tasksmanager.v2',
'Content-Type': 'application/json'
}
##request = Crud.post(#base_url, body: #body, headers: #headers)
end
def retrieve
self.class.get('http://dummy.restapiexample.com/api/v1/employee/1')
end
end
Just parse response from API and use fetched id. You don't need to pass id when create an employee, it is generated automatically
class Crud
include HTTParty
base_uri 'http://dummy.restapiexample.com/api/v1'
def create
nome = Faker::Name.first_name
salario = Faker::Number.decimal(l_digits: 4, r_digits: 2)
idade = Faker::Number.number(digits: 2)
#note, you should pass body as JSON string
body = { name: nome, salary: salario, age: idade }.to_json
headers = {
'Accept' => 'application/vnd.tasksmanager.v2',
'Content-Type' => 'application/json'
}
self.class.post('/create', body: body, headers: headers)
end
def retrieve(id)
self.class.get("/employee/#{ id }")
end
end
> client = Crud.new
> response = client.create
> id = JSON.parse(response)['id']
> client.retrieve(id)
Please, read about variables in ruby - what is the difference between local, instance and global variables. Global variables should be used in rare case, more often you need instance/local ones.

HTTP GET request with session key

I'm a bit of newbie regarding HTTP GET/POST request. I want to use a get request that requires some kind of authorization.
I'm trying to use the following api API DOCUMENTATION.
Under "get list" it says that it wants the following parameters:
Parameters
- Accept-Language: Language prefered in the response. Note: nb and nn will return the same as no header string
- Authorization: Basic auth. The session_id should be sent as both username and password header string
I use the following code to authorize myself, but the last "GET requests" gives an error:
require 'openssl'
require 'base64'
require 'uri'
require 'net/http'
require 'json'
username = 'MYUSERNAME'
password = 'MYPASSWORD'
service = 'NEXTAPI'
# Create auth
string = Base64.encode64(username) + ':' + Base64.encode64(password) + ':' + Base64.encode64((Time.now.\
to_i * 1000).to_s)
public_key_data = File.read(service + '_TEST_public.pem')
public_key = OpenSSL::PKey::RSA.new(public_key_data)
auth = URI::escape(Base64.encode64(public_key.public_encrypt(string)),
Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
# Setup HTTPS
http = Net::HTTP.new('api.test.nordnet.se', 443)
http.use_ssl = true
# Get status of server
response = http.get('/next/2/', {'Accept' => 'application/json'})
puts response.body
# POST login
response = http.post('/next/2/login', "auth=#{auth}&service=#{service}", {'Accept' => 'application/json'})
puts response.body
data = JSON.parse(response.body)
session_key = data['session_key']
auth_string = "Basic " + session_key + ":" + session_key
response = http.get('/next/2/lists', {'Authorization' => auth_string })
puts response
Not sure what is going wrong here. Or what I need to do. I get the following error.
#<Net::HTTPNotAcceptable:0x007fac74276d20>
Question 1: How do I properly send my session key as both username and password?
Question 2: How would I actually send parameters and headers, and what are the differences?
Question 3: Is there a difference in what will be needed in regards to headers/parameters depending on if I send a GET or POST request?
Thanks
Was able to solve it. Below is the OK code...
require 'openssl'
require 'base64'
require 'uri'
require 'net/https'
require 'json'
require 'cgi'
username = 'USERNAME'
password = 'PASSWORD'
service = 'NEXTAPI'
def http_get(path,params)
http = Net::HTTP.new('api.test.nordnet.se', 443)
http.use_ssl = true
#return http.get("#{path}?", params)
return http.get("#{path}?")
#return Net::HTTP.get(path)
end
# Create auth
string = Base64.encode64(username) + ':' + Base64.encode64(password) + ':' + Base64.encode64((Time.now.\
to_i * 1000).to_s)
public_key_data = File.read(service + '_TEST_public.pem')
public_key = OpenSSL::PKey::RSA.new(public_key_data)
auth = URI::escape(Base64.encode64(public_key.public_encrypt(string)),
Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
# Setup HTTPS
http = Net::HTTP.new('api.test.nordnet.se', 443)
http.use_ssl = true
# Get status of server
response = http.get('/next/2/', {'Accept' => 'application/json'})
puts response.body
# POST login
response = http.post('/next/2/login', "auth=#{auth}&service=#{service}", {'Accept' => 'application/json'})
puts response.body
data = JSON.parse(response.body)
session_key = data['session_key']
uri = URI('https://api.test.nordnet.se:443/next/2/lists')
Net::HTTP.start(uri.host, uri.port,
:use_ssl => uri.scheme == 'https',
:verify_mode => OpenSSL::SSL::VERIFY_NONE) do |http|
request = Net::HTTP::Get.new uri.request_uri
request.add_field('Accept', 'application/json')
request.basic_auth session_key, session_key
response = http.request request # Net::HTTPResponse object
puts response.body# puts response.body
end

How to POST to exchange a 23andme auth code for a token

The 23andme web site has an API and they give the following instructions:
Send a POST /token/ request with these parameters (client_id and client_secret are on your dashboard):
curl https://api.23andme.com/token/
-d client_id=xxx \
-d client_secret=yyy \
-d grant_type=authorization_code \
-d code=zzz \
-d "redirect_uri=https://localhost:5000/receive_code/"
-d "scope=basic%20rs3094315"
If successful, you'll get a 200 JSON response like this:
{"access_token":"89822b93d2",
"token_type":"bearer",
"expires_in": 86400,
"refresh_token":"33c53cd7bb",
"scope":"rs3094315 basic"}
So, here's what I tried, but it didn't work. I know Ruby, but have never used net/http or uri.
def self.get_token(code)
uri = URI.parse("https://api.23andme.com/token")
https = Net::HTTP.new(uri.host, uri.port)
https.use_ssl = true
request = Net::HTTP::Post.new(uri.path)
request["client_id"] = 'my client id goes here'
request["client_secret"] = 'my client secret goes here'
request["grant_type"] = 'authorization_code'
request["code"] = code
request["redirect_uri"] = 'http://localhost:3000'
request["scope"] = 'names basic haplogroups relatives'
response = https.request(request)
return response.to_s
end
You are doing it right using curl with the -d option to set the parameter in the body of the POST request. However, with the Net::HTTP::Post object, the syntax request["key"] = value is used to set the headers of the Http object.
Use set_form_data to set all the parameters into the body of the POST request.
For example, use it in this way:
request.set_form_data({"client_id" => "my client id goes here", "code" => code})
The following will clarify the above:
$ > request["client_id"] = 'my client id goes here'
# => "my client id goes here"
$ > request.body
# => nil # body is nil!!!
$ > request.each_header { |e| p e }
# "client_id"
# => {"client_id"=>["my client id goes here"]}
$ > request.set_form_data("client_secret" => 'my client secret goes here')
# => "application/x-www-form-urlencoded"
$ > request.body
# => "client_secret=my+client+secret+goes+here" # body contains the added param!!!
I just wanted to post the code that worked. Thanks to Rafa.
def self.get_token(code)
uri = URI.parse("https://api.23andme.com/token")
https = Net::HTTP.new(uri.host, uri.port)
https.use_ssl = true
https.verify_mode = OpenSSL::SSL::VERIFY_NONE
request = Net::HTTP::Post.new(uri.path,
initheader = {'Content-Type' =>'application/json'})
request.set_form_data({"client_id" => 'my client id',
"client_secret" => 'my client secret',
"grant_type" => 'authorization_code',
"code" => code,
"redirect_uri" => 'http://localhost:3000/receive_code/',
"scope" => 'names basic haplogroups relatives'})
response = https.request(request)
data = JSON.load response.read_body
return data["access_token"]
end

Bitstamp API signature in Ruby

I'm trying to access balance of Bitstamp account with API.
secret = "secret"
key = "key"
nonce = (1000*Time.now.to_f).to_i.to_s
client_id = "123123"
message = nonce + client_id + key
signature = HMAC::SHA256.hexdigest(secret, message).upcase
puts open("https://www.bitstamp.net/api/balance/?nonce=#{nonce}&key=#{key}&signature=#{signature}").read
it clearly generates all required attributes
https://www.bitstamp.net/api/balance/?nonce=1392137355403&key=key&signature=955A3FFC6FEBE69385B9503307873DBCD21E9B7B8EDE67817FFF70961189CE50
yet the error says attributes are missing, why?
{"error": "Missing key, signature and nonce parameters"}
The problem was that I was sending the request as GET instead of POST. Here is the full code I am using now that works.
require 'open-uri'
require 'json'
require 'base64'
require 'openssl'
require 'hmac-sha2'
require 'net/http'
require 'net/https'
require 'uri'
def bitstamp_private_request(method, attrs = {})
secret = "xxx"
key = "xxx"
client_id = "xxx"
nonce = nonce_generator
message = nonce + client_id + key
signature = HMAC::SHA256.hexdigest(secret, message).upcase
url = URI.parse("https://www.bitstamp.net/api/#{method}/")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
data = {
nonce: nonce,
key: key,
signature: signature
}
data.merge!(attrs)
data = data.map { |k,v| "#{k}=#{v}"}.join('&')
headers = {
'Content-Type' => 'application/x-www-form-urlencoded'
}
resp = http.post(url.path, data, headers)
console_log "https://www.bitstamp.net/api/#{method}/"
resp.body
end
def nonce_generator
(Time.now.to_f*1000).to_i.to_s
end

Resources