Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ruby 3 / Rails 7 compatiblity #32

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Empty file added .env
Empty file.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ spec/dummy/log/*
*.gem
/Gemfile.lock
/spec/dummy/log/development.log
tmp/
.idea
15 changes: 15 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
FROM ruby:3.2.2-bullseye

RUN apt update -qq && apt install -y libpq-dev dh-autoreconf

RUN gem update --system && gem install foreman

WORKDIR /gem
ADD . /gem
RUN bundle install
VOLUME .:/gem/

ARG DEFAULT_PORT 3001
EXPOSE 3001

#CMD ["bin/dev"]
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,18 @@ bundle exec rake cryptocoin_payable:process_prices (see below)
rails d cryptocoin_payable:install
```

## Build Docker
```
docker-compose up --build
```

## Run Tests


```sh
cucumber features
rspec
rubocop
docker-compose run web bin/cucumber features
docker-compose run web rspec
docker-compose run web rubocop
```

## Usage
Expand All @@ -79,6 +85,9 @@ CryptocoinPayable.configure do |config|
config.request_delay = 0.5
config.expire_payments_after = 15.minutes

# defaults to STDOUT
# config.logger = Rails.logger

config.configure_btc do |btc_config|
# btc_config.confirmations = 3
# btc_config.node_path = ''
Expand Down
11 changes: 11 additions & 0 deletions bin/cucumber
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

vendored_cucumber_bin = Dir["#{File.dirname(__FILE__)}/../vendor/{gems,plugins}/cucumber*/bin/cucumber"].first
if vendored_cucumber_bin
load File.expand_path(vendored_cucumber_bin)
else
require 'rubygems' unless ENV['NO_RUBYGEMS']
require 'cucumber'
load Cucumber::BINARY
end
10 changes: 5 additions & 5 deletions cryptocoin_payable.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ Gem::Specification.new do |spec|
spec.required_rubygems_version = '>= 1.3.6'
spec.required_ruby_version = '>= 2.2.0'

spec.add_development_dependency 'bundler', '~> 1.15'
spec.add_development_dependency 'bundler', '~> 2.4'
spec.add_development_dependency 'cucumber'
spec.add_development_dependency 'cucumber-rails'
spec.add_development_dependency 'database_cleaner', '~> 1.7'
spec.add_development_dependency 'pg', '~> 0.21.0'
spec.add_development_dependency 'pg', '~> 1.5.4'
spec.add_development_dependency 'rails', '>= 4.0.0'
spec.add_development_dependency 'rake', '~> 12.3'
spec.add_development_dependency 'rake', '~> 13'
spec.add_development_dependency 'rspec-benchmark', '~> 0.4'
spec.add_development_dependency 'rspec-rails', '~> 3.7'
spec.add_development_dependency 'rspec-retry', '~> 0.6'
Expand All @@ -35,9 +35,9 @@ Gem::Specification.new do |spec|
spec.add_development_dependency 'vcr', '~> 4.0'
spec.add_development_dependency 'webmock', '~> 3.4'

spec.add_dependency 'activerecord-import', '~> 0.27'
spec.add_dependency 'activerecord-import', '~> 1.5'
spec.add_dependency 'cash-addr', '~> 0.2'
spec.add_dependency 'eth', '0.4.8'
spec.add_dependency 'eth', '0.5.11'
spec.add_dependency 'money-tree', '0.10.0'
spec.add_dependency 'state_machines-activerecord', '~> 0.5'
end
Expand Down
37 changes: 37 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
version: '3.4'
services:
web:
build: .
command: /bin/bash -c "cd spec/dummy; rm -f /tmp/server.pid && bundle exec rails server -b 0.0.0.0 -p 3001 -P /tmp/server.pid"
env_file: .env
ports:
- 3001:3001
depends_on:
- db
- redis
volumes:
- .:/gem
- ruby-bundle-cache:/bundle
tty: true
stdin_open: true

redis:
image: "redis:7-alpine"
ports:
- 6379
volumes:
- ./tmp/redis_data:/var/lib/redis/data

db:
image: postgres
environment:
POSTGRES_HOST_AUTH_METHOD: trust
restart: always
ports:
- 5433:5433
volumes:
- ./tmp/postgres_data:/var/lib/postgresql/data

volumes:
ruby-bundle-cache:
external: true
5 changes: 5 additions & 0 deletions lib/cryptocoin_payable/adapters/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ class Base
# def self.create_address(id)
# end

def logger
CryptocoinPayable.configuration.logger
end

def convert_subunit_to_main(subunit)
subunit / self.class.subunit_in_main.to_f
end
Expand Down Expand Up @@ -76,6 +80,7 @@ def adapter_api_key
@adapter_api_key ||= coin_config && coin_config.adapter_api_key
end


def parse_timestamp(timestamp)
timestamp.nil? ? nil : Time.strptime(timestamp.to_s, '%s')
end
Expand Down
45 changes: 24 additions & 21 deletions lib/cryptocoin_payable/adapters/bitcoin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,53 +11,56 @@ def self.coin_symbol
end

def fetch_transactions(address)
fetch_block_explorer_transactions(address)
rescue StandardError
fetch_blockstream_transactions(address)
rescue StandardError => e
logger.info "Blockstream API failed, falling back to BlockCypher #{e.message}"
fetch_block_cypher_transactions(address)
end

def create_address(id)
super.to_address(network: network)
super.to_address(network:)
end

private

def prefix
CryptocoinPayable.configuration.testnet ? 'testnet.' : ''
def prefix_blockstream
CryptocoinPayable.configuration.testnet ? 'testnet/' : ''
end
def prefix_block_cypher
CryptocoinPayable.configuration.testnet ? 'test3' : ''
end

def network
CryptocoinPayable.configuration.testnet ? :bitcoin_testnet : :bitcoin
end

def parse_total_tx_value_block_explorer(output_transactions, address)
def parse_total_tx_value_blockstream(output_transactions, address)
output_transactions
.reject { |out| out['scriptPubKey']['addresses'].nil? }
.select { |out| out['scriptPubKey']['addresses'].include?(address) }
.map { |out| (out['value'].to_f * self.class.subunit_in_main).to_i }
.select { |out| out['scriptpubkey_address'] == address }
.map { |out| out['value'] }
.inject(:+) || 0
end

def fetch_block_explorer_transactions(address)
url = "https://#{prefix}blockexplorer.com/api/txs/?address=#{address}"
parse_block_explorer_transactions(get_request(url).body, address)
def fetch_blockstream_transactions(address)
url = "https://blockstream.info/#{prefix_blockstream}api/address/#{address}/txs"
parse_blockstream_transactions(get_request(url).body, address)
end

def parse_block_explorer_transactions(response, address)
def parse_blockstream_transactions(response, address)
json = JSON.parse(response)
json['txs'].map { |tx| convert_block_explorer_transactions(tx, address) }
json.map { |tx| convert_blockstream_transactions(tx, address) }
rescue JSON::ParserError
raise ApiError, response
end

def convert_block_explorer_transactions(transaction, address)
def convert_blockstream_transactions(transaction, address)
{
transaction_hash: transaction['txid'],
block_hash: transaction['blockhash'],
block_time: parse_timestamp(transaction['blocktime']),
estimated_time: parse_timestamp(transaction['time']),
estimated_value: parse_total_tx_value_block_explorer(transaction['vout'], address),
confirmations: transaction['confirmations']
block_hash: transaction['status']['block_hash'],
block_time: parse_timestamp(transaction['status']['block_time']),
estimated_time: parse_timestamp(transaction['status']['block_time']),
estimated_value: parse_total_tx_value_blockstream(transaction['vout'], address),
confirmations: 1 #blocktstream only returns true or false for confirmed
}
end

Expand All @@ -68,7 +71,7 @@ def parse_total_tx_value_block_cypher(output_transactions, address)
end

def fetch_block_cypher_transactions(address)
url = "https://api.blockcypher.com/v1/btc/main/addrs/#{address}/full"
url = "https://api.blockcypher.com/v1/btc/#{prefix_block_cypher}/addrs/#{address}/full"
parse_block_cypher_transactions(get_request(url).body, address)
end

Expand Down
2 changes: 1 addition & 1 deletion lib/cryptocoin_payable/adapters/bitcoin_cash.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def fetch_transactions(address)
raise NetworkNotSupported if CryptocoinPayable.configuration.testnet

url = "https://#{prefix}blockexplorer.com/api/txs/?address=#{legacy_address(address)}"
parse_block_explorer_transactions(get_request(url).body, address)
parse_bitcoin_explorer_transactions(get_request(url).body, address)
end

def create_address(id)
Expand Down
11 changes: 7 additions & 4 deletions lib/cryptocoin_payable/adapters/ethereum.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,19 @@ def fetch_transactions(address)
response = get_request(url)
json = JSON.parse(response.body)

raise ApiError, json['message'] if json['status'] == '0' && json['message'] == 'NOTOK'
raise ApiError, json['message'] if json['status'] == '0' && json['message'].include?('NOTOK')

json['result'].map { |tx| convert_transactions(tx, address) }
end

def create_address(id)
Eth::Utils.public_key_to_address(super.public_key.uncompressed.to_hex)
Eth::Util.public_key_to_address(super.public_key.uncompressed.to_hex)
end

private

def subdomain
@subdomain ||= CryptocoinPayable.configuration.testnet ? 'rinkeby' : 'api'
@subdomain ||= CryptocoinPayable.configuration.testnet ? 'api-sepolia' : 'api'
end

# Example response:
Expand Down Expand Up @@ -64,10 +64,13 @@ def subdomain
def convert_transactions(transaction, _address)
{
transaction_hash: transaction['hash'],
block_hash: transaction['block_hash'],
block_hash: transaction['blockHash'],
block_time: nil, # Not supported
estimated_time: parse_timestamp(transaction['timeStamp']),
estimated_value: transaction['value'].to_i, # Units here are 'Wei'
gas: transaction['gas'].to_i,
gas_price: transaction['gasPrice'].to_i,
gas_used: transaction['gasUsed'].to_i,
confirmations: transaction['confirmations'].to_i
}
end
Expand Down
8 changes: 4 additions & 4 deletions lib/cryptocoin_payable/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ def self.configure
end

class Configuration
attr_accessor :testnet, :expire_payments_after, :request_delay, :btc, :bch, :eth
attr_accessor :testnet, :expire_payments_after, :request_delay, :btc, :bch, :eth, :logger
attr_writer :currency

def currency
Expand All @@ -29,10 +29,10 @@ def configure_bch
def configure_eth
@eth ||= EthConfiguration.new
yield(@eth)
end

Eth.configure do |config|
config.chain_id = CryptocoinPayable.configuration.testnet ? 4 : 1
end
def logger
@logger ||= Logger.new(STDOUT)
end

class CoinConfiguration
Expand Down
12 changes: 6 additions & 6 deletions spec/acceptance/adapters/bitcoin_cash_spec.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
require 'active_record'
require 'cryptocoin_payable'

describe CryptocoinPayable::Adapters::BitcoinCash, :vcr do
it 'gets transactions for a given address' do
pending 'not using bitcoin cash at the moment'
response = subject.fetch_transactions('bitcoincash:qzmdvhfdxv7j79hs6080hdm47szxfxqpccemzq6n52')

expect(response).to eq(
Expand All @@ -20,19 +18,21 @@
end

it 'gets an empty result when no transactions found' do
pending 'not using bitcoin cash at the moment'
response = subject.fetch_transactions('bitcoincash:qqu5af4540fw6eg3cqr3t8ndhplpd0xf0vmqpw59ef')
expect(response).to eq([])
end

it 'raises an error when an invalid address is passed' do
pending 'not using bitcoin cash at the moment'
expect { subject.fetch_transactions('foo') }.to raise_error CryptocoinPayable::ApiError
end

it 'creates BIP32 addresses' do
3.times do
expect(subject.create_address(0)).to eq('bitcoincash:qpfspf58t6vcsvq7xeumpuudqhvj38s5sus4uutspg')
expect(subject.create_address(1)).to eq('bitcoincash:qz94rwzlgccnkvaaea5klmtmad32l8gndgrcfwaryc')
expect(subject.create_address(2)).to eq('bitcoincash:qrgpwhl6x5qvf8ratcdl992r5afuv6286ujfa82xrh')
expect(subject.create_address(0)).to eq('bchtest:qpfspf58t6vcsvq7xeumpuudqhvj38s5su58cmf8x5')
expect(subject.create_address(1)).to eq('bchtest:qz94rwzlgccnkvaaea5klmtmad32l8gndg82dfl5ry')
expect(subject.create_address(2)).to eq('bchtest:qrgpwhl6x5qvf8ratcdl992r5afuv6286ukmeqg3yt')
end
end
end