Skip to content

Commit

Permalink
feat: add bitcoin address mapping (#1679)
Browse files Browse the repository at this point in the history
* feat: add bitcoin address mapping

* refactor: address transactions

* refactor: search pending transactions with bitcoin address

* chore: fix rgb transaction attribute
  • Loading branch information
rabbitz authored Mar 18, 2024
1 parent 3b1c941 commit 993a28b
Show file tree
Hide file tree
Showing 18 changed files with 364 additions and 219 deletions.
77 changes: 6 additions & 71 deletions app/controllers/api/v1/address_pending_transactions_controller.rb
Original file line number Diff line number Diff line change
@@ -1,82 +1,17 @@
module Api
module V1
class AddressPendingTransactionsController < ApplicationController
before_action :validate_query_params
before_action :validate_pagination_params, :pagination_params
before_action :find_address
before_action :validate_pagination_params

def show
expires_in 10.seconds, public: true, must_revalidate: true, stale_while_revalidate: 10.seconds

ckb_transactions = @address.ckb_transactions.tx_pending
ckb_transactions_ids = CellInput.where(ckb_transaction_id: ckb_transactions.ids).
where.not(previous_cell_output_id: nil, from_cell_base: false).
distinct.pluck(:ckb_transaction_id)
@ckb_transactions = CkbTransaction.where(id: ckb_transactions_ids).
order(transactions_ordering).page(@page).per(@page_size)

render json: serialized_ckb_transactions
end

private

def validate_query_params
validator = Validations::Address.new(params)

if validator.invalid?
errors = validator.error_object[:errors]
status = validator.error_object[:status]

render json: errors, status:
end
end

def pagination_params
@page = params[:page] || 1
@page_size = params[:page_size] || CkbTransaction.default_per_page
end

def find_address
@address = Address.find_address!(params[:id])

raise Api::V1::Exceptions::AddressNotFoundError if @address.is_a?(NullAddress)
end

def transactions_ordering
sort, order = params.fetch(:sort, "id.desc").split(".", 2)
sort = case sort
when "time" then "block_timestamp"
else "id"
end

order = order.match?(/^(asc|desc)$/i) ? order : "asc"

"#{sort} #{order} NULLS LAST"
end

def serialized_ckb_transactions
options = FastJsonapi::PaginationMetaGenerator.new(
request:,
records: @ckb_transactions,
page: @page,
page_size: @page_size,
).call
ckb_transaction_serializer = CkbTransactionsSerializer.new(
@ckb_transactions,
options.merge(params: { previews: true, address: @address }),
json = Addresses::PendingTransactions.run!(
{ request:,
key: params[:id], sort: params[:sort],
page: params[:page], page_size: params[:page_size] },
)

if QueryKeyUtils.valid_address?(params[:id])
if @address.address_hash == @address.query_address
ckb_transaction_serializer.serialized_json
else
ckb_transaction_serializer.serialized_json.gsub(
@address.address_hash, @address.query_address
)
end
else
ckb_transaction_serializer.serialized_json
end
render json:
end
end
end
Expand Down
90 changes: 9 additions & 81 deletions app/controllers/api/v1/address_transactions_controller.rb
Original file line number Diff line number Diff line change
@@ -1,101 +1,29 @@
module Api
module V1
class AddressTransactionsController < ApplicationController
before_action :validate_query_params
before_action :validate_pagination_params, :pagination_params
before_action :find_address, only: %i[show download_csv]
before_action :validate_pagination_params

def show
expires_in 10.seconds, public: true, must_revalidate: true, stale_while_revalidate: 5.seconds

order_by, asc_or_desc = account_books_ordering
tx_ids = AccountBook.joins(:ckb_transaction).
where(account_books: { address_id: @address.id },
ckb_transactions: { tx_status: "committed" }).
order(order_by => asc_or_desc).
page(@page).per(@page_size).fast_page

total_count = AccountBook.where(address_id: @address.id).count
total_count = tx_ids.total_count if total_count < 1_000

ckb_transaction_ids = tx_ids.map(&:ckb_transaction_id)
ckb_transactions = CkbTransaction.where(id: ckb_transaction_ids).
select(:id, :tx_hash, :block_id, :block_number, :block_timestamp,
:is_cellbase, :updated_at, :capacity_involved, :created_at).
order(order_by => asc_or_desc)

options = FastJsonapi::PaginationMetaGenerator.new(
request:,
records: ckb_transactions,
page: @page,
page_size: @page_size,
total_count:,
).call
ckb_transaction_serializer = CkbTransactionsSerializer.new(
ckb_transactions,
options.merge(params: { previews: true, address: @address }),
json = Addresses::CkbTransactions.run!(
{ request:,
key: params[:id], sort: params[:sort],
page: params[:page], page_size: params[:page_size] },
)

json =
if QueryKeyUtils.valid_address?(params[:id])
if @address.address_hash == @address.query_address
ckb_transaction_serializer.serialized_json
else
ckb_transaction_serializer.serialized_json.gsub(@address.address_hash, @address.query_address)
end
else
ckb_transaction_serializer.serialized_json
end

render json:
end

def download_csv
address = Addresses::Explore.run!(key: params[:id])
raise Api::V1::Exceptions::AddressNotFoundError if address.is_a?(NullAddress)

args = params.permit(:id, :start_date, :end_date, :start_number, :end_number, address_transaction: {}).
merge(address_id: @address.id)
merge(address_id: address.map(&:id))
file = CsvExportable::ExportAddressTransactionsJob.perform_now(args.to_h)

send_data file, type: "text/csv; charset=utf-8; header=present",
disposition: "attachment;filename=ckb_transactions.csv"
end

private

def validate_query_params
validator = Validations::Address.new(params)

if validator.invalid?
errors = validator.error_object[:errors]
status = validator.error_object[:status]

render json: errors, status:
end
end

def pagination_params
@page = params[:page] || 1
@page_size = params[:page_size] || CkbTransaction.default_per_page
end

def find_address
@address = Address.find_address!(params[:id])
raise Api::V1::Exceptions::AddressNotFoundError if @address.is_a?(NullAddress)
end

def account_books_ordering
sort, order = params.fetch(:sort, "ckb_transaction_id.desc").split(".", 2)
sort =
case sort
when "time" then "ckb_transactions.block_timestamp"
else "ckb_transactions.id"
end

if order.nil? || !order.match?(/^(asc|desc)$/i)
order = "asc"
end

[sort, order]
end
end
end
end
7 changes: 1 addition & 6 deletions app/controllers/api/v1/addresses_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,7 @@ class AddressesController < ApplicationController
before_action :validate_query_params

def show
if BitcoinUtils.valid_address?(params[:id])
bitcoin_address = BitcoinAddress.find_by(address_hash: params[:id])
address = bitcoin_address ? bitcoin_address.ckb_address : NullAddress.new(params[:id])
else
address = Address.find_address!(params[:id])
end
address = Address.find_address!(params[:id])

render json: json_response(address)
end
Expand Down
80 changes: 80 additions & 0 deletions app/interactions/addresses/ckb_transactions.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
module Addresses
class CkbTransactions < ActiveInteraction::Base
include Api::V1::Exceptions

object :request, class: ActionDispatch::Request
string :key, default: nil
string :sort, default: "ckb_transaction_id.desc"
integer :page, default: 1
integer :page_size, default: CkbTransaction.default_per_page

def execute
address = Explore.run!(key:)
raise AddressNotFoundError if address.is_a?(NullAddress)

address_id = address.map(&:id)

order_by, asc_or_desc = account_books_ordering
account_books =
AccountBook.joins(:ckb_transaction).
where(account_books: { address_id: },
ckb_transactions: { tx_status: "committed" }).
order(order_by => asc_or_desc).
page(page).per(page_size).fast_page

ckb_transaction_ids = account_books.map(&:ckb_transaction_id)
records = CkbTransaction.where(id: ckb_transaction_ids).
select(select_fields).
order(order_by => asc_or_desc)

options = paginate_options(records, address_id)
options.merge!(params: { previews: true, address: })

result = CkbTransactionsSerializer.new(records, options)
wrap_result(result, address)
end

private

def account_books_ordering
sort_by, sort_order = sort.split(".", 2)
sort_by =
case sort_by
when "time" then "ckb_transactions.block_timestamp"
else "ckb_transactions.id"
end

if sort_order.nil? || !sort_order.match?(/^(asc|desc)$/i)
sort_order = "asc"
end

[sort_by, sort_order]
end

def paginate_options(records, address_id)
total_count = AccountBook.where(address_id:).count
FastJsonapi::PaginationMetaGenerator.new(
request:, records:, page:, page_size:, total_count:,
).call
end

def select_fields
%i[id tx_hash block_id block_number block_timestamp
is_cellbase updated_at capacity_involved created_at]
end

# A lock script can correspond to multiple CKB addresses
def wrap_result(result, address)
if QueryKeyUtils.valid_address?(key)
ckb_address = address[0]
if ckb_address.address_hash == ckb_address.query_address
result.serialized_json
else
result.serialized_json.gsub(ckb_address.address_hash, ckb_address.query_address)
end
else
result.serialized_json
end
end
end
end
37 changes: 37 additions & 0 deletions app/interactions/addresses/explore.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
module Addresses
class Explore < ActiveInteraction::Base
string :key, default: nil

def execute
result = find_address
result ? wrap_result(result) : NullAddress.new(key)
end

private

def find_address
return Address.find_by(lock_hash: key) if QueryKeyUtils.valid_hex?(key)
return find_by_address_hash(key) if QueryKeyUtils.valid_address?(key)
return find_by_bitcoin_address_hash(key) if BitcoinUtils.valid_address?(key)
end

def find_by_address_hash(key)
lock_hash = CkbUtils.parse_address(key).script.compute_hash
address = Address.find_by(lock_hash:)
address.query_address = key if address
address
rescue StandardError
nil
end

def find_by_bitcoin_address_hash(key)
address_ids = BitcoinAddressMapping.includes(:bitcoin_address).
where(bitcoin_address: { address_hash: key }).pluck(:address_id)
Address.where(id: address_ids)
end

def wrap_result(result)
result.is_a?(Array) ? result : [result]
end
end
end
Loading

0 comments on commit 993a28b

Please sign in to comment.