diff --git a/config-schema.dhall b/config-schema.dhall index 7d69f3ad342032288381f20f21797e0b4ed12554..9bbe3bccb26388269794f148b3cb981e274fe3c6 100644 --- a/config-schema.dhall +++ b/config-schema.dhall @@ -41,6 +41,7 @@ , rev_ai_token : Text , server : { host : Text, port : Natural } , sgx : Text +, simpleswap_api_key : Text , sip : { app : Text, realm : Text } , sip_host : Text , snikket_hosting_api : Text diff --git a/config.dhall.sample b/config.dhall.sample index 71251c5dd4bb52576e30d319ed9d3f169dbadd20..d0071a8f3937c4a2c53f1b4f60e77d7091e94c36 100644 --- a/config.dhall.sample +++ b/config.dhall.sample @@ -83,5 +83,6 @@ in rev_ai_token = "", upstream_domain = "example.net", approved_domains = toMap { `example.com` = Some "customer_id" }, - keepgo = Some { api_key = "", access_token = "" } + keepgo = Some { api_key = "", access_token = "" }, + simpleswap_api_key = "" } diff --git a/forms/alt_top_up.rb b/forms/alt_top_up.rb index b7f06028ed68ec30fb683ecf5229e2ccb3106e46..fbeaea163061141f37465970a07d61838e362ac3 100644 --- a/forms/alt_top_up.rb +++ b/forms/alt_top_up.rb @@ -15,15 +15,15 @@ render "alt_top_up/mailing_address" render "alt_top_up/interac" if @currency == :CAD render "alt_top_up/btc_addresses" -add_btc_label = if !@btc_addresses || @btc_addresses.empty? - "You have no Bitcoin addresses, would you like to create one?" -else - "Or, create a new Bitcoin address?" -end - field( - var: "add_btc_address", - label: add_btc_label, - type: "boolean", - value: false + var: "http://jabber.org/protocol/commands#actions", + label: "Action", + type: "list-single", + options: [ + { label: "Done", value: "complete" }, + { label: "Add new Bitcoin address to account", value: "BTC" }, + { label: "Get single-use Monero address", value: "XMR" }, + { label: "Get single-use Ethereum address", value: "ETH" } + ], + value: "complete" ) diff --git a/forms/alt_top_up/btc.rb b/forms/alt_top_up/btc.rb new file mode 100644 index 0000000000000000000000000000000000000000..9841b5e02111a5139822769fc2886ce42190b299 --- /dev/null +++ b/forms/alt_top_up/btc.rb @@ -0,0 +1,5 @@ +result! +title "New Bitcoin Address" +instructions "Your new address has been created" + +render "alt_top_up/btc_addresses" diff --git a/forms/alt_top_up/simpleswap.rb b/forms/alt_top_up/simpleswap.rb new file mode 100644 index 0000000000000000000000000000000000000000..c7faa1507899d345bcd02ac7868e970eb3f6aa9f --- /dev/null +++ b/forms/alt_top_up/simpleswap.rb @@ -0,0 +1,15 @@ +result! +title "Single-use Address" +instructions "Your single-use address has been created" + +DESCRIPTION = + "You can make a payment of any amount to this address and " \ + "it will be credited to your account at the current exchange rate " \ + "once fully confirmed.".freeze + +field( + var: "addresses", + label: "Address", + description: DESCRIPTION, + value: @addresses +) diff --git a/lib/add_bitcoin_address.rb b/lib/add_bitcoin_address.rb deleted file mode 100644 index f18bb40544ce23efe5c45bbd280f2104a5bd0ef0..0000000000000000000000000000000000000000 --- a/lib/add_bitcoin_address.rb +++ /dev/null @@ -1,50 +0,0 @@ -# frozen_string_literal: true - -class AddBitcoinAddress - def self.for(iq, alt_form, customer) - if alt_form.parse(iq.form)[:add_btc_address] - new(iq, customer) - else - DoNot.new(iq) - end - end - - def initialize(iq, customer) - @reply = iq.reply - @reply.status = :completed - @customer = customer - end - - def write - @customer.add_btc_address.then do |addr| - form.fields = [{ - var: "btc_address", - type: "fixed", - label: "Bitcoin Address", - value: addr - }] - BLATHER << @reply - end - end - -protected - - def form - form = @reply.form - form.type = :result - form.title = "New Bitcoin Address" - form.instructions = "Your new address has been created" - form - end - - class DoNot - def initialize(iq) - @reply = iq.reply - @reply.status = :completed - end - - def write - BLATHER << @reply - end - end -end diff --git a/lib/alt_top_up_form.rb b/lib/alt_top_up_form.rb index f39c08ba69eed968155f79ebada6f7bbc4f0c348..88714d280700c67609f79b970d885a8869869fd5 100644 --- a/lib/alt_top_up_form.rb +++ b/lib/alt_top_up_form.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require_relative "simple_swap" + class AltTopUpForm def self.for(customer) customer.btc_addresses.then do |addrs| @@ -8,6 +10,7 @@ class AltTopUpForm end def initialize(customer, btc_addresses) + @customer = customer @balance = customer.balance @currency = customer.currency @btc_addresses = btc_addresses @@ -23,10 +26,58 @@ class AltTopUpForm end def parse(form) - { - add_btc_address: ["1", "true"].include?( - form.field("add_btc_address")&.value.to_s - ) - } + action = + form.field("http://jabber.org/protocol/commands#actions")&.value.to_s + case action + when "BTC" + BitcoinAddress.new(@customer) + when /\A[A-Z]{3}\Z/ + SimpleSwapAddress.new(@customer, action, @btc_addresses.first) + else + NoOp.new + end + end + + class NoOp + def action(*); end + end + + class BitcoinAddress + def initialize(customer) + @customer = customer + end + + def action(reply) + @customer.add_btc_address.then do |addr| + reply.command << FormTemplate.render( + "alt_top_up/btc", + btc_addresses: [addr] + ) + end + end + end + + class SimpleSwapAddress + def initialize(customer, currency, btc_address, simple_swap: SimpleSwap.new) + @customer = customer + @currency = currency.downcase + @btc_address = btc_address + @simple_swap = simple_swap + end + + def btc_address + @btc_address || @customer.add_btc_address + end + + def action(reply) + EMPromise.resolve(btc_address).then { |btc| + @simple_swap.fetch_addr(@currency, btc) + }.then do |addr| + reply.command << FormTemplate.render( + "alt_top_up/simpleswap", + addresses: [addr] + ) + end + end end end diff --git a/lib/command.rb b/lib/command.rb index 0645c659a7044bfcc3b4fbcdac6995678fc473ed..71d35e7c1729b90daa9c16bd092c6c2298055025 100644 --- a/lib/command.rb +++ b/lib/command.rb @@ -76,12 +76,14 @@ class Command def finish(text=nil, type: :info, status: :completed) reply = @iq.reply reply.status = status - yield reply if block_given? - if text - reply.note_type = type - reply.note_text = text + EMPromise.resolve(block_given? ? yield(reply) : nil).then { + if text + reply.note_type = type + reply.note_text = text + end + }.then do + EMPromise.reject(FinalStanza.new(reply)) end - EMPromise.reject(FinalStanza.new(reply)) end def customer diff --git a/lib/customer_finacials.rb b/lib/customer_finacials.rb index 8ff43d40fd7f8f477a8295327fe85a0c0c19117f..ea0df7491ba4c28d3da46cd47b3ab9337e1a6963 100644 --- a/lib/customer_finacials.rb +++ b/lib/customer_finacials.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "bigdecimal" + class CustomerFinancials def initialize(customer_id) @customer_id = customer_id diff --git a/lib/simple_swap.rb b/lib/simple_swap.rb new file mode 100644 index 0000000000000000000000000000000000000000..e26de11c064a8bddd7258f2b4aa2f926af074e8d --- /dev/null +++ b/lib/simple_swap.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require "em-http/middleware/json_response" + +class SimpleSwap + def initialize(api_key: CONFIG[:simpleswap_api_key]) + @api_key = api_key + end + + def fetch_range(currency) + req( + :aget, "get_ranges", + query: { + fixed: "false", + currency_from: currency, + currency_to: "btc" + } + ).then { |req| + { min: req.response["min"]&.to_d || 0, max: req.response["max"]&.to_d } + } + end + + def fetch_rate(currency) + req( + :aget, "get_estimated", + query: { + fixed: "false", + currency_from: currency, + currency_to: "btc", + amount: 1 + } + ).then { |req| req.response.to_d } + end + + def fetch_addr(currency, target) + req( + :apost, "create_exchange", + body: { + fixed: false, + currency_from: currency, + currency_to: "btc", + amount: 10, + address_to: target + }.to_json + ).then(&method(:parse)) + end + + def parse(req) + return req.response["address_from"] if req.response["address_from"] + + raise req.response["message"] + end + + def req(m, path, query: {}, body: nil) + EM::HttpRequest.new( + "https://api.simpleswap.io/#{path}", tls: { verify_peer: true } + ).tap { |req| req.use(EM::Middleware::JSONResponse) }.public_send( + m, + head: { + "Accept" => "application/json", "Content-Type" => "application/json" + }, + query: query.merge(api_key: @api_key), + body: body + ) + end +end diff --git a/sgx_jmp.rb b/sgx_jmp.rb index 7b6a6db32a35fae716013fc6422f5bd5228f1d3b..260849559407295a71c38ede9983f37c160ca007 100644 --- a/sgx_jmp.rb +++ b/sgx_jmp.rb @@ -69,7 +69,6 @@ COMMAND_MANAGER = SessionManager.new( require_relative "lib/polyfill" require_relative "lib/alt_top_up_form" require_relative "lib/admin_command" -require_relative "lib/add_bitcoin_address" require_relative "lib/backend_sgx" require_relative "lib/bwmsgsv2_repo" require_relative "lib/bandwidth_iris_patch" @@ -619,13 +618,13 @@ Command.new( list_for: ->(customer:, **) { !!customer&.currency } ) { Command.customer.then { |customer| - EMPromise.all([AltTopUpForm.for(customer), customer]) - }.then do |(alt_form, customer)| + AltTopUpForm.for(customer) + }.then do |alt_form| Command.reply { |reply| reply.allowed_actions = [:complete] reply.command << alt_form.form }.then do |iq| - AddBitcoinAddress.for(iq, alt_form, customer).write + Command.finish { |reply| alt_form.parse(iq.form).action(reply) } end end }.register(self).then(&CommandList.method(:register)) diff --git a/test/test_add_bitcoin_address.rb b/test/test_add_bitcoin_address.rb deleted file mode 100644 index 8fc07911834918046926d17e4d354eede045dd4d..0000000000000000000000000000000000000000 --- a/test/test_add_bitcoin_address.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -require "test_helper" -require "alt_top_up_form" -require "add_bitcoin_address" - -class AddBitcoinAddressTest < Minitest::Test - def test_for - iq = Blather::Stanza::Iq::Command.new - cust = customer - AddBitcoinAddress.for(iq, AltTopUpForm.new(cust, []), cust) - end - - def test_for_add_bitcoin - iq = Blather::Stanza::Iq::Command.new - iq.form.fields = [{ var: "add_btc_address", value: "true" }] - cust = customer - AddBitcoinAddress.for(iq, AltTopUpForm.new(cust, []), cust) - end - - def test_write - cust = Minitest::Mock.new - cust.expect(:add_btc_address, EMPromise.resolve("newaddress")) - iq = Blather::Stanza::Iq::Command.new - AddBitcoinAddress.new(iq, cust).write.sync - assert_mock cust - end - em :test_write - - class DoNotTest < Minitest::Test - AddBitcoinAddress::DoNot::BLATHER = Minitest::Mock.new - - def test_write - AddBitcoinAddress::DoNot::BLATHER.expect( - :<<, - EMPromise.resolve(nil) - ) do |stanza| - assert_equal :completed, stanza.status - end - iq = Blather::Stanza::Iq::Command.new - AddBitcoinAddress::DoNot.new(iq).write.sync - assert_mock AddBitcoinAddress::DoNot::BLATHER - end - em :test_write - end -end diff --git a/test/test_admin_command.rb b/test/test_admin_command.rb index 506cc3a1b5fe1dc2a1f8ad1f9dc4791de2abf734..16730db91555b238dc15ec0a899b55e239bc695c 100644 --- a/test/test_admin_command.rb +++ b/test/test_admin_command.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require "test_helper" require "admin_command" BackendSgx::IQ_MANAGER = Minitest::Mock.new diff --git a/test/test_alt_top_up_form.rb b/test/test_alt_top_up_form.rb index 2ae9d43a5da86113aaf861864f21d98d3f1d5ea8..4de0024a129ff0aff7cb8597b404daccf042ebb9 100644 --- a/test/test_alt_top_up_form.rb +++ b/test/test_alt_top_up_form.rb @@ -62,32 +62,36 @@ class AltTopUpFormTest < Minitest::Test ) end - def test_parse_true + def test_parse_complete iq_form = Blather::Stanza::X.new iq_form.fields = [ - { var: "add_btc_address", value: "true" } + { var: "http://jabber.org/protocol/commands#actions", value: "complete" } ] - assert AltTopUpForm.new(customer, []).parse(iq_form)[:add_btc_address] + assert_kind_of( + AltTopUpForm::NoOp, + AltTopUpForm.new(customer, []).parse(iq_form) + ) end - def test_parse_1 + def test_parse_btc iq_form = Blather::Stanza::X.new iq_form.fields = [ - { var: "add_btc_address", value: "1" } + { var: "http://jabber.org/protocol/commands#actions", value: "BTC" } ] - assert AltTopUpForm.new(customer, []).parse(iq_form)[:add_btc_address] + assert_kind_of( + AltTopUpForm::BitcoinAddress, + AltTopUpForm.new(customer, []).parse(iq_form) + ) end - def test_parse_false + def test_parse_xmr iq_form = Blather::Stanza::X.new iq_form.fields = [ - { var: "add_btc_address", value: "false" } + { var: "http://jabber.org/protocol/commands#actions", value: "XMR" } ] - refute AltTopUpForm.new(customer, []).parse(iq_form)[:add_btc_address] - end - - def test_parse_not_presend - iq_form = Blather::Stanza::X.new - refute AltTopUpForm.new(customer, []).parse(iq_form)[:add_btc_address] + assert_kind_of( + AltTopUpForm::SimpleSwapAddress, + AltTopUpForm.new(customer, []).parse(iq_form) + ) end end