blockstream-satellite-api/main.rb

269 lines
8.2 KiB
Ruby

require 'sinatra'
require "sinatra/activerecord"
require 'sinatra/param'
require "faraday"
require 'securerandom'
require 'openssl'
require 'time'
require 'tempfile'
require_relative 'constants'
require_relative 'error_handlers'
require_relative './models/init'
require_relative 'helpers/init'
configure do
set :raise_errors, false
set :show_exceptions, :after_handler
$lightning_charge = Faraday.new(:url => CHARGE_ROOT)
end
before do
content_type :json
end
configure :test, :development do
get '/order/:uuid/sent_message' do
(order = Order.find_by(uuid: params[:uuid], status: [:sent, :transmitting])) || uuid_not_found_error
send_file order.message_path, :disposition => 'attachment'
end
end
# GET /info
#
# returns:
# information about the c-lightning node where satellite API payments are terminated
#
get '/info' do
# call lightning-charge info, which invokes lightning-cli getinfo
response = $lightning_charge.get '/info'
response.body
end
get '/queue.html' do
content_type "text/html;charset=utf-8"
erb :queue
end
# GET /orders/queued
# params:
# limit - return top limit orders (optional)
# returns:
# array of JSON orders sorted by bid-per-byte descending
get '/orders/queued' do
param :limit, Integer, default: PAGE_SIZE, max: MAX_PAGE_SIZE, message: "can't display more than top #{MAX_PAGE_SIZE} orders"
Order.where(status: [:paid, :transmitting])
.select(Order::PUBLIC_FIELDS)
.order(bid_per_byte: :desc)
.limit(params[:limit]).to_json(:only => Order::PUBLIC_FIELDS)
end
# GET /orders/sent
# params:
# before - return the previous PAGE_SIZE orders sent before the given time (time should be sent as in ISO 8601 format and defaults to now)
# returns:
# JSON array of sent and received orders sorted in reverse chronological order
get '/orders/sent' do
param :before, String, required: false, default: lambda { Time.now.utc.iso8601 }
begin
before = DateTime.iso8601(params[:before])
rescue
invalid_date_error
end
Order.where(status: [:sent, :received]).where("created_at < ?", before)
.select(Order::PUBLIC_FIELDS)
.order(ended_transmission_at: :desc)
.limit(PAGE_SIZE).to_json(:only => Order::PUBLIC_FIELDS)
end
# GET /orders/pending
# params:
# before - return the previous PAGE_SIZE orders sent before the given time (time should be sent as in ISO 8601 format and defaults to now)
# returns:
# array of JSON orders sorted in reverse chronological order
get '/orders/pending' do
param :before, String, required: false, default: lambda { Time.now.utc.iso8601 }
begin
before = DateTime.iso8601(params[:before])
rescue
invalid_date_error
end
Order.where(status: :pending).where("created_at < ?", before)
.select(Order::PUBLIC_FIELDS)
.order(created_at: :desc)
.limit(PAGE_SIZE).to_json(:only => Order::PUBLIC_FIELDS)
end
get '/message/:tx_seq_num' do
(order = Order.find_by(tx_seq_num: params[:tx_seq_num], status: [:sent, :transmitting, :received])) || sequence_number_not_found_error
send_file order.message_path, :disposition => 'attachment'
end
# POST /order/tx/:tx_seq_num
#
# acknowledge that uplink transmission has begun for the given sequence number
# params:
# regions - JSON array of coverage region numbers for which transmission has begun
#
# NB: this endpoint must be protected from access from all hosts other than the uplink transmitters
post '/order/tx/:tx_seq_num' do
param :tx_seq_num, Integer, required: true
param :regions, String, required: true
order = fetch_order_by_tx_seq_num
JSON.parse(params[:regions]).each do |region_number|
(region = Region.find_by_number(region_number)) || region_not_found_error(region_number)
order.tx_confirmations.create(region: region)
end
{:message => "transmission confirmed for regions #{params[:regions]}"}.to_json
end
# POST /order/rx/:tx_seq_num
#
# acknowledge receipt for the given sequence number
# params:
# region - coverage region number
#
# NB: this endpoint must be protected from access from all hosts other than the uplink transmitters
post '/order/rx/:tx_seq_num' do
param :tx_seq_num, Integer, required: true
param :region, Integer, required: true
order = fetch_order_by_tx_seq_num
(region = Region.find_by_number(params[:region])) || region_not_found_error(params[:region])
order.rx_confirmations.create(region: region)
{:message => "reception confirmed for region #{params[:region]}"}.to_json
end
# POST /order
#
# upload a message, along with a bid (in millisatoshis)
# return JSON object with status, uuid, and lightning payment invoice
post '/order' do
param :bid, Integer, required: true, min: 0, message: "must be a positive integer number of msatoshis"
param :file, Hash, required: false
param :message, String, required: false, max_length: 1024
bid = Integer(params[:bid])
if params[:message]
tmpfile = Tempfile.new('message_param')
tmpfile.write(params[:message])
tmpfile.close
elsif params[:file]
unless tmpfile = params[:file][:tempfile]
message_file_missing_error
end
else
message_missing_error
end
order = Order.new(uuid: SecureRandom.uuid)
message_file = File.new(order.message_path, "wb")
message_size = 0
sha256 = OpenSSL::Digest::SHA256.new
tmpfile.open
while block = tmpfile.read(65536)
message_size += block.size
if message_size > MAX_MESSAGE_SIZE
message_file_too_large_error
end
sha256 << block
message_file.write(block)
end
message_file.close()
if message_size < MIN_MESSAGE_SIZE
FileUtils.rm(message_file)
message_file_too_small_error
end
order.message_size = message_size
order.message_digest = sha256.to_s
if bid.to_f / order.message_size_with_overhead < MIN_PER_BYTE_BID
bid_too_small_error(order.message_size_with_overhead * MIN_PER_BYTE_BID)
end
invoice = new_invoice(order, bid)
order.invoices << invoice
order.save
{:auth_token => order.user_auth_token, :uuid => order.uuid, :lightning_invoice => JSON.parse(invoice.invoice)}.to_json
end
post '/order/:uuid/bump' do
param :uuid, String, required: true
param :bid_increase, Integer, required: true, min: 0, message: "must be a positive integer number of msatoshis"
param :auth_token, String, required: true, default: lambda { env['HTTP_X_AUTH_TOKEN'] },
message: "auth_token must be provided either in the DELETE body or in an X-Auth-Token header"
bid_increase = Integer(params[:bid_increase])
order = get_and_authenticate_order
unless order.bump
order_bump_error(order)
end
invoice = new_invoice(order, bid_increase)
order.invoices << invoice
order.save
{:auth_token => order.user_auth_token, :uuid => order.uuid, :lightning_invoice => JSON.parse(invoice.invoice)}.to_json
end
get '/order/:uuid' do
param :uuid, String, required: true
param :auth_token, String, required: true, default: lambda { env['HTTP_X_AUTH_TOKEN'] },
message: "auth_token must be provided either in the DELETE body or in an X-Auth-Token header"
get_and_authenticate_order.as_sanitized_json
end
delete '/order/:uuid' do
param :uuid, String, required: true
param :auth_token, String, required: true, default: lambda { env['HTTP_X_AUTH_TOKEN'] },
message: "auth_token must be provided either in the DELETE body or in an X-Auth-Token header"
order = get_and_authenticate_order
unless order.cancel!
order_cancellation_error(order)
end
{:message => "order cancelled"}.to_json
end
# invoice paid callback from charged
post '/callback/:lid/:charged_auth_token' do
param :lid, String, required: true
param :charged_auth_token, String, required: true
invoice = get_and_authenticate_invoice
if invoice.nil?
invoice_not_found_error
end
unless invoice.order
orphaned_invoice_error
end
if invoice.paid?
order_already_paid_error
end
invoice.pay!
{:message => "invoice #{invoice.lid} paid"}.to_json
end
# subscribe to one or more SSE channels
# params:
# channels - comma-separated list of channels to subscribe to
# returns:
# SSE event stream
# available channels:
# transmissions - an event is pushed to this channel when each message transmission begins and ends
get '/subscribe/:channels' do
param :channels, String, is: 'transmissions'
redirect "http://#{request.host}:4500/stream?channels=#{params[:channels]}"
end