Initial commit

This commit is contained in:
Yaser
2026-08-13 19:50:53 +03:30
commit 38084458fe
879 changed files with 95198 additions and 0 deletions

View File

@@ -0,0 +1,463 @@
describe Bitcoin::Blockchain do
context :features do
it 'defaults' do
blockchain1 = Bitcoin::Blockchain.new
expect(blockchain1.features).to eq Bitcoin::Blockchain::DEFAULT_FEATURES
end
it 'override defaults' do
blockchain2 = Bitcoin::Blockchain.new(cash_addr_format: true)
expect(blockchain2.features[:cash_addr_format]).to be_truthy
end
it 'custom feautures' do
blockchain3 = Bitcoin::Blockchain.new(custom_feature: :custom)
expect(blockchain3.features.keys).to contain_exactly(*Bitcoin::Blockchain::SUPPORTED_FEATURES)
end
end
context :configure do
let(:blockchain) { Bitcoin::Blockchain.new }
it 'default settings' do
expect(blockchain.settings).to eq({})
end
it 'currencies and server configuration' do
currencies = Currency.where(type: :coin).first(2).map(&:to_blockchain_api_settings)
settings = { server: 'http://user:password@127.0.0.1:18332',
currencies: currencies,
something: :custom }
blockchain.configure(settings)
expect(blockchain.settings).to eq(settings.slice(*Peatio::Blockchain::Abstract::SUPPORTED_SETTINGS))
end
end
context :transaction_sources do
let(:server) { 'http://user:password@127.0.0.1:18332' }
let(:endpoint) { '127.0.0.1:18332' }
let(:blockchain) do
Bitcoin::Blockchain.new.tap {|b| b.configure(server: server)}
end
def request_raw_transaction(transaction)
{
jsonrpc: '1.0',
method: :getrawtransaction,
params: [transaction.hash, 1]
}.to_json
end
context 'transaction 3 vins' do
let(:transaction_file_name) { 'b3aa85765aa52cf7eb0a7f0eb7ac3447b9b1a82b9323bdd8d73fc305073a3711.json' }
let(:transaction_data) do
Rails.root.join('spec', 'resources', 'bitcoin-data', transaction_file_name)
.yield_self { |file_path| File.open(file_path) }
.yield_self { |file| JSON.load(file) }
end
let(:vin1_transaction_file_name) { 'vin-1ec4bf89b77f0d72ee41f41c97a6d380bd69e0221bf182b993b64bb37d017b57.json' }
let(:vin1_transaction_data) do
Rails.root.join('spec', 'resources', 'bitcoin-data', vin1_transaction_file_name)
.yield_self { |file_path| File.open(file_path) }
.yield_self { |file| JSON.load(file) }
end
let(:vin2_transaction_file_name) { 'vin-8bde1da3d2315e09f910cb9782018fe243740c56cd7ca78a19016b169e74180a.json' }
let(:vin2_transaction_data) do
Rails.root.join('spec', 'resources', 'bitcoin-data', vin2_transaction_file_name)
.yield_self { |file_path| File.open(file_path) }
.yield_self { |file| JSON.load(file) }
end
let(:vin3_transaction_file_name) { 'vin-64a04cad438b64e260bc0b832bde79913d618d3206e834de66bbcb1304629d61.json' }
let(:vin3_transaction_data) do
Rails.root.join('spec', 'resources', 'bitcoin-data', vin3_transaction_file_name)
.yield_self { |file_path| File.open(file_path) }
.yield_self { |file| JSON.load(file) }
end
let(:transaction) { Peatio::Transaction.new(hash: 'b3aa85765aa52cf7eb0a7f0eb7ac3447b9b1a82b9323bdd8d73fc305073a3711') }
let(:vin1_transaction) { Peatio::Transaction.new(hash: '1ec4bf89b77f0d72ee41f41c97a6d380bd69e0221bf182b993b64bb37d017b57') }
let(:vin2_transaction) { Peatio::Transaction.new(hash: '8bde1da3d2315e09f910cb9782018fe243740c56cd7ca78a19016b169e74180a') }
let(:vin3_transaction) { Peatio::Transaction.new(hash: '64a04cad438b64e260bc0b832bde79913d618d3206e834de66bbcb1304629d61') }
before do
stub_request(:post, endpoint)
.with(body: request_raw_transaction(transaction))
.to_return(body: transaction_data.to_json)
stub_request(:post, endpoint)
.with(body: request_raw_transaction(vin1_transaction))
.to_return(body: vin1_transaction_data.to_json)
stub_request(:post, endpoint)
.with(body: request_raw_transaction(vin2_transaction))
.to_return(body: vin2_transaction_data.to_json)
stub_request(:post, endpoint)
.with(body: request_raw_transaction(vin3_transaction))
.to_return(body: vin3_transaction_data.to_json)
end
it do
addresses = blockchain.transaction_sources(transaction)
expect(addresses).to eq(['1GsV7tXxXhfdp7kEFoFkgQLUWyv2xgPjnQ'])
end
end
context 'miner transaction' do
let(:transaction) { Peatio::Transaction.new(hash: '1da4b5a135cc0fee9d3aeb5428a898fd12fe5b1b777fd291dbbdbb72c2da8759') }
let(:transaction_file_name) { 'miner_transaction.json' }
let(:transaction_data) do
Rails.root.join('spec', 'resources', 'bitcoin-data', transaction_file_name)
.yield_self { |file_path| File.open(file_path) }
.yield_self { |file| JSON.load(file) }
end
before do
stub_request(:post, endpoint)
.with(body: request_raw_transaction(transaction))
.to_return(body: transaction_data.to_json)
end
it do
addresses = blockchain.transaction_sources(transaction)
expect(addresses).to eq([])
end
end
end
context :latest_block_number do
around do |example|
WebMock.disable_net_connect!
example.run
WebMock.allow_net_connect!
end
let(:server) { 'http://user:password@127.0.0.1:18332' }
let(:endpoint) { '127.0.0.1:18332' }
let(:blockchain) do
Bitcoin::Blockchain.new.tap {|b| b.configure(server: server)}
end
it 'returns latest block number' do
block_number = 1489174
stub_request(:post, endpoint)
.with(body: { jsonrpc: '1.0',
method: :getblockcount,
params: [] }.to_json)
.to_return(body: { result: block_number,
error: nil,
id: nil }.to_json)
expect(blockchain.latest_block_number).to eq(block_number)
end
it 'raises error if there is error in response body' do
stub_request(:post, endpoint)
.with(body: { jsonrpc: '1.0',
method: :getblockcount,
params: [] }.to_json)
.to_return(body: { result: nil,
error: { code: -32601, message: 'Method not found' },
id: nil }.to_json)
expect{ blockchain.latest_block_number }.to raise_error(Peatio::Blockchain::ClientError)
end
it 'keeps alive' do
stub_request(:post, endpoint)
.to_return(body: { result: 1489174,
error: nil,
id: nil }.to_json)
.with(headers: { 'Connection': 'keep-alive',
'Keep-Alive': '30' })
blockchain.latest_block_number
end
end
context :fetch_block! do
around do |example|
WebMock.disable_net_connect!
example.run
WebMock.allow_net_connect!
end
let(:block_file_name) { '1354419-1354420.json' }
let(:block_data) do
Rails.root.join('spec', 'resources', 'bitcoin-data', block_file_name)
.yield_self { |file_path| File.open(file_path) }
.yield_self { |file| JSON.load(file) }
end
let(:start_block) { block_data.first['result']['height'] }
let(:latest_block) { block_data.last['result']['height'] }
def request_block_hash_body(block_height)
{ jsonrpc: '1.0',
method: :getblockhash,
params: [block_height]
}.to_json
end
def request_block_body(block_hash)
{ jsonrpc: '1.0',
method: :getblock,
params: [block_hash, 2]
}.to_json
end
before do
block_data.each do |blk|
# stub get_block_hash
stub_request(:post, endpoint)
.with(body: request_block_hash_body(blk['result']['height']))
.to_return(body: {result: blk['result']['hash']}.to_json)
# stub get_block
stub_request(:post, endpoint)
.with(body: request_block_body(blk['result']['hash']))
.to_return(body: blk.to_json)
end
end
let(:currency) do
Currency.find_by(id: :btc)
end
let(:server) { 'http://user:password@127.0.0.1:18332' }
let(:endpoint) { 'http://127.0.0.1:18332' }
let(:blockchain) do
Bitcoin::Blockchain.new.tap { |b| b.configure(server: server, currencies: [currency]) }
end
context 'first block' do
subject { blockchain.fetch_block!(start_block) }
it 'builds expected number of transactions' do
expect(subject.count).to eq(14)
end
it 'all transactions are valid' do
expect(subject.all?(&:valid?)).to be_truthy
end
end
context 'last block' do
subject { blockchain.fetch_block!(latest_block) }
it 'builds expected number of transactions' do
expect(subject.count).to eq(20)
end
it 'all transactions are valid' do
expect(subject.all?(&:valid?)).to be_truthy
end
end
end
context :build_transaction do
let(:tx_file_name) { '1858591d8ce638c37d5fcd92b9b33ee96be1b950e593cf0cbf45e6bfb1ad8a22.json' }
let(:tx_hash) do
Rails.root.join('spec', 'resources', 'bitcoin-data', tx_file_name)
.yield_self { |file_path| File.open(file_path) }
.yield_self { |file| JSON.load(file) }
end
let(:expected_transactions) do
[{:hash=>"1858591d8ce638c37d5fcd92b9b33ee96be1b950e593cf0cbf45e6bfb1ad8a22",
:txout=>0,
:to_address=>"mg4KVGerD3rYricWC8CoBaayDp1YCKMfvL",
:amount=>0.325e0,
:status=>"success",
:currency_id=>currency.id},
{:hash=>"1858591d8ce638c37d5fcd92b9b33ee96be1b950e593cf0cbf45e6bfb1ad8a22",
:txout=>1,
:to_address=>"mqaBwWDjJCE2Egsf6pfysgD5ZBrfsP7NkA",
:amount=>0.1964466932e2,
:status=>"success",
:currency_id=>currency.id}]
end
let(:currency) do
Currency.find_by(id: :btc)
end
let(:blockchain) do
Bitcoin::Blockchain.new.tap { |b| b.configure(currencies: [currency.to_blockchain_api_settings]) }
end
it 'builds formatted transactions for passed transaction' do
expect(blockchain.send(:build_transaction, tx_hash)).to contain_exactly(*expected_transactions)
end
context 'multiple currencies' do
let(:expected_transactions) do
[{:hash=>"1858591d8ce638c37d5fcd92b9b33ee96be1b950e593cf0cbf45e6bfb1ad8a22",
:txout=>0,
:to_address=>"mg4KVGerD3rYricWC8CoBaayDp1YCKMfvL",
:amount=>0.325e0,
:status=>"success",
:currency_id=>currency1.id},
{:hash=>"1858591d8ce638c37d5fcd92b9b33ee96be1b950e593cf0cbf45e6bfb1ad8a22",
:txout=>1,
:to_address=>"mqaBwWDjJCE2Egsf6pfysgD5ZBrfsP7NkA",
:amount=>0.1964466932e2,
:status=>"success",
:currency_id=>currency1.id},
{:hash=>"1858591d8ce638c37d5fcd92b9b33ee96be1b950e593cf0cbf45e6bfb1ad8a22",
:txout=>0,
:to_address=>"mg4KVGerD3rYricWC8CoBaayDp1YCKMfvL",
:amount=>0.325e0,
:status=>"success",
:currency_id=>currency2.id},
{:hash=>"1858591d8ce638c37d5fcd92b9b33ee96be1b950e593cf0cbf45e6bfb1ad8a22",
:txout=>1,
:to_address=>"mqaBwWDjJCE2Egsf6pfysgD5ZBrfsP7NkA",
:amount=>0.1964466932e2,
:status=>"success",
:currency_id=>currency2.id}]
end
let(:currency1) do
Currency.find_by(id: :btc)
end
let(:currency2) do
Currency.find_by(id: :btc)
end
let(:blockchain) do
Bitcoin::Blockchain.new.tap do |b|
b.configure(currencies: [currency1.to_blockchain_api_settings, currency2.to_blockchain_api_settings])
end
end
it 'builds formatted transactions for passed transaction per each currency' do
expect(blockchain.send(:build_transaction, tx_hash)).to contain_exactly(*expected_transactions)
end
end
context 'three vout transaction' do
let(:tx_file_name) { '1da5cd163a9aaf830093115ac3ac44355e0bcd15afb59af78f84ad4084973ad0.json' }
let(:expected_transactions) do
[{:hash=>"1da5cd163a9aaf830093115ac3ac44355e0bcd15afb59af78f84ad4084973ad0",
:txout=>0,
:to_address=>"2N5WyM3QT1Kb6fvkSZj3Xvcx2at7Ydm5VmL",
:amount=>0.1e0,
:status=>"success",
:currency_id=>"btc"},
{:hash=>"1da5cd163a9aaf830093115ac3ac44355e0bcd15afb59af78f84ad4084973ad0",
:txout=>1,
:to_address=>"2MzDFuDK9ZEEiRsuCDFkPdeHQLGvwbC9ufG",
:amount=>0.2e0,
:status=>"success",
:currency_id=>"btc"},
{:hash=>"1da5cd163a9aaf830093115ac3ac44355e0bcd15afb59af78f84ad4084973ad0",
:txout=>2,
:to_address=>"2MuvCKKi1MzGtvZqvcbqn5twjA2v5XLaTWe",
:amount=>0.11749604e0,
:status=>"success",
:currency_id=>"btc"}]
end
it 'builds formatted transactions for each vout' do
expect(blockchain.send(:build_transaction, tx_hash)).to contain_exactly(*expected_transactions)
end
end
end
context :load_balance_of_address! do
around do |example|
WebMock.disable_net_connect!
example.run
WebMock.allow_net_connect!
end
let(:server) { 'http://user:password@127.0.0.1:18332' }
let(:blockchain) do
Bitcoin::Blockchain.new.tap {|b| b.configure(server: server)}
end
let(:response) do
[
[
[
"mh2e7YHio7fTjLXHZ3KRXDfU52RbwQbhtK",
0,
""
]
],
[
[
"mkuYucVhRQDiSHszbZ9M7d7vygKymyZ549",
0.14458097,
"my_imported_address_from_electrum"
],
[
"mpD544rTPbRNDr9yzK9MTGS4ckfVxUNY42",
0.24997197,
"imported_address_from_bitgo"
]
]
]
end
before do
stub_request(:post, 'http://127.0.0.1:18332')
.with(body: { jsonrpc: '1.0',
method: :listaddressgroupings,
params: [] }.to_json)
.to_return(body: { result: response,
error: nil,
id: nil }.to_json)
end
context 'address with balance is defined' do
it 'requests rpc listaddressgroupings and finds address balance' do
address = 'mpD544rTPbRNDr9yzK9MTGS4ckfVxUNY42'
result = blockchain.load_balance_of_address!(address, :btc)
expect(result).to be_a(BigDecimal)
expect(result).to eq('0.24997197'.to_d)
end
it 'requests rpc listaddressgroupings and finds address with zero balance' do
address = 'mh2e7YHio7fTjLXHZ3KRXDfU52RbwQbhtK'
result = blockchain.load_balance_of_address!(address, :btc)
expect(result).to be_a(BigDecimal)
expect(result).to eq('0'.to_d)
end
end
context 'address is not defined' do
it 'requests rpc listaddressgroupings and do not find address' do
address = '1PoxQx6Pk5NwWN1yyBx2jifFvS9eJAksdf'
expect{ blockchain.load_balance_of_address!(address, :btc)}.to raise_error(Peatio::Blockchain::UnavailableAddressBalanceError)
end
end
context 'client error is raised' do
before do
stub_request(:post, 'http://127.0.0.1:18332')
.with(body: { jsonrpc: '1.0',
method: :listaddressgroupings,
params: [] }.to_json)
.to_return(body: { result: nil,
error: { code: -32601, message: 'Method not found' },
id: nil }.to_json)
end
it 'raise wrapped client error' do
expect{ blockchain.load_balance_of_address!('anything', :btc)}.to raise_error(Peatio::Blockchain::ClientError)
end
end
end
end