From c21ac521acc0ecaa0189205e7f3050aabe7dea6d Mon Sep 17 00:00:00 2001 From: icebaker Date: Sun, 2 Apr 2023 19:58:36 -0300 Subject: [PATCH 1/4] adding support for btc payments --- Gemfile.lock | 4 +- adapters/bitcoin/request.rb | 31 ++++ controllers/bitcoin/request.rb | 25 +++ controllers/bitcoin/request/decode.rb | 55 +++++++ controllers/bitcoin/transaction.rb | 4 +- controllers/bitcoin/transaction/all.rb | 15 +- docs/README.md | 18 +++ models/bitcoin/request.rb | 77 +++++++++ models/satoshis.rb | 16 +- ports/dsl/lighstorm.rb | 2 + .../bitcoin/request/decode_spec.rb | 152 ++++++++++++++++++ .../lightning/invoice/actions/pay_spec.rb | 4 +- .../lightning/node/actions/pay_spec.rb | 12 +- spec/models/lightning/edges/payment_spec.rb | 30 ++-- spec/models/satoshis_spec.rb | 20 +++ spec/ports/dsl/lighstorm_spec.rb | 1 + 16 files changed, 430 insertions(+), 36 deletions(-) create mode 100644 adapters/bitcoin/request.rb create mode 100644 controllers/bitcoin/request.rb create mode 100644 controllers/bitcoin/request/decode.rb create mode 100644 models/bitcoin/request.rb create mode 100644 spec/controllers/bitcoin/request/decode_spec.rb diff --git a/Gemfile.lock b/Gemfile.lock index 8472b10..ec50305 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -26,7 +26,7 @@ GEM grpc (~> 1.53) method_source (1.0.0) parallel (1.22.1) - parser (3.2.1.1) + parser (3.2.2.0) ast (~> 2.4.1) pry (0.14.2) coderay (~> 1.1) @@ -47,7 +47,7 @@ GEM rspec-expectations (3.12.2) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) - rspec-mocks (3.12.4) + rspec-mocks (3.12.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) rspec-support (3.12.0) diff --git a/adapters/bitcoin/request.rb b/adapters/bitcoin/request.rb new file mode 100644 index 0000000..1214965 --- /dev/null +++ b/adapters/bitcoin/request.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'digest' + +module Lighstorm + module Adapter + module Bitcoin + class Request + def self.decode(uri) + result = { + _source: :decode, + _key: Digest::SHA256.hexdigest([uri[:host]].concat(uri[:params].values).join('/')), + address: { code: uri[:host] } + } + + if uri[:params]['amount'] + result[:amount] = { + millisatoshis: uri[:params]['amount'].to_f * 100_000_000_000.0 + } + end + + result[:description] = uri[:params]['label'] if uri[:params]['label'] && uri[:params]['label'] != '' + + result[:message] = uri[:params]['message'] if uri[:params]['message'] && uri[:params]['message'] != '' + + result + end + end + end + end +end diff --git a/controllers/bitcoin/request.rb b/controllers/bitcoin/request.rb new file mode 100644 index 0000000..17ccb9c --- /dev/null +++ b/controllers/bitcoin/request.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require_relative '../concerns/impersonatable' +require_relative './request/decode' + +module Lighstorm + module Controller + module Bitcoin + module Request + extend Impersonatable + + class DSL < Impersonatable::DSL + def create + raise 'TODO' + # Create.perform(components, preview: preview, &vcr) + end + + def decode(uri) + Decode.model(Decode.data(uri: uri)) + end + end + end + end + end +end diff --git a/controllers/bitcoin/request/decode.rb b/controllers/bitcoin/request/decode.rb new file mode 100644 index 0000000..a919262 --- /dev/null +++ b/controllers/bitcoin/request/decode.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'uri' + +require_relative '../../../adapters/bitcoin/request' +require_relative '../../../models/bitcoin/request' + +module Lighstorm + module Controller + module Bitcoin + module Request + module Decode + def self.parse(uri) + # BIP21: https://bips.xyz/21 + parsed = if uri =~ /^\w+:/ + URI.parse(uri.sub('bitcoin:', 'bitcoin://')) + else + URI.parse("unknown://#{uri}") + end + + uri_hash = { + scheme: parsed.scheme, + host: parsed.host, + port: parsed.port, + path: parsed.path, + query: parsed.query, + fragment: parsed.fragment + } + + params = parsed.query ? URI.decode_www_form(parsed.query).to_h : {} + + { + raw: uri, + parsed: uri_hash.merge({ params: params }) + } + end + + def self.adapt(raw) + Lighstorm::Adapter::Bitcoin::Request.decode(raw[:parsed]) + end + + def self.data(uri:) + raw = parse(uri) + + adapt(raw) + end + + def self.model(data, components) + Lighstorm::Model::Bitcoin::Request.new(data, components) + end + end + end + end + end +end diff --git a/controllers/bitcoin/transaction.rb b/controllers/bitcoin/transaction.rb index da041c2..ca17c02 100644 --- a/controllers/bitcoin/transaction.rb +++ b/controllers/bitcoin/transaction.rb @@ -11,8 +11,8 @@ module Transaction extend Impersonatable class DSL < Impersonatable::DSL - def all(limit: nil) - All.model(All.data(components, limit: limit)) + def all(direction: nil, limit: nil) + All.model(All.data(components, direction: direction, limit: limit)) end def find_by_hash(hash, &vcr) diff --git a/controllers/bitcoin/transaction/all.rb b/controllers/bitcoin/transaction/all.rb index f899546..0085440 100644 --- a/controllers/bitcoin/transaction/all.rb +++ b/controllers/bitcoin/transaction/all.rb @@ -9,7 +9,7 @@ module Controller module Bitcoin module Transaction module All - def self.fetch(components, hash: nil, limit: nil) + def self.fetch(components, direction: nil, hash: nil, limit: nil) at = Time.now transactions = [] @@ -17,7 +17,12 @@ def self.fetch(components, hash: nil, limit: nil) response = components[:grpc].lightning.get_transactions response.transactions.each do |transaction| - next unless hash.nil? || transaction.to_h[:tx_hash] == hash + next unless hash.nil? || transaction.tx_hash == hash + + next unless + direction.nil? || + (direction == 'in' && transaction.amount.positive?) || + (direction == 'out' && transaction.amount.negative?) transactions << transaction.to_h end @@ -41,11 +46,11 @@ def self.transform(adapted) adapted[:get_transactions] end - def self.data(components, hash: nil, limit: nil, &vcr) + def self.data(components, hash: nil, direction: nil, limit: nil, &vcr) raw = if vcr.nil? - fetch(components, hash: hash, limit: limit) + fetch(components, hash: hash, direction: direction, limit: limit) else - vcr.call(-> { fetch(components, hash: hash, limit: limit) }) + vcr.call(-> { fetch(components, hash: hash, direction: direction, limit: limit) }) end adapted = adapt(raw) diff --git a/docs/README.md b/docs/README.md index cd3d1b3..cecfcaa 100644 --- a/docs/README.md +++ b/docs/README.md @@ -419,6 +419,24 @@ balance.to_h ## Bitcoin +### Request + +#### Decode + +Learn about [BIP 21](https://bips.xyz/21). + +```ruby +Lighstorm::Bitcoin::Request.decode( + 'bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?amount=50&label=Luke-Jr&message=Donation%20for%20project%20xyz' +) +``` + +#### Create + +```ruby +# TODO +``` + ### Address #### Create diff --git a/models/bitcoin/request.rb b/models/bitcoin/request.rb new file mode 100644 index 0000000..0a242f3 --- /dev/null +++ b/models/bitcoin/request.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require 'uri' + +require_relative '../satoshis' +require_relative './address' + +module Lighstorm + module Model + module Bitcoin + class Request + attr_reader :_key, :description, :message + + def initialize(data, components) + @data = data + @components = components + + @_key = @data[:_key] + @description = @data[:description] + @message = @data[:message] + end + + def address + @address ||= Address.new(@data[:address], @components) + end + + def amount + @amount ||= @data[:amount] ? Satoshis.new(millisatoshis: @data[:amount][:millisatoshis]) : nil + end + + def uri + return @uri unless @uri.nil? + + @uri = "bitcoin:#{address.code}" + + params = {} + + params[:amount] = amount.bitcoins if amount + params[:label] = description if description + params[:message] = message if message + + @uri = "#{@uri}?#{URI.encode_www_form(params)}" if params.keys.size.positive? + + @uri + end + + def pay( + fee:, required_confirmations: 6, + amount: nil, description: nil, + preview: false, &vcr + ) + address.pay( + amount: amount || self.amount.to_h, + fee: fee, + description: description || self.description, + required_confirmations: required_confirmations, + preview: false, &vcr + ) + end + + def to_h + output = { + _key: _key, + address: address.to_h, + uri: uri + } + + output[:amount] = amount.to_h if amount + output[:description] = description if description + output[:message] = message if message + + output + end + end + end + end +end diff --git a/models/satoshis.rb b/models/satoshis.rb index 40c49cf..b7077b0 100644 --- a/models/satoshis.rb +++ b/models/satoshis.rb @@ -20,7 +20,7 @@ def initialize(millisatoshis: nil, bitcoins: nil) end def parts_per_million(reference_millisatoshis) - ( + integer_if_possible( ( if reference_millisatoshis.zero? 0 @@ -32,15 +32,15 @@ def parts_per_million(reference_millisatoshis) end def millisatoshis - @amount_in_millisatoshis + integer_if_possible(@amount_in_millisatoshis) end def satoshis - @amount_in_millisatoshis.to_f / 1000.0 + integer_if_possible(@amount_in_millisatoshis.to_f / 1000.0) end def bitcoins - @amount_in_millisatoshis.to_f / 100_000_000_000 + integer_if_possible(@amount_in_millisatoshis.to_f / 100_000_000_000.0) end def sats @@ -60,6 +60,14 @@ def to_h millisatoshis: millisatoshis } end + + private + + def integer_if_possible(value) + return value.to_i if value.to_i == value + + value + end end end end diff --git a/ports/dsl/lighstorm.rb b/ports/dsl/lighstorm.rb index 5964673..880025d 100644 --- a/ports/dsl/lighstorm.rb +++ b/ports/dsl/lighstorm.rb @@ -18,6 +18,7 @@ require_relative '../../controllers/lightning/invoice' require_relative '../../controllers/bitcoin/transaction' +require_relative '../../controllers/bitcoin/request' require_relative '../../controllers/bitcoin/address' module Lighstorm @@ -37,6 +38,7 @@ module Lightning module Bitcoin Address = Controller::Bitcoin::Address + Request = Controller::Bitcoin::Request Transaction = Controller::Bitcoin::Transaction end diff --git a/spec/controllers/bitcoin/request/decode_spec.rb b/spec/controllers/bitcoin/request/decode_spec.rb new file mode 100644 index 0000000..e3318cc --- /dev/null +++ b/spec/controllers/bitcoin/request/decode_spec.rb @@ -0,0 +1,152 @@ +# frozen_string_literal: true + +require 'json' + +require_relative '../../../../controllers/bitcoin/request/decode' +require_relative '../../../../controllers/bitcoin/request' + +RSpec.describe Lighstorm::Controller::Bitcoin::Request::Decode do + context 'complete' do + let(:uri) do + 'bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?amount=50&label=Luke-Jr&message=Donation%20for%20project%20xyz' + end + + it 'models' do + data = described_class.data(uri: uri) + + expect(data).to eq( + { _source: :decode, + _key: 'cfe53fceea2fb1ce8a0d40584e1e595bbc2182e445fd726df0ac80d288bb2952', + address: { code: '175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W' }, + amount: { millisatoshis: 5_000_000_000_000 }, + description: 'Luke-Jr', + message: 'Donation for project xyz' } + ) + + model = described_class.model(data, Lighstorm::Controller::Bitcoin::Request.components) + + expect(model.address.code).to eq('175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W') + expect(model.amount.bitcoins).to eq(50) + expect(model.description).to eq('Luke-Jr') + expect(model.message).to eq('Donation for project xyz') + expect(model.uri).to eq('bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?amount=50&label=Luke-Jr&message=Donation+for+project+xyz') + + expect(model.to_h).to eq( + { + _key: 'cfe53fceea2fb1ce8a0d40584e1e595bbc2182e445fd726df0ac80d288bb2952', + address: { code: '175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W' }, + amount: { millisatoshis: 5_000_000_000_000 }, + description: 'Luke-Jr', + message: 'Donation for project xyz', + uri: 'bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?amount=50&label=Luke-Jr&message=Donation+for+project+xyz' + } + ) + end + end + + context 'address' do + let(:uri) do + 'bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W' + end + + it 'models' do + data = described_class.data(uri: uri) + + expect(data).to eq( + { + _source: :decode, + _key: '416b10c7da18c34ae1444db9363e382e0db29b8323be07c34f312506425b834c', + address: { code: '175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W' } + } + ) + + model = described_class.model(data, Lighstorm::Controller::Bitcoin::Request.components) + + expect(model.address.code).to eq('175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W') + expect(model.amount).to be_nil + expect(model.description).to be_nil + expect(model.message).to be_nil + expect(model.uri).to eq('bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W') + + expect(model.to_h).to eq( + { + _key: '416b10c7da18c34ae1444db9363e382e0db29b8323be07c34f312506425b834c', + address: { code: '175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W' }, + uri: 'bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W' + } + ) + end + end + + context 'no scheme' do + let(:uri) do + '175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W' + end + + it 'models' do + data = described_class.data(uri: uri) + + expect(data).to eq( + { + _source: :decode, + _key: '416b10c7da18c34ae1444db9363e382e0db29b8323be07c34f312506425b834c', + address: { code: '175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W' } + } + ) + + model = described_class.model(data, Lighstorm::Controller::Bitcoin::Request.components) + + expect(model.address.code).to eq('175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W') + expect(model.amount).to be_nil + expect(model.description).to be_nil + expect(model.message).to be_nil + expect(model.uri).to eq('bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W') + + expect(model.to_h).to eq( + { + _key: '416b10c7da18c34ae1444db9363e382e0db29b8323be07c34f312506425b834c', + address: { code: '175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W' }, + uri: 'bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W' + } + ) + end + end + + context 'complete without scheme' do + let(:uri) do + '175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?amount=50&label=Luke-Jr&message=Donation+for+project+xyz' + end + + it 'models' do + data = described_class.data(uri: uri) + + expect(data).to eq( + { _source: :decode, + _key: 'cfe53fceea2fb1ce8a0d40584e1e595bbc2182e445fd726df0ac80d288bb2952', + address: { code: '175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W' }, + amount: { millisatoshis: 5_000_000_000_000 }, + description: 'Luke-Jr', + message: 'Donation for project xyz' } + ) + + model = described_class.model(data, Lighstorm::Controller::Bitcoin::Request.components) + + expect(model.address.code).to eq('175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W') + expect(model.amount.bitcoins).to eq(50) + expect(model.description).to eq('Luke-Jr') + expect(model.message).to eq('Donation for project xyz') + expect(model.uri).to eq('bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?amount=50&label=Luke-Jr&message=Donation+for+project+xyz') + + expect(model.to_h).to eq( + { + _key: 'cfe53fceea2fb1ce8a0d40584e1e595bbc2182e445fd726df0ac80d288bb2952', + address: { code: '175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W' }, + amount: { millisatoshis: 5_000_000_000_000 }, + description: 'Luke-Jr', + message: 'Donation for project xyz', + uri: 'bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?amount=50&label=Luke-Jr&message=Donation+for+project+xyz' + } + ) + end + end +end diff --git a/spec/controllers/lightning/invoice/actions/pay_spec.rb b/spec/controllers/lightning/invoice/actions/pay_spec.rb index 732992c..b20b512 100644 --- a/spec/controllers/lightning/invoice/actions/pay_spec.rb +++ b/spec/controllers/lightning/invoice/actions/pay_spec.rb @@ -196,7 +196,7 @@ expect(action.result.hops.last.amount.millisatoshis).to eq(params[:amount][:millisatoshis]) Contract.expect( - action.to_h, '176e70048ea1618e0412201e4e6c494326f5a8a1de41c73a081082ef390b35e5' + action.to_h, '0e86079201a777a5b22557498ee6103b5ddf62241e7b73546d7e84aa9a925158' ) do |actual, expected| expect(actual.hash).to eq(expected.hash) expect(actual.contract).to eq(expected.contract) @@ -278,7 +278,7 @@ expect(action.result.hops.last.amount.millisatoshis).to eq(params[:amount][:millisatoshis]) Contract.expect( - action.to_h, '32223ce26ec6f43a68f78b2c20a7427f9fb269f6714db1c5f79e8f8ac0f63f02' + action.to_h, '2179a0240df1c3c5cdb20c84bf2991e469274670fbb4d87c013c2e2fbc0b9025' ) do |actual, expected| expect(actual.hash).to eq(expected.hash) expect(actual.contract).to eq(expected.contract) diff --git a/spec/controllers/lightning/node/actions/pay_spec.rb b/spec/controllers/lightning/node/actions/pay_spec.rb index a8a02d3..06dc5e3 100644 --- a/spec/controllers/lightning/node/actions/pay_spec.rb +++ b/spec/controllers/lightning/node/actions/pay_spec.rb @@ -96,7 +96,7 @@ expect(model.hops.last.amount.millisatoshis).to eq(params[:amount][:millisatoshis]) Contract.expect( - model.to_h, 'f3f98ff41f35d3047890a8bedcb3b6eb4e0621b0939fc00d9fe19d48d7a8e972' + model.to_h, '3115f113268ef62f4d2af45d9b8e389689e39881c5e508d955a8c3db3e3b2907' ) do |actual, expected| expect(actual.hash).to eq(expected.hash) expect(actual.contract).to eq(expected.contract) @@ -169,7 +169,7 @@ expect(action.result.hops.last.amount.millisatoshis).to eq(params[:amount][:millisatoshis]) Contract.expect( - action.to_h, 'ac91973911ee5018ed03a5a0d90dd15dd508ca33e4612aac7914843d898473c8' + action.to_h, 'e4388f26923e46ee472caf18b08e114de935e210fa0d59e526120a3f3ca5e257' ) do |actual, expected| expect(actual.hash).to eq(expected.hash) expect(actual.contract).to eq(expected.contract) @@ -261,7 +261,7 @@ expect(model.hops.last.amount.millisatoshis).to eq(params[:amount][:millisatoshis]) Contract.expect( - model.to_h, 'f3f98ff41f35d3047890a8bedcb3b6eb4e0621b0939fc00d9fe19d48d7a8e972' + model.to_h, '3115f113268ef62f4d2af45d9b8e389689e39881c5e508d955a8c3db3e3b2907' ) do |actual, expected| expect(actual.hash).to eq(expected.hash) expect(actual.contract).to eq(expected.contract) @@ -333,7 +333,7 @@ expect(action.result.hops.last.amount.millisatoshis).to eq(params[:amount][:millisatoshis]) Contract.expect( - action.to_h, '7a9a77d3b26b7f24ad095387b868767b1fb731b44f897c4db999be8510dbad43' + action.to_h, '0b9f0d982bbf0b3c1b59b68ecb8395cbc47d267d838bade66a9a0246c10a3e85' ) do |actual, expected| expect(actual.hash).to eq(expected.hash) expect(actual.contract).to eq(expected.contract) @@ -481,7 +481,7 @@ expect(model.hops).to be_nil Contract.expect( - model.to_h, '77e2f72deb98fda98eebef3f8984410dd8bfbd896058cbc93918bcf606f24355' + model.to_h, '887fa14bbe9ff66d841f1fdb7fbbe23d7fcb5ea76a06741e5108cf197f01544a' ) do |actual, expected| expect(actual.hash).to eq(expected.hash) expect(actual.contract).to eq(expected.contract) @@ -553,7 +553,7 @@ expect(e.response.last[:failure_reason]).to eq(:FAILURE_REASON_NO_ROUTE) Contract.expect( - e.to_h, 'dcae6cfd1d85026057db959d2b9baf62d265bb463271f2aafbd7bed07358f960' + e.to_h, 'd41a2a58a08274d90f61605c07135fc35b8cb2573899b9f4947939a665820557' ) do |actual, expected| expect(actual.hash).to eq(expected.hash) expect(actual.contract).to eq(expected.contract) diff --git a/spec/models/lightning/edges/payment_spec.rb b/spec/models/lightning/edges/payment_spec.rb index ee5f185..2f1fb16 100644 --- a/spec/models/lightning/edges/payment_spec.rb +++ b/spec/models/lightning/edges/payment_spec.rb @@ -69,7 +69,7 @@ Lighstorm::Model::Satoshis.new(millisatoshis: 1000) end - let(:to_h_contract) { 'bbe712677e9e72c5eb65865cda29b7e92ae3b9a196b5a946badaf45d4a9d306e' } + let(:to_h_contract) { '36a6dee173c4783a3ac04c0d69e60cffb34e0fe33846d8bdb7c7967888e46fb2' } it 'models' do expect(data[:meta][:calls][:decode_pay_req]).to be_nil @@ -184,7 +184,7 @@ Lighstorm::Model::Satoshis.new(millisatoshis: 1000) end - let(:to_h_contract) { 'abd510a173e1dc4de2547d48adab3b892a636802489e2fc99f239fe62d353760' } + let(:to_h_contract) { '0de14ab266b071700de90c0aa8a2f027873b30d7d997f45343a76e5d91b5f68f' } it 'models' do expect(data[:meta][:calls].keys.sort).to eq( @@ -301,7 +301,7 @@ Lighstorm::Model::Satoshis.new(millisatoshis: 150_000) end - let(:to_h_contract) { 'b965114e1ac50b653d74e39e8920235c95d79a2376a9e49837e78432e9587d21' } + let(:to_h_contract) { '5447edbf9384e7366a4c74283d324be2aa8ef390c44fcee365d17307bd0f43c1' } it 'models' do expect(payment._key.size).to eq(64) @@ -400,7 +400,7 @@ Lighstorm::Model::Satoshis.new(millisatoshis: 3_050_000_000) end - let(:to_h_contract) { '96769c5f7263f741463b7f0da83351219064a47f70180d6d4d2c9399c36ba88b' } + let(:to_h_contract) { '82049dafe05d8c5ddb3511538fc19af36d1f9ca4f4a91e8d99d41efd11eaac30' } it 'models' do expect(payment._key.size).to eq(64) @@ -497,7 +497,7 @@ Lighstorm::Model::Satoshis.new(millisatoshis: 137_000) end - let(:to_h_contract) { '99cbf5f017de4101e0612424d9378d9fa1d7628ae95a5644a97d4ea7cbb1bd0c' } + let(:to_h_contract) { '744a266bf986652626047c64f22d26e6902cb10ebb93978c3a9338643a73cf24' } it 'models' do expect(payment._key.size).to eq(64) @@ -598,7 +598,7 @@ Lighstorm::Model::Satoshis.new(millisatoshis: 130_000_000) end - let(:to_h_contract) { 'caf71adfc1bbe7a2f91a5a5935776aad3d921b8e2b7cb850cf98ce070d3db530' } + let(:to_h_contract) { 'a5560d0326bf693487f32bfa81e3388c76724330736e136186b78e69b56a6ff0' } it 'models' do expect(payment._key.size).to eq(64) @@ -699,7 +699,7 @@ Lighstorm::Model::Satoshis.new(millisatoshis: 1200) end - let(:to_h_contract) { 'ffb21922e00a6f37fa7ee599d38bb6f494a60efb40f4fc66a49a84e9b21d0b0d' } + let(:to_h_contract) { '39039307f9b86cb29aa45300709600111c5a985c6ce1fc8d0905a625e7b6f02c' } it 'models' do expect(payment._key.size).to eq(64) @@ -784,7 +784,7 @@ Lighstorm::Model::Satoshis.new(millisatoshis: 1500) end - let(:to_h_contract) { 'ffb21922e00a6f37fa7ee599d38bb6f494a60efb40f4fc66a49a84e9b21d0b0d' } + let(:to_h_contract) { '39039307f9b86cb29aa45300709600111c5a985c6ce1fc8d0905a625e7b6f02c' } it 'models' do expect(payment._key.size).to eq(64) @@ -869,7 +869,7 @@ Lighstorm::Model::Satoshis.new(millisatoshis: 1000) end - let(:to_h_contract) { 'e44bca0cb1d28df0865ee6eb880977367a4a5b18a55193f44cc5e86dcaacf154' } + let(:to_h_contract) { '5faff4c86ce94b6f06af506fa564da999640f9909ecfb4745a3a3bbbb1446f07' } it 'models' do expect(payment._key.size).to eq(64) @@ -983,7 +983,7 @@ Lighstorm::Model::Satoshis.new(millisatoshis: 1210) end - let(:to_h_contract) { 'bc43dd9fc7648e54185c27060219c2ee52adc14ec503469c0221513f826dfae3' } + let(:to_h_contract) { '4a98877e35774b817e887922ccfb86c5d6ac7ebb7b18749077922886741542be' } it 'models' do expect(payment._key.size).to eq(64) @@ -1096,7 +1096,7 @@ Lighstorm::Model::Satoshis.new(millisatoshis: 1121) end - let(:to_h_contract) { 'd54e69d7f72d4dbb8c5c617ff00bad9c9e42064b94e6e0a50a7a1174c9b19432' } + let(:to_h_contract) { 'bf98664158b4c62929000d2df2dc3d2140f1b629b372306498b0d13689fe4a6e' } it 'models' do expect(payment._key.size).to eq(64) @@ -1209,7 +1209,7 @@ Lighstorm::Model::Satoshis.new(millisatoshis: 1278) end - let(:to_h_contract) { '735c79f5d1120313407dd5f2b947c9afb44b6d5ccc5eb68e813a4d124d491d14' } + let(:to_h_contract) { '5360e82c935621238eedc10fea167472af32cc795bba828f23b74ae8c4156c89' } it 'models' do expect(payment._key.size).to eq(64) @@ -1322,7 +1322,7 @@ Lighstorm::Model::Satoshis.new(millisatoshis: 3_000_000) end - let(:to_h_contract) { 'bcdf5ab24266264839bed40e9ed45af9676a7b27d2ec95ad209efaacb1809b04' } + let(:to_h_contract) { 'fb3562b218ee7deee56bcd12b54e5cdf697f8bb8a68c6fc3b7de5c95c1f5fc1c' } it 'models' do expect(payment._key.size).to eq(64) @@ -1447,7 +1447,7 @@ Lighstorm::Model::Satoshis.new(millisatoshis: 49_000) end - let(:to_h_contract) { '8e72f213933366aa4099659d167f90890bce1448b1a04183e2cd8d7563dce7bc' } + let(:to_h_contract) { 'e5e5b0901e2a5c1e401aea998b2b75bfd9f6d794747dc07521005b771c00954a' } it 'models' do expect(payment._key.size).to eq(64) @@ -1563,7 +1563,7 @@ Lighstorm::Model::Satoshis.new(millisatoshis: 49_000) end - let(:to_h_contract) { '8e72f213933366aa4099659d167f90890bce1448b1a04183e2cd8d7563dce7bc' } + let(:to_h_contract) { 'e5e5b0901e2a5c1e401aea998b2b75bfd9f6d794747dc07521005b771c00954a' } it 'models' do expect(payment._key.size).to eq(64) diff --git a/spec/models/satoshis_spec.rb b/spec/models/satoshis_spec.rb index c4e4ce0..6d9e9eb 100644 --- a/spec/models/satoshis_spec.rb +++ b/spec/models/satoshis_spec.rb @@ -23,6 +23,26 @@ end end + describe 'satoshis int' do + it 'creates' do + amount = described_class.new( + millisatoshis: 50_000_000_000_0 + ) + + expect(amount.millisatoshis).to eq(500_000_000_000) + expect(amount.satoshis).to eq(500_000_000) + expect(amount.bitcoins).to eq(5) + + expect(amount.msats).to eq(500_000_000_000) + expect(amount.sats).to eq(500_000_000) + expect(amount.btc).to eq(5) + + expect(amount.parts_per_million(25_000_000_000_000)).to eq(20_000) + + expect(amount.to_h).to eq({ millisatoshis: 500_000_000_000 }) + end + end + describe 'bitcoins' do it 'creates 0.0045' do amount = described_class.new( diff --git a/spec/ports/dsl/lighstorm_spec.rb b/spec/ports/dsl/lighstorm_spec.rb index 5edc7e0..41ec226 100644 --- a/spec/ports/dsl/lighstorm_spec.rb +++ b/spec/ports/dsl/lighstorm_spec.rb @@ -11,6 +11,7 @@ expect(Lighstorm::Wallet).to respond_to(:balance) expect(Lighstorm::Wallet::Activity).to respond_to(:all) + expect(Lighstorm::Bitcoin::Request).to respond_to(:decode, :create) expect(Lighstorm::Bitcoin::Address).to respond_to(:new, :create) expect(Lighstorm::Bitcoin::Transaction).to respond_to(:all, :find_by_hash) From 9d22317b3ae9d93fb706c6acfadcaa3926281207 Mon Sep 17 00:00:00 2001 From: icebaker Date: Sun, 2 Apr 2023 21:38:23 -0300 Subject: [PATCH 2/4] improving btc payment flow --- controllers/bitcoin/request.rb | 38 ++- docs/README.md | 109 +++++++- models/bitcoin/request.rb | 2 +- models/bitcoin/transaction.rb | 21 +- .../bitcoin/address/actions/pay_spec.rb | 6 +- .../bitcoin/request/decode_spec.rb | 1 + .../address_spec.rb} | 8 +- spec/ports/dsl/bitcoin/request_spec.rb | 232 ++++++++++++++++++ .../ports/dsl/{ => lightning}/channel_spec.rb | 4 +- .../ports/dsl/{ => lightning}/invoice_spec.rb | 4 +- spec/ports/dsl/{ => lightning}/node_spec.rb | 4 +- 11 files changed, 407 insertions(+), 22 deletions(-) rename spec/ports/dsl/{bitcoin_address.rb => bitcoin/address_spec.rb} (90%) create mode 100644 spec/ports/dsl/bitcoin/request_spec.rb rename spec/ports/dsl/{ => lightning}/channel_spec.rb (98%) rename spec/ports/dsl/{ => lightning}/invoice_spec.rb (96%) rename spec/ports/dsl/{ => lightning}/node_spec.rb (95%) diff --git a/controllers/bitcoin/request.rb b/controllers/bitcoin/request.rb index 17ccb9c..c649dd4 100644 --- a/controllers/bitcoin/request.rb +++ b/controllers/bitcoin/request.rb @@ -1,7 +1,11 @@ # frozen_string_literal: true +require 'digest' + require_relative '../concerns/impersonatable' require_relative './request/decode' +require_relative './address/actions/create' +require_relative '../action' module Lighstorm module Controller @@ -10,13 +14,39 @@ module Request extend Impersonatable class DSL < Impersonatable::DSL - def create - raise 'TODO' - # Create.perform(components, preview: preview, &vcr) + def create(address: nil, amount: nil, description: nil, message: nil, preview: false, &vcr) + if address.nil? + address_action = Address::Create.perform(components, preview: preview, &vcr) + return address_action if preview + + address = address_action.result.to_h + else + address_action = nil + end + + model = Model::Bitcoin::Request.new( + { + _key: Digest::SHA256.hexdigest( + [ + address ? address[:code] : nil, + amount ? amount[:millisatoshis] : nil, + description, message + ].join('/') + ), + address: address, amount: amount, description: description, message: message + }, + components + ) + + Action::Output.new({ + request: address_action&.request, + response: address_action&.response, + result: model + }) end def decode(uri) - Decode.model(Decode.data(uri: uri)) + Decode.model(Decode.data(uri: uri), components) end end end diff --git a/docs/README.md b/docs/README.md index cecfcaa..ec38997 100644 --- a/docs/README.md +++ b/docs/README.md @@ -421,20 +421,123 @@ balance.to_h ### Request +#### Create + +```ruby +Lighstorm::Bitcoin::Request.create( + { address: { code: '175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W' }, + amount: { millisatoshis: 5000000000000 }, + description: 'Luke-Jr', + message: 'Donation for project xyz', + preview: true } +) +``` + +```ruby +action = Lighstorm::Bitcoin::Request.create( + { address: { code: '175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W' }, + amount: { millisatoshis: 5000000000000 }, + description: 'Luke-Jr', + message: 'Donation for project xyz' } +) + +action.request +action.response + +request = action.result + +request.address.code # '175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W' +request.amount.bitcoins # 50 +request.description # 'Luke-Jr' +request.message # 'Donation for project xyz' +request.uri # 'bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?amount=50&label=Luke-Jr&message=Donation+for+project+xyz' +``` + +If you don't provide a Bitcoin Address, a new one will be generated for your request, with your wallet as the destination: + +```ruby +action = Lighstorm::Bitcoin::Request.create + +action.request +action.response + +request = action.result + +request.address.code # 'bc1qytzke5v5qa4wqzhct37gwnpqs08tuyq9stst5j' +request.uri # 'bitcoin:bc1qytzke5v5qa4wqzhct37gwnpqs08tuyq9stst5j' +``` + +```ruby +action = Lighstorm::Bitcoin::Request.create( + { amount: { millisatoshis: 5000000000000 }, + description: 'Luke-Jr', + message: 'Donation for project xyz' } +) + +action.request +action.response + +request = action.result + +request.address.code # 'bc1qytzke5v5qa4wqzhct37gwnpqs08tuyq9stst5j' +request.amount.bitcoins # 50 +request.description # 'Luke-Jr' +request.message # 'Donation for project xyz' +request.uri # 'bitcoin:bc1qytzke5v5qa4wqzhct37gwnpqs08tuyq9stst5j?amount=50&label=Luke-Jr&message=Donation+for+project+xyz' +``` + #### Decode Learn about [BIP 21](https://bips.xyz/21). ```ruby -Lighstorm::Bitcoin::Request.decode( +request = Lighstorm::Bitcoin::Request.decode( 'bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?amount=50&label=Luke-Jr&message=Donation%20for%20project%20xyz' ) + +request._key + +request.address.code # '175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W' +request.amount.bitcoins # 50 +request.description # 'Luke-Jr' +request.message # 'Donation for project xyz' +request.uri # 'bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?amount=50&label=Luke-Jr&message=Donation+for+project+xyz' ``` -#### Create +#### Pay + +Learn about [BIP 21](https://bips.xyz/21). + +```ruby +action = Lighstorm::Bitcoin::Request.decode( + 'bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?amount=50&label=Luke-Jr&message=Donation%20for%20project%20xyz' +).pay( + amount: { millisatoshis: 5000000000000 }, + description: 'Making a Donation', + fee: { satoshis_per_vitual_byte: 1 }, + required_confirmations: 6, + preview: true +) +``` ```ruby -# TODO +action = Lighstorm::Bitcoin::Request.decode( + 'bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?amount=50&label=Luke-Jr&message=Donation%20for%20project%20xyz' +).pay(fee: { satoshis_per_vitual_byte: 1 }) + +action.request +action.response + +transaction = action.result + +transaction._key +transaction.at +transaction.amount.bitcoins # 50 +transaction.fee.millisatoshis # 154_000 +transaction.description # 'Luke-Jr' + +transaction.hash # 5ee90f3d8f3efac87c80797773d696e59986477c9201e5cf15a8abac5f632dd4 +transaction.to.address.code # '175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W' ``` ### Address diff --git a/models/bitcoin/request.rb b/models/bitcoin/request.rb index 0a242f3..a7ecca2 100644 --- a/models/bitcoin/request.rb +++ b/models/bitcoin/request.rb @@ -54,7 +54,7 @@ def pay( fee: fee, description: description || self.description, required_confirmations: required_confirmations, - preview: false, &vcr + preview: preview, &vcr ) end diff --git a/models/bitcoin/transaction.rb b/models/bitcoin/transaction.rb index e67d830..9312ea0 100644 --- a/models/bitcoin/transaction.rb +++ b/models/bitcoin/transaction.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require_relative '../satoshis' +require_relative './address' module Lighstorm module Model @@ -25,8 +26,22 @@ def fee @fee ||= Satoshis.new(millisatoshis: @data[:fee][:millisatoshis]) end + def to + @to ||= if @data[:to] + Struct.new(:data) do + def address + @address ||= Address.new({ code: data[:address][:code] }, nil) + end + + def to_h + { address: address.to_h } + end + end.new(@data[:to]) + end + end + def to_h - { + output = { _key: _key, at: at, hash: hash, @@ -34,6 +49,10 @@ def to_h fee: fee.to_h, description: description } + + output[:to] = to.to_h if to + + output end end end diff --git a/spec/controllers/bitcoin/address/actions/pay_spec.rb b/spec/controllers/bitcoin/address/actions/pay_spec.rb index a54d31e..4a89052 100644 --- a/spec/controllers/bitcoin/address/actions/pay_spec.rb +++ b/spec/controllers/bitcoin/address/actions/pay_spec.rb @@ -123,7 +123,7 @@ expect(model.description).to eq('Wallet Withdrawal') Contract.expect( - model.to_h, '2f2827afe3acae0d17abdbee49f1006f63a826ccadd06f5bc3fc8e859e6dffad' + model.to_h, '43ba1c30001bdee50a01d6b61006444535ff8073cb25cd31dd690ecdaed44ca1' ) do |actual, expected| expect(actual.hash).to eq(expected.hash) expect(actual.contract).to eq(expected.contract) @@ -185,7 +185,7 @@ expect(action.result.description).to eq('Wallet Withdrawal') Contract.expect( - action.to_h, 'f2a2ecb8b4443ed8511f30cc76b1639552421ec0afdca3b134e0dc8b071e3e2b' + action.to_h, '3974376ae14a4685ef4612c453dd839a6d020897b70fcb91518411acc9297a45' ) do |actual, expected| expect(actual.hash).to eq(expected.hash) expect(actual.contract).to eq(expected.contract) @@ -194,4 +194,4 @@ end end end -end +end \ No newline at end of file diff --git a/spec/controllers/bitcoin/request/decode_spec.rb b/spec/controllers/bitcoin/request/decode_spec.rb index e3318cc..296b93d 100644 --- a/spec/controllers/bitcoin/request/decode_spec.rb +++ b/spec/controllers/bitcoin/request/decode_spec.rb @@ -25,6 +25,7 @@ model = described_class.model(data, Lighstorm::Controller::Bitcoin::Request.components) + expect(model._key.size).to eq(64) expect(model.address.code).to eq('175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W') expect(model.amount.bitcoins).to eq(50) expect(model.description).to eq('Luke-Jr') diff --git a/spec/ports/dsl/bitcoin_address.rb b/spec/ports/dsl/bitcoin/address_spec.rb similarity index 90% rename from spec/ports/dsl/bitcoin_address.rb rename to spec/ports/dsl/bitcoin/address_spec.rb index 13bbae0..e557236 100644 --- a/spec/ports/dsl/bitcoin_address.rb +++ b/spec/ports/dsl/bitcoin/address_spec.rb @@ -2,8 +2,8 @@ require 'json' -require_relative '../../../ports/dsl/lighstorm' -require_relative '../../../ports/dsl/lighstorm/errors' +require_relative '../../../../ports/dsl/lighstorm' +require_relative '../../../../ports/dsl/lighstorm/errors' RSpec.describe Lighstorm::Bitcoin::Address do describe 'create invoice' do @@ -73,10 +73,10 @@ transaction = action.result expect(transaction._key.size).to eq(64) - expect(transaction.at.utc.to_s).to eq('2023-04-02 13:14:44 UTC') + expect(transaction.at.utc.to_s).to eq('2023-04-02 23:14:28 UTC') expect(transaction.hash).to eq(action.response[:txid]) expect(transaction.amount.millisatoshis).to eq(-250_000_000) - expect(transaction.fee.millisatoshis).to eq(154_000) + expect(transaction.fee.millisatoshis).to eq(203_000) expect(transaction.description).to eq('external') end end diff --git a/spec/ports/dsl/bitcoin/request_spec.rb b/spec/ports/dsl/bitcoin/request_spec.rb new file mode 100644 index 0000000..aaa6072 --- /dev/null +++ b/spec/ports/dsl/bitcoin/request_spec.rb @@ -0,0 +1,232 @@ +# frozen_string_literal: true + +require 'json' + +require_relative '../../../../ports/dsl/lighstorm' +require_relative '../../../../ports/dsl/lighstorm/errors' + +RSpec.describe Lighstorm::Bitcoin::Request do + describe 'create invoice' do + let(:vcr_key) { 'Lighstorm::Bitcoin::Request::Create' } + + context 'defined address' do + let(:params) do + { address: { code: '175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W' }, + amount: { millisatoshis: 5_000_000_000_000 }, + description: 'Luke-Jr', + message: 'Donation for project xyz' } + end + + context 'perform' do + it 'performs' do + action = described_class.create(params) do |fn, from = :fetch| + VCR.reel.unsafe('I_KNOW_WHAT_I_AM_DOING').replay("#{vcr_key}/#{from}", params) { fn.call } + end + + expect(action.result.class).to eq(Lighstorm::Model::Bitcoin::Request) + + expect(action.result._key.size).to eq(64) + expect(action.result.address.code).to eq(params[:address][:code]) + expect(action.result.amount.millisatoshis).to eq(params[:amount][:millisatoshis]) + expect(action.result.description).to eq(params[:description]) + expect(action.result.message).to eq(params[:message]) + + expect(action.request).to be_nil + expect(action.response).to be_nil + + expect(action.to_h.keys).to eq(%i[request response result]) + + expect(action.result.to_h).to eq( + { _key: 'aac0e78574a8e8364dfe73140e69505c3024f6257cb34761a911b2e6ba99417f', + address: { code: '175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W' }, + uri: 'bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?amount=50&label=Luke-Jr&message=Donation+for+project+xyz', + amount: { millisatoshis: 5_000_000_000_000 }, + description: 'Luke-Jr', + message: 'Donation for project xyz' } + ) + end + end + end + + context 'new address' do + let(:params) do + { amount: { millisatoshis: 5_000_000_000_000 }, + description: 'Luke-Jr', + message: 'Donation for project xyz' } + end + + context 'perform' do + it 'performs' do + action = described_class.create(params) do |fn, from = :fetch| + VCR.reel.unsafe('I_KNOW_WHAT_I_AM_DOING').replay("#{vcr_key}/#{from}", params) { fn.call } + end + + expect(action.result.class).to eq(Lighstorm::Model::Bitcoin::Request) + + expect(action.result._key.size).to eq(64) + expect(action.result.address.code).to eq('bcrt1qfxahhms7c7z5elhnjc4hymvzhwmwmh5slv7jv5') + expect(action.result.amount.millisatoshis).to eq(params[:amount][:millisatoshis]) + expect(action.result.description).to eq(params[:description]) + expect(action.result.message).to eq(params[:message]) + + expect(action.request).to eq( + { service: :lightning, method: :new_address, params: { type: :WITNESS_PUBKEY_HASH } } + ) + expect(action.response).to eq({ address: 'bcrt1qfxahhms7c7z5elhnjc4hymvzhwmwmh5slv7jv5' }) + + expect(action.to_h.keys).to eq(%i[request response result]) + + result_to_h = action.result.to_h + + expect(result_to_h[:address][:created_at].class).to eq(Time) + result_to_h[:address][:created_at] = result_to_h[:address][:created_at].utc.to_s + + expect(result_to_h).to eq( + { + _key: '80a6f24aaaf4c8c36526838853032afd491481085c331322814f35f7ec0d58fd', + address: { + _key: 'e2d74b31321c86571493704e79df0db28b239fed35588e6fa45374ef78efd897', + created_at: '2023-04-02 23:26:12 UTC', + code: 'bcrt1qfxahhms7c7z5elhnjc4hymvzhwmwmh5slv7jv5' + }, + uri: 'bitcoin:bcrt1qfxahhms7c7z5elhnjc4hymvzhwmwmh5slv7jv5?amount=50&label=Luke-Jr&message=Donation+for+project+xyz', + amount: { millisatoshis: 5_000_000_000_000 }, + description: 'Luke-Jr', + message: 'Donation for project xyz' + } + ) + end + end + end + + context 'new address only' do + context 'perform' do + it 'performs' do + action = described_class.create do |fn, from = :fetch| + VCR.reel.unsafe('I_KNOW_WHAT_I_AM_DOING').replay("#{vcr_key}/#{from}") { fn.call } + end + + expect(action.result.class).to eq(Lighstorm::Model::Bitcoin::Request) + + expect(action.result._key.size).to eq(64) + expect(action.result.address.code).to eq('bc1qytzke5v5qa4wqzhct37gwnpqs08tuyq9stst5j') + expect(action.result.amount).to be_nil + expect(action.result.description).to be_nil + expect(action.result.message).to be_nil + + expect(action.request).to eq( + { service: :lightning, method: :new_address, params: { type: :WITNESS_PUBKEY_HASH } } + ) + expect(action.response).to eq({ address: 'bc1qytzke5v5qa4wqzhct37gwnpqs08tuyq9stst5j' }) + + expect(action.to_h.keys).to eq(%i[request response result]) + + result_to_h = action.result.to_h + + expect(result_to_h[:address][:created_at].class).to eq(Time) + result_to_h[:address][:created_at] = result_to_h[:address][:created_at].utc.to_s + + expect(result_to_h).to eq( + { _key: 'ab3ed16c2e149776c14373a3b4e67541dd530635e3f6460e85d233a36107e0ac', + address: { + _key: 'a1dd3ab47f04d1d9ba995ba00f66dd65073170c421de6da0ba9e28ae99bc02ec', + created_at: '2023-04-03 00:05:15 UTC', + code: 'bc1qytzke5v5qa4wqzhct37gwnpqs08tuyq9stst5j' + }, + uri: 'bitcoin:bc1qytzke5v5qa4wqzhct37gwnpqs08tuyq9stst5j' } + ) + end + end + end + + context 'payment' do + let(:vcr_key) { 'Lighstorm::Bitcoin::Request::Pay' } + + let(:params) do + { address: { code: 'bcrt1quykkly6egez6dzgfz072h5806p4g2gajm873xn' }, + amount: { millisatoshis: 500_000 }, + description: 'Pay Alice', + message: 'Hi Alice!' } + end + + context 'perform' do + it 'pays' do + action = described_class.create(params) do |fn, from = :fetch| + VCR.reel.unsafe('I_KNOW_WHAT_I_AM_DOING').replay("#{vcr_key}/#{from}", params) { fn.call } + end + + request = action.result + + expect(request.to_h).to eq( + { + _key: 'e107e5737d0a2347d6b471fbdd2222163b63e9c7c9a48d169681db9946be21cd', + address: { code: 'bcrt1quykkly6egez6dzgfz072h5806p4g2gajm873xn' }, + uri: 'bitcoin:bcrt1quykkly6egez6dzgfz072h5806p4g2gajm873xn?amount=5.0e-06&label=Pay+Alice&message=Hi+Alice%21', + amount: { millisatoshis: 500_000 }, + description: 'Pay Alice', + message: 'Hi Alice!' + } + ) + + preview = request.pay(fee: { satoshis_per_vitual_byte: 1 }, preview: true) + + expect(preview).to eq( + { service: :lightning, + method: :send_coins, + params: { + addr: 'bcrt1quykkly6egez6dzgfz072h5806p4g2gajm873xn', + amount: 500, + sat_per_vbyte: 1, + min_confs: 6, + label: 'Pay Alice' + } } + ) + + action = request.pay(fee: { satoshis_per_vitual_byte: 1 }) do |fn, from = :fetch| + VCR.reel.unsafe('I_KNOW_WHAT_I_AM_DOING').replay("#{vcr_key}/#{from}", params) { fn.call } + end + + expect(action.request.to_h).to eq( + { + service: :lightning, + method: :send_coins, + params: { + addr: 'bcrt1quykkly6egez6dzgfz072h5806p4g2gajm873xn', + amount: 500, + sat_per_vbyte: 1, + min_confs: 6, + label: 'Pay Alice' + } + } + ) + + expect(action.response.to_h).to eq( + { txid: '5ee90f3d8f3efac87c80797773d696e59986477c9201e5cf15a8abac5f632dd4' } + ) + + transaction = action.result + transaction_to_h = transaction.to_h + + expect(transaction_to_h[:at].class).to eq(Time) + transaction_to_h[:at] = transaction_to_h[:at].utc.to_s + + expect(transaction.hash).to eq('5ee90f3d8f3efac87c80797773d696e59986477c9201e5cf15a8abac5f632dd4') + expect(transaction.amount.millisatoshis).to eq(-500_000) + expect(transaction.fee.millisatoshis).to eq(154_000) + expect(transaction.description).to eq('Pay Alice') + expect(transaction.to.address.code).to eq('bcrt1quykkly6egez6dzgfz072h5806p4g2gajm873xn') + + expect(transaction_to_h).to eq( + { _key: '0f229ba4a0f18b53d62179da80a8bb351160133873c42323a8692a1cd9f89e5f', + at: '2023-04-02 23:44:41 UTC', + hash: '5ee90f3d8f3efac87c80797773d696e59986477c9201e5cf15a8abac5f632dd4', + amount: { millisatoshis: -500_000 }, + fee: { millisatoshis: 154_000 }, + description: 'Pay Alice', + to: { address: { code: 'bcrt1quykkly6egez6dzgfz072h5806p4g2gajm873xn' } } } + ) + end + end + end + end +end diff --git a/spec/ports/dsl/channel_spec.rb b/spec/ports/dsl/lightning/channel_spec.rb similarity index 98% rename from spec/ports/dsl/channel_spec.rb rename to spec/ports/dsl/lightning/channel_spec.rb index cdcafdf..ed35186 100644 --- a/spec/ports/dsl/channel_spec.rb +++ b/spec/ports/dsl/lightning/channel_spec.rb @@ -2,8 +2,8 @@ require 'json' -require_relative '../../../ports/dsl/lighstorm' -require_relative '../../../ports/dsl/lighstorm/errors' +require_relative '../../../../ports/dsl/lighstorm' +require_relative '../../../../ports/dsl/lighstorm/errors' RSpec.describe Lighstorm::Lightning::Channel do context 'adapts' do diff --git a/spec/ports/dsl/invoice_spec.rb b/spec/ports/dsl/lightning/invoice_spec.rb similarity index 96% rename from spec/ports/dsl/invoice_spec.rb rename to spec/ports/dsl/lightning/invoice_spec.rb index 03263e9..1112736 100644 --- a/spec/ports/dsl/invoice_spec.rb +++ b/spec/ports/dsl/lightning/invoice_spec.rb @@ -2,8 +2,8 @@ require 'json' -require_relative '../../../ports/dsl/lighstorm' -require_relative '../../../ports/dsl/lighstorm/errors' +require_relative '../../../../ports/dsl/lighstorm' +require_relative '../../../../ports/dsl/lighstorm/errors' RSpec.describe Lighstorm::Lightning::Invoice do describe 'create invoice' do diff --git a/spec/ports/dsl/node_spec.rb b/spec/ports/dsl/lightning/node_spec.rb similarity index 95% rename from spec/ports/dsl/node_spec.rb rename to spec/ports/dsl/lightning/node_spec.rb index 9d2d51a..60dab58 100644 --- a/spec/ports/dsl/node_spec.rb +++ b/spec/ports/dsl/lightning/node_spec.rb @@ -2,8 +2,8 @@ require 'json' -require_relative '../../../ports/dsl/lighstorm' -require_relative '../../../ports/dsl/lighstorm/errors' +require_relative '../../../../ports/dsl/lighstorm' +require_relative '../../../../ports/dsl/lighstorm/errors' RSpec.describe Lighstorm::Lightning::Node do context 'adapts' do From c21f08618d69abf171647280485d11a1e04a0003 Mon Sep 17 00:00:00 2001 From: icebaker Date: Tue, 4 Apr 2023 21:29:46 -0300 Subject: [PATCH 3/4] adding support for multiple btc formats --- adapters/bitcoin/request.rb | 4 +- controllers/bitcoin/address.rb | 4 +- controllers/bitcoin/address/actions/create.rb | 50 ++-- controllers/bitcoin/address/actions/pay.rb | 2 +- controllers/bitcoin/request.rb | 4 +- controllers/lightning/invoice/decode.rb | 10 +- docs/README.md | 27 ++- models/bitcoin/address.rb | 44 +++- models/bitcoin/request.rb | 4 +- models/lightning/invoice.rb | 5 + .../bitcoin/address/actions/create_spec.rb | 28 +-- .../bitcoin/address/actions/pay_spec.rb | 14 +- .../bitcoin/request/decode_spec.rb | 84 +++++-- .../lightning/invoice/actions/create_spec.rb | 23 +- .../lightning/invoice/actions/pay_spec.rb | 4 +- spec/controllers/lightning/invoice_spec.rb | 114 ++++++++++ spec/models/bitcoin/address_spec.rb | 213 ++++++++++++++++++ spec/models/lightning/edges/payment_spec.rb | 26 +-- spec/models/lightning/invoice_spec.rb | 28 +-- spec/ports/dsl/bitcoin/address_spec.rb | 10 +- spec/ports/dsl/bitcoin/request_spec.rb | 151 ++++++++++--- spec/ports/dsl/lightning/invoice_spec.rb | 4 +- 22 files changed, 687 insertions(+), 166 deletions(-) create mode 100644 spec/models/bitcoin/address_spec.rb diff --git a/adapters/bitcoin/request.rb b/adapters/bitcoin/request.rb index 1214965..4afbe59 100644 --- a/adapters/bitcoin/request.rb +++ b/adapters/bitcoin/request.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'bigdecimal' + require 'digest' module Lighstorm @@ -15,7 +17,7 @@ def self.decode(uri) if uri[:params]['amount'] result[:amount] = { - millisatoshis: uri[:params]['amount'].to_f * 100_000_000_000.0 + millisatoshis: BigDecimal(uri[:params]['amount']) * BigDecimal('100000000000.0') } end diff --git a/controllers/bitcoin/address.rb b/controllers/bitcoin/address.rb index 3e2e73e..63e71b6 100644 --- a/controllers/bitcoin/address.rb +++ b/controllers/bitcoin/address.rb @@ -14,8 +14,8 @@ def new(code:) Model::Bitcoin::Address.new({ code: code }, components) end - def create(preview: false, &vcr) - Create.perform(components, preview: preview, &vcr) + def create(format: 'taproot', preview: false, &vcr) + Create.perform(components, format: format, preview: preview, &vcr) end end end diff --git a/controllers/bitcoin/address/actions/create.rb b/controllers/bitcoin/address/actions/create.rb index 352608e..69cab0e 100644 --- a/controllers/bitcoin/address/actions/create.rb +++ b/controllers/bitcoin/address/actions/create.rb @@ -10,45 +10,43 @@ module Controller module Bitcoin module Address module Create - def self.call(components, grpc_request) - { - at: Time.now, - new_address: components[:grpc].send(grpc_request[:service]).send( - grpc_request[:method], grpc_request[:params] - ).to_h - } - end + # https://bitcoin.design/guide/glossary/address/ + SPECIFICATIONS = { + # Taproot: pay-to-taproot (P2TR) + # TAPROOT_PUBKEY (4): A new type of Bitcoin address that comes with the Taproot upgrade. + # It improves privacy and supports more complex transactions (like smart contracts). + # Addresses usually start with "bc1p" (mainnet) or "tb1p" (testnet). + 'taproot' => :TAPROOT_PUBKEY, - def self.prepare + # SegWit: pay-to-witness-public-key-hash (P2WPKH) # WITNESS_PUBKEY_HASH (0) # A modern and efficient Bitcoin address type. It uses less space in transactions, # so it has lower fees. Addresses usually start with "bc1" (mainnet) or "tb1" (testnet). + 'segwit' => :WITNESS_PUBKEY_HASH, + # Script: pay-to-script-hash (P2SH) # NESTED_PUBKEY_HASH (1) # \A backward-compatible version of the modern address. It works with older wallets and # services but has slightly higher fees. Addresses usually start with "3" (mainnet) # or "2" (testnet). + 'script' => :NESTED_PUBKEY_HASH + }.freeze - # UNUSED_WITNESS_PUBKEY_HASH (2) - # An unused version of the modern address. It's like having a reserved spot for a future - # address, but it's not being used right now. - - # UNUSED_NESTED_PUBKEY_HASH (3) - # An unused version of the backward-compatible address. Similar to the unused modern - # address, it's a reserved spot for a future address but not in use currently. - - # TAPROOT_PUBKEY (4): A new type of Bitcoin address that comes with the Taproot upgrade. - # It improves privacy and supports more complex transactions (like smart contracts). - # Addresses usually start with "bc1p" (mainnet) or "tb1p" (testnet). + def self.call(components, grpc_request) + { + at: Time.now, + new_address: components[:grpc].send(grpc_request[:service]).send( + grpc_request[:method], grpc_request[:params] + ).to_h + } + end - # UNUSED_TAPROOT_PUBKEY (5) - # An unused version of the Taproot address. Like the other unused addresses, it's a - # reserved spot for a future address but not in use at the moment. + def self.prepare(format:) { service: :lightning, method: :new_address, params: { - type: :WITNESS_PUBKEY_HASH + type: SPECIFICATIONS[format] } } end @@ -71,8 +69,8 @@ def self.model(data, components) Model::Bitcoin::Address.new(data, components) end - def self.perform(components, preview: false, &vcr) - grpc_request = prepare + def self.perform(components, format: 'taproot', preview: false, &vcr) + grpc_request = prepare(format: format) return grpc_request if preview diff --git a/controllers/bitcoin/address/actions/pay.rb b/controllers/bitcoin/address/actions/pay.rb index 1842738..e1680d4 100644 --- a/controllers/bitcoin/address/actions/pay.rb +++ b/controllers/bitcoin/address/actions/pay.rb @@ -38,7 +38,7 @@ def self.prepare(address_code:, amount:, fee:, description:, required_confirmati params: { addr: address_code, amount: (amount[:millisatoshis].to_f / 1000.0).to_i, - sat_per_vbyte: fee[:satoshis_per_vitual_byte], + sat_per_vbyte: fee[:maximum][:satoshis_per_vitual_byte], min_confs: required_confirmations } } diff --git a/controllers/bitcoin/request.rb b/controllers/bitcoin/request.rb index c649dd4..c795584 100644 --- a/controllers/bitcoin/request.rb +++ b/controllers/bitcoin/request.rb @@ -14,9 +14,9 @@ module Request extend Impersonatable class DSL < Impersonatable::DSL - def create(address: nil, amount: nil, description: nil, message: nil, preview: false, &vcr) + def create(address: nil, format: 'taproot', amount: nil, description: nil, message: nil, preview: false, &vcr) if address.nil? - address_action = Address::Create.perform(components, preview: preview, &vcr) + address_action = Address::Create.perform(components, format: format, preview: preview, &vcr) return address_action if preview address = address_action.result.to_h diff --git a/controllers/lightning/invoice/decode.rb b/controllers/lightning/invoice/decode.rb index 4b28dd3..c059dcf 100644 --- a/controllers/lightning/invoice/decode.rb +++ b/controllers/lightning/invoice/decode.rb @@ -11,8 +11,10 @@ module Invoice module Decode def self.fetch(components, code) { - _code: code, - decode_pay_req: components[:grpc].lightning.decode_pay_req(pay_req: code).to_h + _code: code.sub('lightning:', ''), + decode_pay_req: components[:grpc].lightning.decode_pay_req( + pay_req: code.sub('lightning:', '') + ).to_h } end @@ -30,9 +32,9 @@ def self.transform(adapted) def self.data(components, code, &vcr) raw = if vcr.nil? - fetch(components, code.sub('lightning:', '')) + fetch(components, code) else - vcr.call(-> { fetch(components, code.sub('lightning:', '')) }) + vcr.call(-> { fetch(components, code) }) end adapted = adapt(raw) diff --git a/docs/README.md b/docs/README.md index ec38997..25e67cd 100644 --- a/docs/README.md +++ b/docs/README.md @@ -514,7 +514,7 @@ action = Lighstorm::Bitcoin::Request.decode( ).pay( amount: { millisatoshis: 5000000000000 }, description: 'Making a Donation', - fee: { satoshis_per_vitual_byte: 1 }, + fee: { maximum: { satoshis_per_vitual_byte: 1 } }, required_confirmations: 6, preview: true ) @@ -523,7 +523,7 @@ action = Lighstorm::Bitcoin::Request.decode( ```ruby action = Lighstorm::Bitcoin::Request.decode( 'bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?amount=50&label=Luke-Jr&message=Donation%20for%20project%20xyz' -).pay(fee: { satoshis_per_vitual_byte: 1 }) +).pay(fee: { maximum: { satoshis_per_vitual_byte: 1 } }) action.request action.response @@ -572,7 +572,7 @@ Lighstorm::Bitcoin::Address.new( code: 'bcrt1qq5gl3thf4ka93eluz0guweek9vmeyqyrck3py2' ).pay( amount: { millisatoshis: 250_000_000 }, - fee: { satoshis_per_vitual_byte: 4 }, + fee: { maximum: { satoshis_per_vitual_byte: 4 } }, preview: true ) @@ -580,7 +580,7 @@ action = Lighstorm::Bitcoin::Address.new( code: 'bcrt1qq5gl3thf4ka93eluz0guweek9vmeyqyrck3py2' ).pay( amount: { millisatoshis: 500_000_000 }, - fee: { satoshis_per_vitual_byte: 1 }, + fee: { maximum: { satoshis_per_vitual_byte: 1 } }, description: 'Wallet Withdrawal', required_confirmations: 6 ) @@ -870,6 +870,7 @@ invoice.state # https://github.com/lightning/bolts/blob/master/11-payment-encoding.md invoice.code # "lnbc20m1pv...qqdhhwkj" +invoice.uri # "lightning:lnbc20m1pv...qqdhhwkj" invoice.amount.millisatoshis @@ -935,6 +936,24 @@ invoice.secret.valid_proof?( ) # => true ``` +#### Decode + +```ruby +invoice = Lighstorm::Lightning::Invoice.decode('lnbc20m1pv...qqdhhwkj') + +invoice.amount.millisatoshis +invoice.description.memo +invoice.expires_at + +invoice.created_at +invoice.payable +invoice.secret.hash + +# https://github.com/lightning/bolts/blob/master/11-payment-encoding.md +invoice.code # "lnbc20m1pv...qqdhhwkj" +invoice.uri # "lightning:lnbc20m1pv...qqdhhwkj" +``` + #### Pay [Understanding Lightning Invoices](https://docs.lightning.engineering/the-lightning-network/payment-lifecycle/understanding-lightning-invoices) diff --git a/models/bitcoin/address.rb b/models/bitcoin/address.rb index 1173fd7..e06b4f6 100644 --- a/models/bitcoin/address.rb +++ b/models/bitcoin/address.rb @@ -32,17 +32,57 @@ def pay( ) end + def specification + @specification ||= infer_specification(code) + end + def to_h if created_at || _key { _key: _key, created_at: created_at, - code: code + code: code, + specification: specification.to_h } else - { code: code } + { code: code, specification: specification.to_h } end end + + private + + def infer_specification(address_code) + data = case address_code + when /^(bc1p|tb1p|bcrt1p)/ + { format: 'taproot', bip: 341, code: 'P2TR' } + when /^(bc1q|tb1q|bcrt1q)/ + { format: 'segwit', bip: 173, code: 'P2WPKH' } + when /^[23]/ + { format: 'script', bip: 16, code: 'P2SH' } + when /^[mn1]/ + { format: 'legacy', bip: nil, code: 'P2PKH' } + else + { format: 'unknown', bip: nil, code: nil } + end + + Struct.new(:data) do + def format + data[:format] + end + + def code + data[:code] + end + + def bip + data[:bip] + end + + def to_h + { format: format, code: code, bip: bip } + end + end.new(data) + end end end end diff --git a/models/bitcoin/request.rb b/models/bitcoin/request.rb index a7ecca2..46a913b 100644 --- a/models/bitcoin/request.rb +++ b/models/bitcoin/request.rb @@ -25,7 +25,7 @@ def address end def amount - @amount ||= @data[:amount] ? Satoshis.new(millisatoshis: @data[:amount][:millisatoshis]) : nil + @amount ||= @data[:amount] && @data[:amount][:millisatoshis] ? Satoshis.new(millisatoshis: @data[:amount][:millisatoshis]) : nil end def uri @@ -35,7 +35,7 @@ def uri params = {} - params[:amount] = amount.bitcoins if amount + params[:amount] = ('%.20f' % amount.bitcoins).sub(/\.?0+$/, '') if amount params[:label] = description if description params[:message] = message if message diff --git a/models/lightning/invoice.rb b/models/lightning/invoice.rb index 1502f86..0ba80c9 100644 --- a/models/lightning/invoice.rb +++ b/models/lightning/invoice.rb @@ -28,6 +28,10 @@ def initialize(data, components) @code = data[:code] end + def uri + @uri ||= "lightning:#{@code}" + end + def payment if payable != 'once' || @data[:payments].size > 1 raise InvoiceMayHaveMultiplePaymentsError, "payable: #{payable}, payments: #{@data[:payments].size.size}" @@ -77,6 +81,7 @@ def to_h payable: payable, state: state, code: code, + uri: uri, amount: amount&.to_h, received: received&.to_h, description: description.to_h, diff --git a/spec/controllers/bitcoin/address/actions/create_spec.rb b/spec/controllers/bitcoin/address/actions/create_spec.rb index fd9e0ca..02665e3 100644 --- a/spec/controllers/bitcoin/address/actions/create_spec.rb +++ b/spec/controllers/bitcoin/address/actions/create_spec.rb @@ -11,14 +11,14 @@ context 'gradual' do it 'flows' do - request = described_class.prepare + request = described_class.prepare(format: 'taproot') expect(request).to eq( { service: :lightning, method: :new_address, params: { - type: :WITNESS_PUBKEY_HASH + type: :TAPROOT_PUBKEY } } ) @@ -39,19 +39,19 @@ expect(adapted_to_h).to eq( { _source: :new_address, - _key: 'ed0248d083d5a9f7de6fb286f16a559525981fdc597be74da9e66a90485bc8dc', - created_at: '2023-04-01 23:17:05 UTC', - code: 'bcrt1qd6zttxeyr4xwn7skwrmkvkfyx3emexs5lckewx' } + _key: 'e36bb402a48cefa9308e2850bd3bd8916c019a90aaf365eec36c84224c52f7ad', + code: 'bcrt1peunl67vvh76fmsdfkgyhgpmf7vf7duduw6sxulzy0xrvrsf7dxrqgstrn5', + created_at: '2023-04-04 12:41:45 UTC' } ) model = described_class.model(adapted, Lighstorm::Controller::Bitcoin::Address.components) expect(model._key.size).to eq(64) - expect(model.created_at.utc.to_s).to eq('2023-04-01 23:17:05 UTC') - expect(model.code).to eq('bcrt1qd6zttxeyr4xwn7skwrmkvkfyx3emexs5lckewx') + expect(model.created_at.utc.to_s).to eq('2023-04-04 12:41:45 UTC') + expect(model.code).to eq('bcrt1peunl67vvh76fmsdfkgyhgpmf7vf7duduw6sxulzy0xrvrsf7dxrqgstrn5') Contract.expect( - model.to_h, '19b9b21feb645a6717f6cf43a98207f81d798c9b9d4b6523f961053d41dc746a' + model.to_h, '8c2ebd984c979d5e504fa3786a5fa11de2f5952360806ee0e8e985be48e3ba84' ) do |actual, expected| expect(actual.hash).to eq(expected.hash) expect(actual.contract).to eq(expected.contract) @@ -72,7 +72,7 @@ service: :lightning, method: :new_address, params: { - type: :WITNESS_PUBKEY_HASH + type: :TAPROOT_PUBKEY } } ) @@ -88,17 +88,17 @@ end expect(action.response).to eq( - { address: 'bcrt1qhs2949xljflpxcl34ehyrwf56n75k6vj588mn9' } + { address: 'bcrt1pthpaqp4y30hxf5vh7y27z7yk3jh62htm9t7u3qce43dxtdqcuzssll03j4' } ) expect(action.result.class).to eq(Lighstorm::Model::Bitcoin::Address) expect(action.result._key.size).to eq(64) - expect(action.result.created_at.utc.to_s).to eq('2023-04-01 23:19:57 UTC') - expect(action.result.code).to eq('bcrt1qhs2949xljflpxcl34ehyrwf56n75k6vj588mn9') + expect(action.result.created_at.utc.to_s).to eq('2023-04-04 12:43:43 UTC') + expect(action.result.code).to eq('bcrt1pthpaqp4y30hxf5vh7y27z7yk3jh62htm9t7u3qce43dxtdqcuzssll03j4') Contract.expect( - action.to_h, '03cf73d9d2ea3741b5be4c03710ed7d912e6a1ae341ba1005d9021ec87fddadf' + action.to_h, '8490c5e8bf4f610cf62e323f42011daab114719631b193686ed509c0a33baa0b' ) do |actual, expected| expect(actual.hash).to eq(expected.hash) expect(actual.contract).to eq(expected.contract) @@ -107,4 +107,4 @@ end end end -end +end \ No newline at end of file diff --git a/spec/controllers/bitcoin/address/actions/pay_spec.rb b/spec/controllers/bitcoin/address/actions/pay_spec.rb index 4a89052..2670a0e 100644 --- a/spec/controllers/bitcoin/address/actions/pay_spec.rb +++ b/spec/controllers/bitcoin/address/actions/pay_spec.rb @@ -13,7 +13,7 @@ { address_code: 'bcrt1qq5gl3thf4ka93eluz0guweek9vmeyqyrck3py2', amount: { millisatoshis: 500_000_000 }, - fee: { satoshis_per_vitual_byte: 1 }, + fee: { maximum: { satoshis_per_vitual_byte: 1 } }, description: 'Wallet Withdrawal', required_confirmations: 1 } @@ -123,7 +123,7 @@ expect(model.description).to eq('Wallet Withdrawal') Contract.expect( - model.to_h, '43ba1c30001bdee50a01d6b61006444535ff8073cb25cd31dd690ecdaed44ca1' + model.to_h, '5961259e87ed775b8ec88246b467269bb06f19db5bddcf2413d8382957db553e' ) do |actual, expected| expect(actual.hash).to eq(expected.hash) expect(actual.contract).to eq(expected.contract) @@ -138,7 +138,7 @@ Lighstorm::Controller::Bitcoin::Address.components, address_code: 'bcrt1qq5gl3thf4ka93eluz0guweek9vmeyqyrck3py2', amount: { millisatoshis: 500_000_000 }, - fee: { satoshis_per_vitual_byte: 1 }, + fee: { maximum: { satoshis_per_vitual_byte: 1 } }, description: 'Wallet Withdrawal', required_confirmations: 1, preview: true @@ -164,7 +164,7 @@ Lighstorm::Controller::Bitcoin::Address.components, address_code: 'bcrt1qq5gl3thf4ka93eluz0guweek9vmeyqyrck3py2', amount: { millisatoshis: 500_000_000 }, - fee: { satoshis_per_vitual_byte: 1 }, + fee: { maximum: { satoshis_per_vitual_byte: 1 } }, description: 'Wallet Withdrawal', required_confirmations: 1 ) do |fn, from = :fetch| @@ -172,20 +172,20 @@ end expect(action.response).to eq( - { txid: '6f5f1b781e2c7d676702abdbc9726e88f12c75d606751b10ed97811b25f9a00d' } + { txid: '9068b5e021e2259fe9461a4e8108d3b8dc4494d72ff2684bda20f0630c1623d9' } ) expect(action.result.class).to eq(Lighstorm::Model::Bitcoin::Transaction) expect(action.result._key.size).to eq(64) - expect(action.result.at.utc.to_s).to eq('2023-04-01 23:31:10 UTC') + expect(action.result.at.utc.to_s).to eq('2023-04-03 22:14:12 UTC') expect(action.result.hash).to eq(action.response[:txid]) expect(action.result.amount.millisatoshis).to eq(-500_000_000) expect(action.result.fee.millisatoshis).to eq(154_000) expect(action.result.description).to eq('Wallet Withdrawal') Contract.expect( - action.to_h, '3974376ae14a4685ef4612c453dd839a6d020897b70fcb91518411acc9297a45' + action.to_h, '890b67ce4c074ee9aaa9a67b78ad5ff2275ce17e3493ce7000f6986fcb6810cc' ) do |actual, expected| expect(actual.hash).to eq(expected.hash) expect(actual.contract).to eq(expected.contract) diff --git a/spec/controllers/bitcoin/request/decode_spec.rb b/spec/controllers/bitcoin/request/decode_spec.rb index 296b93d..f2a3b51 100644 --- a/spec/controllers/bitcoin/request/decode_spec.rb +++ b/spec/controllers/bitcoin/request/decode_spec.rb @@ -33,14 +33,15 @@ expect(model.uri).to eq('bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?amount=50&label=Luke-Jr&message=Donation+for+project+xyz') expect(model.to_h).to eq( - { - _key: 'cfe53fceea2fb1ce8a0d40584e1e595bbc2182e445fd726df0ac80d288bb2952', - address: { code: '175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W' }, + { _key: 'cfe53fceea2fb1ce8a0d40584e1e595bbc2182e445fd726df0ac80d288bb2952', + address: { + code: '175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W', + specification: { format: 'legacy', code: 'P2PKH', bip: nil } + }, + uri: 'bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?amount=50&label=Luke-Jr&message=Donation+for+project+xyz', amount: { millisatoshis: 5_000_000_000_000 }, description: 'Luke-Jr', - message: 'Donation for project xyz', - uri: 'bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?amount=50&label=Luke-Jr&message=Donation+for+project+xyz' - } + message: 'Donation for project xyz' } ) end end @@ -70,11 +71,12 @@ expect(model.uri).to eq('bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W') expect(model.to_h).to eq( - { - _key: '416b10c7da18c34ae1444db9363e382e0db29b8323be07c34f312506425b834c', - address: { code: '175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W' }, - uri: 'bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W' - } + { _key: '416b10c7da18c34ae1444db9363e382e0db29b8323be07c34f312506425b834c', + address: { + code: '175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W', + specification: { format: 'legacy', code: 'P2PKH', bip: nil } + }, + uri: 'bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W' } ) end end @@ -104,11 +106,12 @@ expect(model.uri).to eq('bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W') expect(model.to_h).to eq( - { - _key: '416b10c7da18c34ae1444db9363e382e0db29b8323be07c34f312506425b834c', - address: { code: '175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W' }, - uri: 'bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W' - } + { _key: '416b10c7da18c34ae1444db9363e382e0db29b8323be07c34f312506425b834c', + address: { + code: '175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W', + specification: { format: 'legacy', code: 'P2PKH', bip: nil } + }, + uri: 'bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W' } ) end end @@ -139,14 +142,51 @@ expect(model.uri).to eq('bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?amount=50&label=Luke-Jr&message=Donation+for+project+xyz') expect(model.to_h).to eq( - { - _key: 'cfe53fceea2fb1ce8a0d40584e1e595bbc2182e445fd726df0ac80d288bb2952', - address: { code: '175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W' }, + { _key: 'cfe53fceea2fb1ce8a0d40584e1e595bbc2182e445fd726df0ac80d288bb2952', + address: { + code: '175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W', + specification: { format: 'legacy', code: 'P2PKH', bip: nil } + }, + uri: 'bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?amount=50&label=Luke-Jr&message=Donation+for+project+xyz', amount: { millisatoshis: 5_000_000_000_000 }, description: 'Luke-Jr', - message: 'Donation for project xyz', - uri: 'bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?amount=50&label=Luke-Jr&message=Donation+for+project+xyz' - } + message: 'Donation for project xyz' } + ) + end + end + + context 'precision' do + let(:uri) do + 'bitcoin:bcrt1qlkuvzpzug6v2xl93sdkm25qcxetddyzx6cz9yj?amount=0.00000000001' + end + + it 'models' do + data = described_class.data(uri: uri) + + expect(data).to eq( + { _source: :decode, + _key: '0a250ed2eb2daccbb6c897e807a1da44f70ec30f214c8f325946d06193cf26b9', + address: { code: 'bcrt1qlkuvzpzug6v2xl93sdkm25qcxetddyzx6cz9yj' }, + amount: { millisatoshis: 1 } } + ) + + model = described_class.model(data, Lighstorm::Controller::Bitcoin::Request.components) + + expect(model.address.code).to eq('bcrt1qlkuvzpzug6v2xl93sdkm25qcxetddyzx6cz9yj') + expect(model.amount.millisatoshis).to eq(1) + expect(model.amount.bitcoins).to eq(0.00000000001) + expect(model.description).to be_nil + expect(model.message).to be_nil + expect(model.uri).to eq('bitcoin:bcrt1qlkuvzpzug6v2xl93sdkm25qcxetddyzx6cz9yj?amount=0.00000000001') + + expect(model.to_h).to eq( + { _key: '0a250ed2eb2daccbb6c897e807a1da44f70ec30f214c8f325946d06193cf26b9', + address: { + code: 'bcrt1qlkuvzpzug6v2xl93sdkm25qcxetddyzx6cz9yj', + specification: { format: 'segwit', code: 'P2WPKH', bip: 173 } + }, + uri: 'bitcoin:bcrt1qlkuvzpzug6v2xl93sdkm25qcxetddyzx6cz9yj?amount=0.00000000001', + amount: { millisatoshis: 1 } } ) end end diff --git a/spec/controllers/lightning/invoice/actions/create_spec.rb b/spec/controllers/lightning/invoice/actions/create_spec.rb index c54b0cd..c96b735 100644 --- a/spec/controllers/lightning/invoice/actions/create_spec.rb +++ b/spec/controllers/lightning/invoice/actions/create_spec.rb @@ -98,6 +98,7 @@ payable: 'indefinitely', state: 'open', code: 'lnbcrt1pjz35aqpp55rul98whw4jlp8vehqwctzr3fzp67x5s8zaweypstad0j0tgh33qdqdg3hkuct5d9hkucqzpgxqyz5vqsp5cej84s93fnrn725avqhrcvs4y6720ya3pka5raupf9uhmccr30lq9q8pqqqssq0rwdfklxcvah27ljnx88f97swen9lmgm3fm726rjuz6fgvaljdm4lc5jmhlvs2z3ht4mxaf7xcyga9d3krhyn40v7dfw5pgmwez4vkspt7t4dw', + uri: 'lightning:lnbcrt1pjz35aqpp55rul98whw4jlp8vehqwctzr3fzp67x5s8zaweypstad0j0tgh33qdqdg3hkuct5d9hkucqzpgxqyz5vqsp5cej84s93fnrn725avqhrcvs4y6720ya3pka5raupf9uhmccr30lq9q8pqqqssq0rwdfklxcvah27ljnx88f97swen9lmgm3fm726rjuz6fgvaljdm4lc5jmhlvs2z3ht4mxaf7xcyga9d3krhyn40v7dfw5pgmwez4vkspt7t4dw', amount: nil, received: nil, description: { memo: 'Donation', hash: nil }, @@ -153,13 +154,12 @@ payable: 'indefinitely', state: 'open', code: 'lnbcrt1pjz35aqpp55rul98whw4jlp8vehqwctzr3fzp67x5s8zaweypstad0j0tgh33qdqdg3hkuct5d9hkucqzpgxqyz5vqsp5cej84s93fnrn725avqhrcvs4y6720ya3pka5raupf9uhmccr30lq9q8pqqqssq0rwdfklxcvah27ljnx88f97swen9lmgm3fm726rjuz6fgvaljdm4lc5jmhlvs2z3ht4mxaf7xcyga9d3krhyn40v7dfw5pgmwez4vkspt7t4dw', + uri: 'lightning:lnbcrt1pjz35aqpp55rul98whw4jlp8vehqwctzr3fzp67x5s8zaweypstad0j0tgh33qdqdg3hkuct5d9hkucqzpgxqyz5vqsp5cej84s93fnrn725avqhrcvs4y6720ya3pka5raupf9uhmccr30lq9q8pqqqssq0rwdfklxcvah27ljnx88f97swen9lmgm3fm726rjuz6fgvaljdm4lc5jmhlvs2z3ht4mxaf7xcyga9d3krhyn40v7dfw5pgmwez4vkspt7t4dw', amount: nil, received: nil, description: { memo: 'Donation', hash: nil }, - secret: { - proof: nil, - hash: 'a0f9f29dd77565f09d99b81d8588714883af1a9038baec90305f5af93d68bc62' - }, + secret: { proof: nil, + hash: 'a0f9f29dd77565f09d99b81d8588714883af1a9038baec90305f5af93d68bc62' }, payments: nil } ) @@ -178,7 +178,7 @@ end Contract.expect( - action.to_h, '1a249a948e6df5814a0994b2ff101089e49cabf07ff28bf8bf7f205c5d9abc85' + action.to_h, '64f4bc062f4e1a1fae57980f7156f5288abc2837ce5580a3457e59c22f91d802' ) do |actual, expected| expect(actual.hash).to eq(expected.hash) @@ -197,7 +197,8 @@ received: 'Nil', secret: { hash: 'String:50+', proof: 'Nil' }, settled_at: 'Nil', - state: 'String:0..10' } } + state: 'String:0..10', + uri: 'String:50+' } } ) end end @@ -292,7 +293,7 @@ model = described_class.model(data, Lighstorm::Controller::Lightning::Invoice.components) Contract.expect( - model.to_h, '001dac92137661c295e0fa816a4c3b088a8196257980ae93f1b74e6af4e371bc' + model.to_h, '30f8f5957e7ff3e2efb096f030389c3e3cc35e060c9f8d7f35a153429b2f2ff5' ) do |actual, expected| expect(actual.hash).to eq(expected.hash) @@ -308,7 +309,8 @@ received: 'Nil', secret: { hash: 'String:50+', proof: 'String:50+' }, settled_at: 'Nil', - state: 'String:0..10' } + state: 'String:0..10', + uri: 'String:50+' } ) end end @@ -352,7 +354,7 @@ expect(action.result.class).to eq(Lighstorm::Model::Lightning::Invoice) Contract.expect( - action.to_h, 'f916b13ca90b63c39c8228dd88cb502ef8e2c24a69c77f8eff9495767de9d91b' + action.to_h, '27a60ae7a7ad33936d1d1bfbc54589a56b7c6cb1d4b7dd6fc120bed606a16423' ) do |actual, expected| expect(actual.hash).to eq(expected.hash) @@ -371,7 +373,8 @@ received: 'Nil', secret: { hash: 'String:50+', proof: 'String:50+' }, settled_at: 'Nil', - state: 'String:0..10' } } + state: 'String:0..10', + uri: 'String:50+' } } ) end end diff --git a/spec/controllers/lightning/invoice/actions/pay_spec.rb b/spec/controllers/lightning/invoice/actions/pay_spec.rb index b20b512..76f6b1c 100644 --- a/spec/controllers/lightning/invoice/actions/pay_spec.rb +++ b/spec/controllers/lightning/invoice/actions/pay_spec.rb @@ -196,7 +196,7 @@ expect(action.result.hops.last.amount.millisatoshis).to eq(params[:amount][:millisatoshis]) Contract.expect( - action.to_h, '0e86079201a777a5b22557498ee6103b5ddf62241e7b73546d7e84aa9a925158' + action.to_h, 'ba1ba34d7fc8335e8bce0fc3de33c78c8385435d7441aaca8e94f67f38265d0b' ) do |actual, expected| expect(actual.hash).to eq(expected.hash) expect(actual.contract).to eq(expected.contract) @@ -278,7 +278,7 @@ expect(action.result.hops.last.amount.millisatoshis).to eq(params[:amount][:millisatoshis]) Contract.expect( - action.to_h, '2179a0240df1c3c5cdb20c84bf2991e469274670fbb4d87c013c2e2fbc0b9025' + action.to_h, '3884b5859c10462b30b48aaca5cd6a555179f786d772a651ad672edec21e523e' ) do |actual, expected| expect(actual.hash).to eq(expected.hash) expect(actual.contract).to eq(expected.contract) diff --git a/spec/controllers/lightning/invoice_spec.rb b/spec/controllers/lightning/invoice_spec.rb index 6840e65..26140e6 100644 --- a/spec/controllers/lightning/invoice_spec.rb +++ b/spec/controllers/lightning/invoice_spec.rb @@ -81,4 +81,118 @@ end end end + + describe '.decode' do + context 'code' do + let(:code) do + 'lnbcrt10n1pjzrz5qpp52v9dct3xju8dq2e58r5tydnqaa58d874q6e2zvv2qzpcexttxxysdq2gdhkven9v5cqzpgxqyz5vqsp54zks4d32d6vq7rtzunaafmmyjre9erlkxmcuwe94sz6xme70h5ks9qyyssqp2f6c4434q8ehaxz9lplgpey6zrz292zxqdldlz9awdhvrmtzwhy739xcwq76qprvlkkdc84yupeh8lfhck8c44yjpdjnjddq7672rgp36mffs' + end + + it 'decodes' do + invoice = described_class.decode(code) do |fetch| + VCR.tape.replay("Controller::Lightning::Invoice.decode/#{code}") do + fetch.call + end + end + + expect(invoice.created_at.utc.to_s).to eq('2023-03-27 12:22:24 UTC') + expect(invoice.expires_at.utc.to_s).to eq('2023-03-28 12:22:24 UTC') + expect(invoice.settled_at).to be_nil + + expect(invoice.state).to be_nil + expect(invoice.payable).to eq('once') + expect(invoice.description.memo).to eq('Coffee') + expect(invoice.description.hash).to be_nil + + expect(invoice.secret.hash).to eq('530adc2e26970ed02b3438e8b23660ef68769fd506b2a1318a00838c996b3189') + expect(invoice.secret.proof).to be_nil + expect(invoice.secret.preimage).to be_nil + expect(invoice.amount.millisatoshis).to eq(1000) + + expect(invoice.code).to eq(code) + expect(invoice.uri).to eq("lightning:#{code}") + + invoice_to_h = invoice.to_h + + expect(invoice_to_h[:created_at].class).to eq(Time) + expect(invoice_to_h[:expires_at].class).to eq(Time) + + invoice_to_h[:created_at] = invoice_to_h[:created_at].utc.to_s + invoice_to_h[:expires_at] = invoice_to_h[:expires_at].utc.to_s + + expect(invoice_to_h).to eq( + { _key: '6ec79fb8a553b3147b5c63ee86f8a0973f6f64e3bf58410d6c3e9517d85fac1d', + created_at: '2023-03-27 12:22:24 UTC', + expires_at: '2023-03-28 12:22:24 UTC', + settled_at: nil, + payable: 'once', + state: nil, + code: 'lnbcrt10n1pjzrz5qpp52v9dct3xju8dq2e58r5tydnqaa58d874q6e2zvv2qzpcexttxxysdq2gdhkven9v5cqzpgxqyz5vqsp54zks4d32d6vq7rtzunaafmmyjre9erlkxmcuwe94sz6xme70h5ks9qyyssqp2f6c4434q8ehaxz9lplgpey6zrz292zxqdldlz9awdhvrmtzwhy739xcwq76qprvlkkdc84yupeh8lfhck8c44yjpdjnjddq7672rgp36mffs', + uri: 'lightning:lnbcrt10n1pjzrz5qpp52v9dct3xju8dq2e58r5tydnqaa58d874q6e2zvv2qzpcexttxxysdq2gdhkven9v5cqzpgxqyz5vqsp54zks4d32d6vq7rtzunaafmmyjre9erlkxmcuwe94sz6xme70h5ks9qyyssqp2f6c4434q8ehaxz9lplgpey6zrz292zxqdldlz9awdhvrmtzwhy739xcwq76qprvlkkdc84yupeh8lfhck8c44yjpdjnjddq7672rgp36mffs', + amount: { millisatoshis: 1000 }, + received: nil, + description: { memo: 'Coffee', hash: nil }, + secret: { proof: nil, hash: '530adc2e26970ed02b3438e8b23660ef68769fd506b2a1318a00838c996b3189' }, + payments: nil } + ) + end + end + + context 'uri' do + let(:code) do + 'lnbcrt10n1pjzrz5qpp52v9dct3xju8dq2e58r5tydnqaa58d874q6e2zvv2qzpcexttxxysdq2gdhkven9v5cqzpgxqyz5vqsp54zks4d32d6vq7rtzunaafmmyjre9erlkxmcuwe94sz6xme70h5ks9qyyssqp2f6c4434q8ehaxz9lplgpey6zrz292zxqdldlz9awdhvrmtzwhy739xcwq76qprvlkkdc84yupeh8lfhck8c44yjpdjnjddq7672rgp36mffs' + end + + let(:uri) { "lightning:#{code}" } + + it 'decodes' do + invoice = described_class.decode(uri) do |fetch| + VCR.tape.replay("Controller::Lightning::Invoice.decode/#{uri}") do + fetch.call + end + end + + expect(invoice.created_at.utc.to_s).to eq('2023-03-27 12:22:24 UTC') + expect(invoice.expires_at.utc.to_s).to eq('2023-03-28 12:22:24 UTC') + expect(invoice.settled_at).to be_nil + + expect(invoice.state).to be_nil + expect(invoice.payable).to eq('once') + expect(invoice.description.memo).to eq('Coffee') + expect(invoice.description.hash).to be_nil + + expect(invoice.secret.hash).to eq('530adc2e26970ed02b3438e8b23660ef68769fd506b2a1318a00838c996b3189') + expect(invoice.secret.proof).to be_nil + expect(invoice.secret.preimage).to be_nil + expect(invoice.amount.millisatoshis).to eq(1000) + + expect(invoice.code).to eq(code) + expect(invoice.uri).to eq("lightning:#{code}") + + invoice_to_h = invoice.to_h + + expect(invoice_to_h[:created_at].class).to eq(Time) + expect(invoice_to_h[:expires_at].class).to eq(Time) + + invoice_to_h[:created_at] = invoice_to_h[:created_at].utc.to_s + invoice_to_h[:expires_at] = invoice_to_h[:expires_at].utc.to_s + + expect(invoice_to_h).to eq( + { _key: 'fbc0a5e5ac0f759a2ba3b5126806b66161fcae72213b16fd4a57e68274fda622', + created_at: '2023-03-27 12:22:24 UTC', + expires_at: '2023-03-28 12:22:24 UTC', + settled_at: nil, + payable: 'once', + state: nil, + code: 'lnbcrt10n1pjzrz5qpp52v9dct3xju8dq2e58r5tydnqaa58d874q6e2zvv2qzpcexttxxysdq2gdhkven9v5cqzpgxqyz5vqsp54zks4d32d6vq7rtzunaafmmyjre9erlkxmcuwe94sz6xme70h5ks9qyyssqp2f6c4434q8ehaxz9lplgpey6zrz292zxqdldlz9awdhvrmtzwhy739xcwq76qprvlkkdc84yupeh8lfhck8c44yjpdjnjddq7672rgp36mffs', + uri: 'lightning:lnbcrt10n1pjzrz5qpp52v9dct3xju8dq2e58r5tydnqaa58d874q6e2zvv2qzpcexttxxysdq2gdhkven9v5cqzpgxqyz5vqsp54zks4d32d6vq7rtzunaafmmyjre9erlkxmcuwe94sz6xme70h5ks9qyyssqp2f6c4434q8ehaxz9lplgpey6zrz292zxqdldlz9awdhvrmtzwhy739xcwq76qprvlkkdc84yupeh8lfhck8c44yjpdjnjddq7672rgp36mffs', + amount: { millisatoshis: 1000 }, + received: nil, + description: { memo: 'Coffee', hash: nil }, + secret: { proof: nil, hash: '530adc2e26970ed02b3438e8b23660ef68769fd506b2a1318a00838c996b3189' }, + payments: nil } + ) + end + end + end end diff --git a/spec/models/bitcoin/address_spec.rb b/spec/models/bitcoin/address_spec.rb new file mode 100644 index 0000000..eb7d0e9 --- /dev/null +++ b/spec/models/bitcoin/address_spec.rb @@ -0,0 +1,213 @@ +# frozen_string_literal: true + +require 'json' + +require_relative '../../../models/bitcoin/address' + +RSpec.describe Lighstorm::Model::Bitcoin::Address do + describe 'new' do + context 'mainnet' do + context 'taproot' do + let(:code) { 'bc1pd5jgrk4k0qmrl3ddr0qrap0nrafrdydpsznq9v3a5ny33fz6tklsqut72a' } + + it 'infers' do + address = described_class.new({ code: code }, nil) + expect(address.code).to eq(code) + expect(address.specification.format).to eq('taproot') + expect(address.specification.bip).to eq(341) + expect(address.specification.code).to eq('P2TR') + expect(address.specification.to_h).to eq( + { format: 'taproot', code: 'P2TR', bip: 341 } + ) + + expect(address.to_h).to eq( + { code: 'bc1pd5jgrk4k0qmrl3ddr0qrap0nrafrdydpsznq9v3a5ny33fz6tklsqut72a', + specification: { bip: 341, code: 'P2TR', format: 'taproot' } } + ) + end + end + + context 'segwit' do + let(:code) { 'bc1q99rjmzg9wgt80p2jt9540e7hyv4n2z0t5037rq' } + + it 'infers' do + address = described_class.new({ code: code }, nil) + expect(address.code).to eq(code) + expect(address.specification.format).to eq('segwit') + expect(address.specification.bip).to eq(173) + expect(address.specification.code).to eq('P2WPKH') + expect(address.specification.to_h).to eq( + { format: 'segwit', code: 'P2WPKH', bip: 173 } + ) + + expect(address.to_h).to eq( + { code: 'bc1q99rjmzg9wgt80p2jt9540e7hyv4n2z0t5037rq', + specification: { bip: 173, code: 'P2WPKH', format: 'segwit' } } + ) + end + end + + context 'script' do + let(:code) { '3DfDoqRCYznGZtPLbtJSvk2VoqPk1if9Eq' } + + it 'infers' do + address = described_class.new({ code: code }, nil) + expect(address.code).to eq(code) + expect(address.specification.format).to eq('script') + expect(address.specification.bip).to eq(16) + expect(address.specification.code).to eq('P2SH') + expect(address.specification.to_h).to eq( + { format: 'script', code: 'P2SH', bip: 16 } + ) + + expect(address.to_h).to eq( + { code: '3DfDoqRCYznGZtPLbtJSvk2VoqPk1if9Eq', + specification: { bip: 16, code: 'P2SH', format: 'script' } } + ) + end + end + + context 'legacy' do + let(:code) { '1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2' } + + it 'infers' do + address = described_class.new({ code: code }, nil) + expect(address.code).to eq(code) + expect(address.specification.format).to eq('legacy') + expect(address.specification.bip).to be_nil + expect(address.specification.code).to eq('P2PKH') + expect(address.specification.to_h).to eq( + { format: 'legacy', code: 'P2PKH', bip: nil } + ) + + expect(address.to_h).to eq( + { code: '1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2', + specification: { bip: nil, code: 'P2PKH', format: 'legacy' } } + ) + end + end + + context 'unknown' do + let(:code) { 'lorem' } + + it 'infers' do + address = described_class.new({ code: code }, nil) + expect(address.code).to eq(code) + expect(address.specification.format).to eq('unknown') + expect(address.specification.bip).to be_nil + expect(address.specification.code).to be_nil + expect(address.specification.to_h).to eq( + { format: 'unknown', code: nil, bip: nil } + ) + + expect(address.to_h).to eq( + { code: 'lorem', + specification: { bip: nil, code: nil, format: 'unknown' } } + ) + end + end + end + + context 'regtest' do + context 'taproot' do + let(:code) { 'bcrt1pdsnzpdjmetxmpp5tw769x3htjpx3wvkeflnut7knwmxlkkwe3svsvgwfht' } + + it 'infers' do + address = described_class.new({ code: code }, nil) + expect(address.code).to eq(code) + expect(address.specification.format).to eq('taproot') + expect(address.specification.bip).to eq(341) + expect(address.specification.code).to eq('P2TR') + expect(address.specification.to_h).to eq( + { format: 'taproot', code: 'P2TR', bip: 341 } + ) + + expect(address.to_h).to eq( + { code: 'bcrt1pdsnzpdjmetxmpp5tw769x3htjpx3wvkeflnut7knwmxlkkwe3svsvgwfht', + specification: { bip: 341, code: 'P2TR', format: 'taproot' } } + ) + end + end + + context 'segwit' do + let(:code) { 'bcrt1qmzauakqp7wqae3gyf0ctqhza5qxjtheghrcjna' } + + it 'infers' do + address = described_class.new({ code: code }, nil) + expect(address.code).to eq(code) + expect(address.specification.format).to eq('segwit') + expect(address.specification.bip).to eq(173) + expect(address.specification.code).to eq('P2WPKH') + expect(address.specification.to_h).to eq( + { format: 'segwit', code: 'P2WPKH', bip: 173 } + ) + + expect(address.to_h).to eq( + { code: 'bcrt1qmzauakqp7wqae3gyf0ctqhza5qxjtheghrcjna', + specification: { bip: 173, code: 'P2WPKH', format: 'segwit' } } + ) + end + end + + context 'script' do + let(:code) { '2N3oviLwazRtd5SaTSEkaULKbGny5yAHnqs' } + + it 'infers' do + address = described_class.new({ code: code }, nil) + expect(address.code).to eq(code) + expect(address.specification.format).to eq('script') + expect(address.specification.bip).to eq(16) + expect(address.specification.code).to eq('P2SH') + expect(address.specification.to_h).to eq( + { format: 'script', code: 'P2SH', bip: 16 } + ) + + expect(address.to_h).to eq( + { code: '2N3oviLwazRtd5SaTSEkaULKbGny5yAHnqs', + specification: { bip: 16, code: 'P2SH', format: 'script' } } + ) + end + end + + context 'legacy' do + let(:code) { '1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2' } + + it 'infers' do + address = described_class.new({ code: code }, nil) + expect(address.code).to eq(code) + expect(address.specification.format).to eq('legacy') + expect(address.specification.bip).to be_nil + expect(address.specification.code).to eq('P2PKH') + expect(address.specification.to_h).to eq( + { format: 'legacy', code: 'P2PKH', bip: nil } + ) + + expect(address.to_h).to eq( + { code: '1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2', + specification: { bip: nil, code: 'P2PKH', format: 'legacy' } } + ) + end + end + + context 'unknown' do + let(:code) { 'lorem' } + + it 'infers' do + address = described_class.new({ code: code }, nil) + expect(address.code).to eq(code) + expect(address.specification.format).to eq('unknown') + expect(address.specification.bip).to be_nil + expect(address.specification.code).to be_nil + expect(address.specification.to_h).to eq( + { format: 'unknown', code: nil, bip: nil } + ) + + expect(address.to_h).to eq( + { code: 'lorem', + specification: { bip: nil, code: nil, format: 'unknown' } } + ) + end + end + end + end +end diff --git a/spec/models/lightning/edges/payment_spec.rb b/spec/models/lightning/edges/payment_spec.rb index 2f1fb16..16d4e10 100644 --- a/spec/models/lightning/edges/payment_spec.rb +++ b/spec/models/lightning/edges/payment_spec.rb @@ -69,7 +69,7 @@ Lighstorm::Model::Satoshis.new(millisatoshis: 1000) end - let(:to_h_contract) { '36a6dee173c4783a3ac04c0d69e60cffb34e0fe33846d8bdb7c7967888e46fb2' } + let(:to_h_contract) { '3217bc4a312ea345ef69920d02d970e8fa50c96f5ba7a4dfe1ffa1035900483a' } it 'models' do expect(data[:meta][:calls][:decode_pay_req]).to be_nil @@ -184,7 +184,7 @@ Lighstorm::Model::Satoshis.new(millisatoshis: 1000) end - let(:to_h_contract) { '0de14ab266b071700de90c0aa8a2f027873b30d7d997f45343a76e5d91b5f68f' } + let(:to_h_contract) { 'f43403e538a822bd5cd59a738afbae1ac809371b18b3dfa99fe53091299d0e11' } it 'models' do expect(data[:meta][:calls].keys.sort).to eq( @@ -301,7 +301,7 @@ Lighstorm::Model::Satoshis.new(millisatoshis: 150_000) end - let(:to_h_contract) { '5447edbf9384e7366a4c74283d324be2aa8ef390c44fcee365d17307bd0f43c1' } + let(:to_h_contract) { 'ba9510fe34b2ef211ba29640607732262de017cc816f75858cf314c5b762f139' } it 'models' do expect(payment._key.size).to eq(64) @@ -400,7 +400,7 @@ Lighstorm::Model::Satoshis.new(millisatoshis: 3_050_000_000) end - let(:to_h_contract) { '82049dafe05d8c5ddb3511538fc19af36d1f9ca4f4a91e8d99d41efd11eaac30' } + let(:to_h_contract) { '28bb035462393541d0da8c2d774dcab7649afd3fa8ab6b5ef16fc352dbe921f7' } it 'models' do expect(payment._key.size).to eq(64) @@ -497,7 +497,7 @@ Lighstorm::Model::Satoshis.new(millisatoshis: 137_000) end - let(:to_h_contract) { '744a266bf986652626047c64f22d26e6902cb10ebb93978c3a9338643a73cf24' } + let(:to_h_contract) { '483a342641ba3fcb3876aa57aa614a26e415f6bd270bcd494cc4c1f69cb1c8d8' } it 'models' do expect(payment._key.size).to eq(64) @@ -598,7 +598,7 @@ Lighstorm::Model::Satoshis.new(millisatoshis: 130_000_000) end - let(:to_h_contract) { 'a5560d0326bf693487f32bfa81e3388c76724330736e136186b78e69b56a6ff0' } + let(:to_h_contract) { '0cdf88ab69e8175194cfbb318735b4f1f1e0aea050519a79f9ffd2b1b3c75140' } it 'models' do expect(payment._key.size).to eq(64) @@ -869,7 +869,7 @@ Lighstorm::Model::Satoshis.new(millisatoshis: 1000) end - let(:to_h_contract) { '5faff4c86ce94b6f06af506fa564da999640f9909ecfb4745a3a3bbbb1446f07' } + let(:to_h_contract) { 'b8be3c41f11bbc9ad36134aac1df84ca5d42e36320989b09bbbf47e763469673' } it 'models' do expect(payment._key.size).to eq(64) @@ -983,7 +983,7 @@ Lighstorm::Model::Satoshis.new(millisatoshis: 1210) end - let(:to_h_contract) { '4a98877e35774b817e887922ccfb86c5d6ac7ebb7b18749077922886741542be' } + let(:to_h_contract) { 'f1a195b3b388508de2c0a95266ce6f19211da0f0f34e6ebc77077f948024dcda' } it 'models' do expect(payment._key.size).to eq(64) @@ -1096,7 +1096,7 @@ Lighstorm::Model::Satoshis.new(millisatoshis: 1121) end - let(:to_h_contract) { 'bf98664158b4c62929000d2df2dc3d2140f1b629b372306498b0d13689fe4a6e' } + let(:to_h_contract) { 'ff5d8a35a7ab1b52c7f5163e83991e5a080be2154e98537fd5af3056da8401ee' } it 'models' do expect(payment._key.size).to eq(64) @@ -1209,7 +1209,7 @@ Lighstorm::Model::Satoshis.new(millisatoshis: 1278) end - let(:to_h_contract) { '5360e82c935621238eedc10fea167472af32cc795bba828f23b74ae8c4156c89' } + let(:to_h_contract) { '6bf71a8fa9b2e37b21bb15ea5947e8af1261395aa3dbde2d6309d4099da3315c' } it 'models' do expect(payment._key.size).to eq(64) @@ -1322,7 +1322,7 @@ Lighstorm::Model::Satoshis.new(millisatoshis: 3_000_000) end - let(:to_h_contract) { 'fb3562b218ee7deee56bcd12b54e5cdf697f8bb8a68c6fc3b7de5c95c1f5fc1c' } + let(:to_h_contract) { '4d3648d42df4fd7e7c25be5d7e6be3b5d49af1891cfcdb6a6d6e8f2f564230e2' } it 'models' do expect(payment._key.size).to eq(64) @@ -1447,7 +1447,7 @@ Lighstorm::Model::Satoshis.new(millisatoshis: 49_000) end - let(:to_h_contract) { 'e5e5b0901e2a5c1e401aea998b2b75bfd9f6d794747dc07521005b771c00954a' } + let(:to_h_contract) { '1bb501afdda727bd28649d2014e7ed1a106184dffc4df139d9f525b553fce2b3' } it 'models' do expect(payment._key.size).to eq(64) @@ -1563,7 +1563,7 @@ Lighstorm::Model::Satoshis.new(millisatoshis: 49_000) end - let(:to_h_contract) { 'e5e5b0901e2a5c1e401aea998b2b75bfd9f6d794747dc07521005b771c00954a' } + let(:to_h_contract) { '1bb501afdda727bd28649d2014e7ed1a106184dffc4df139d9f525b553fce2b3' } it 'models' do expect(payment._key.size).to eq(64) diff --git a/spec/models/lightning/invoice_spec.rb b/spec/models/lightning/invoice_spec.rb index 7bdcd5e..bce8ca9 100644 --- a/spec/models/lightning/invoice_spec.rb +++ b/spec/models/lightning/invoice_spec.rb @@ -55,7 +55,7 @@ expect(invoice.secret.hash).to eq('7dc0a651f241c5c940ae303338e96af942b7559009728e2ab046d8f6583419ba') Contract.expect( - invoice.to_h, '69d1cf28d755a38aa5e4024f30992fdf032c3741f325306405a028af853aebd6' + invoice.to_h, 'a6d33323703b4c469122201d93662d9cff0f6d720a0f9341a8edbf158b9a4e9f' ) do |actual, expected| expect(actual.hash).to eq(expected.hash) expect(actual.contract).to eq(expected.contract) @@ -101,7 +101,7 @@ expect(invoice.secret.valid_proof?(invoice.secret.proof)).to be(true) Contract.expect( - invoice.to_h, '69d1cf28d755a38aa5e4024f30992fdf032c3741f325306405a028af853aebd6' + invoice.to_h, 'a6d33323703b4c469122201d93662d9cff0f6d720a0f9341a8edbf158b9a4e9f' ) do |actual, expected| expect(actual.hash).to eq(expected.hash) expect(actual.contract).to eq(expected.contract) @@ -143,7 +143,7 @@ expect(invoice.secret.hash).to eq(secret_hash) Contract.expect( - invoice.to_h, '18fe03304f44560035a9680f378789b3472a0ded753e2b2545dfa48cbf2ce49c' + invoice.to_h, '9e01749ec3c4124742fd4a2b9ed74c7dc04156adcf23f2564f26313ef1ff07aa' ) do |actual, expected| expect(actual.hash).to eq(expected.hash) @@ -187,7 +187,7 @@ expect(invoice.secret.hash).to eq('012bbdc0a6067d78f2e85e17e42ea036cd4d6a2f339a8ccf8129bd142310b7bf') Contract.expect( - invoice.to_h, '34c5b6b8f4d9e706a1741a17df49f68ecbbf3841672c2a38faea616d10628a64' + invoice.to_h, '26c46a85f78133c9f509db56402139f4c70bff937aa27f89729aa352d079e80d' ) do |actual, expected| expect(actual.hash).to eq(expected.hash) @@ -235,7 +235,7 @@ expect { JSON.generate(invoice.to_h) }.not_to raise_error Contract.expect( - invoice.to_h, 'e5ece66732f2b759d9f7e6f0550d42a0638ef15c3eca656806e2b501a8b8828c' + invoice.to_h, '4faf68f7ff17c42044973f3a3a8ea371da401625ca2fba5f00bdaaee43cc6169' ) do |actual, expected| expect(actual.hash).to eq(expected.hash) expect(actual.contract).to eq(expected.contract) @@ -279,7 +279,7 @@ expect(invoice.payment.message).to eq('spontaneous keysend self-payment') Contract.expect( - invoice.to_h, 'e5ece66732f2b759d9f7e6f0550d42a0638ef15c3eca656806e2b501a8b8828c' + invoice.to_h, '4faf68f7ff17c42044973f3a3a8ea371da401625ca2fba5f00bdaaee43cc6169' ) do |actual, expected| expect(actual.hash).to eq(expected.hash) expect(actual.contract).to eq(expected.contract) @@ -327,7 +327,7 @@ expect(invoice.payments.first.message).to eq('spontaneous amp self-payment') Contract.expect( - invoice.to_h, 'a951aa59876623306c86383647ccf3b09a6fd5622d9699ed0bbae6b6ac7ac8e4' + invoice.to_h, 'e016af081caeea0d4547b97741a950d3e4e9ed5e71de1508ab8e21b863a7be86' ) do |actual, expected| expect(actual.hash).to eq(expected.hash) expect(actual.contract).to eq(expected.contract) @@ -370,7 +370,7 @@ expect(invoice.payment.message).to eq('paying the coffee') Contract.expect( - invoice.to_h, 'a85d99eadcfebcc3b8ddc81ad6ad48f0e6f87099680fad209e5903aa5d249032' + invoice.to_h, '47ce40909b22357579629161d58620c5c36faa4d4f514afd94745a3f14b7d253' ) do |actual, expected| expect(actual.hash).to eq(expected.hash) expect(actual.contract).to eq(expected.contract) @@ -412,7 +412,7 @@ expect(invoice.payment.message).to eq('paying what I want') Contract.expect( - invoice.to_h, '7a09c6809372fdea6b18dcad142985977da7f1188f18e4c0249a3b029039c918' + invoice.to_h, 'a9cd278876d5dec062c731ff90497cbbed8b56affd86d366be19dff382d50281' ) do |actual, expected| expect(actual.hash).to eq(expected.hash) expect(actual.contract).to eq(expected.contract) @@ -477,7 +477,7 @@ ).to eq(invoice.received.millisatoshis) Contract.expect( - invoice.to_h, '3cab0ab4dd5f2223b76d5f2db207e07026ad2147c094222a623806f2006fc7bb' + invoice.to_h, '8236a81077793f943f64782f41166f41940c193b8f2b35ee6343983d373d9b23' ) do |actual, expected| expect(actual.hash).to eq(expected.hash) expect(actual.contract).to eq(expected.contract) @@ -533,7 +533,7 @@ expect(invoice.payments.last.secret.preimage).not_to eq(invoice.payments.first.secret.preimage) Contract.expect( - invoice.to_h, '897783e00a083af0c04cac31fa8b61659dbe6f6e62a3c0a877004d2a118129e9' + invoice.to_h, '650daa2a462b08b64970f23b719220a9b6e655c5c23da59ec54005cb7a026506' ) do |actual, expected| expect(actual.hash).to eq(expected.hash) expect(actual.contract).to eq(expected.contract) @@ -595,7 +595,7 @@ expect(invoice.payments.last.secret.preimage).not_to eq(invoice.payments.first.secret.preimage) Contract.expect( - invoice.to_h, '9c5ec08abbf7d271808047badd24e347ed10f9188de7361892b2fa7b758642e0' + invoice.to_h, 'dfee1968d70a1efca9f25ccadfe9dda20eb95b15907862ea7cf5340c8f7f952b' ) do |actual, expected| expect(actual.hash).to eq(expected.hash) expect(actual.contract).to eq(expected.contract) @@ -641,7 +641,7 @@ expect(invoice.secret.hash).to eq('cd1833b549a3f2bd115b433eb21279c185c9a94457062d9b9763b0129fdee567') Contract.expect( - invoice.to_h, '394a76dcaf6544b0ea1378289190381026f2732fdf845a00163746b19e739732' + invoice.to_h, 'be05a9d04997ba2b487e5c50924fe42c53becb751874c996908a4ba6419ef146' ) do |actual, expected| expect(actual.hash).to eq(expected.hash) expect(actual.contract).to eq(expected.contract) @@ -683,7 +683,7 @@ expect(invoice.secret.hash).to eq(secret_hash) Contract.expect( - invoice.to_h, '18fe03304f44560035a9680f378789b3472a0ded753e2b2545dfa48cbf2ce49c' + invoice.to_h, '9e01749ec3c4124742fd4a2b9ed74c7dc04156adcf23f2564f26313ef1ff07aa' ) do |actual, expected| expect(actual.hash).to eq(expected.hash) diff --git a/spec/ports/dsl/bitcoin/address_spec.rb b/spec/ports/dsl/bitcoin/address_spec.rb index e557236..95a2947 100644 --- a/spec/ports/dsl/bitcoin/address_spec.rb +++ b/spec/ports/dsl/bitcoin/address_spec.rb @@ -15,7 +15,7 @@ request = described_class.create(preview: true) expect(request).to eq( - { service: :lightning, method: :new_address, params: { type: :WITNESS_PUBKEY_HASH } } + { service: :lightning, method: :new_address, params: { type: :TAPROOT_PUBKEY } } ) end end @@ -29,7 +29,7 @@ expect(action.result.class).to eq(Lighstorm::Model::Bitcoin::Address) Contract.expect( - action.to_h, '03cf73d9d2ea3741b5be4c03710ed7d912e6a1ae341ba1005d9021ec87fddadf' + action.to_h, '06a789e4851198058a0e66ded776ef6fd9c6390ff5ec7f6990af8c03d910f0fb' ) do |actual, expected| expect(actual.hash).to eq(expected.hash) expect(actual.contract).to eq(expected.contract) @@ -48,7 +48,7 @@ request = address.pay( amount: { millisatoshis: 250_000_000 }, - fee: { satoshis_per_vitual_byte: 1 }, + fee: { maximum: { satoshis_per_vitual_byte: 1 } }, preview: true ) @@ -65,7 +65,7 @@ action = address.pay( amount: { millisatoshis: 250_000_000 }, - fee: { satoshis_per_vitual_byte: 1 } + fee: { maximum: { satoshis_per_vitual_byte: 1 } } ) do |fn, from = :fetch| VCR.reel.unsafe('I_KNOW_WHAT_I_AM_DOING').replay("#{vcr_key}/#{from}", request) { fn.call } end @@ -81,4 +81,4 @@ end end end -end +end \ No newline at end of file diff --git a/spec/ports/dsl/bitcoin/request_spec.rb b/spec/ports/dsl/bitcoin/request_spec.rb index aaa6072..398411b 100644 --- a/spec/ports/dsl/bitcoin/request_spec.rb +++ b/spec/ports/dsl/bitcoin/request_spec.rb @@ -38,7 +38,10 @@ expect(action.result.to_h).to eq( { _key: 'aac0e78574a8e8364dfe73140e69505c3024f6257cb34761a911b2e6ba99417f', - address: { code: '175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W' }, + address: { + code: '175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W', + specification: { format: 'legacy', code: 'P2PKH', bip: nil } + }, uri: 'bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?amount=50&label=Luke-Jr&message=Donation+for+project+xyz', amount: { millisatoshis: 5_000_000_000_000 }, description: 'Luke-Jr', @@ -64,15 +67,15 @@ expect(action.result.class).to eq(Lighstorm::Model::Bitcoin::Request) expect(action.result._key.size).to eq(64) - expect(action.result.address.code).to eq('bcrt1qfxahhms7c7z5elhnjc4hymvzhwmwmh5slv7jv5') + expect(action.result.address.code).to eq('bcrt1pz7qtzvefmhcha5dgv2h3mz4sla2wsw72jsu43qukasy6d6wm08rqvdhy5l') expect(action.result.amount.millisatoshis).to eq(params[:amount][:millisatoshis]) expect(action.result.description).to eq(params[:description]) expect(action.result.message).to eq(params[:message]) expect(action.request).to eq( - { service: :lightning, method: :new_address, params: { type: :WITNESS_PUBKEY_HASH } } + { service: :lightning, method: :new_address, params: { type: :TAPROOT_PUBKEY } } ) - expect(action.response).to eq({ address: 'bcrt1qfxahhms7c7z5elhnjc4hymvzhwmwmh5slv7jv5' }) + expect(action.response).to eq({ address: 'bcrt1pz7qtzvefmhcha5dgv2h3mz4sla2wsw72jsu43qukasy6d6wm08rqvdhy5l' }) expect(action.to_h.keys).to eq(%i[request response result]) @@ -82,18 +85,15 @@ result_to_h[:address][:created_at] = result_to_h[:address][:created_at].utc.to_s expect(result_to_h).to eq( - { - _key: '80a6f24aaaf4c8c36526838853032afd491481085c331322814f35f7ec0d58fd', - address: { - _key: 'e2d74b31321c86571493704e79df0db28b239fed35588e6fa45374ef78efd897', - created_at: '2023-04-02 23:26:12 UTC', - code: 'bcrt1qfxahhms7c7z5elhnjc4hymvzhwmwmh5slv7jv5' - }, - uri: 'bitcoin:bcrt1qfxahhms7c7z5elhnjc4hymvzhwmwmh5slv7jv5?amount=50&label=Luke-Jr&message=Donation+for+project+xyz', + { _key: '7570412f72a96dc838713a97df9c3656096f7ab95964cbf21fffdac779b3bc45', + address: { _key: 'a85588e14839ad23c3ecf0e4a3e9ef59821af353acfe1dfa9a9891762f001020', + created_at: '2023-04-04 12:46:07 UTC', + code: 'bcrt1pz7qtzvefmhcha5dgv2h3mz4sla2wsw72jsu43qukasy6d6wm08rqvdhy5l', + specification: { format: 'taproot', code: 'P2TR', bip: 341 } }, + uri: 'bitcoin:bcrt1pz7qtzvefmhcha5dgv2h3mz4sla2wsw72jsu43qukasy6d6wm08rqvdhy5l?amount=50&label=Luke-Jr&message=Donation+for+project+xyz', amount: { millisatoshis: 5_000_000_000_000 }, description: 'Luke-Jr', - message: 'Donation for project xyz' - } + message: 'Donation for project xyz' } ) end end @@ -109,15 +109,15 @@ expect(action.result.class).to eq(Lighstorm::Model::Bitcoin::Request) expect(action.result._key.size).to eq(64) - expect(action.result.address.code).to eq('bc1qytzke5v5qa4wqzhct37gwnpqs08tuyq9stst5j') + expect(action.result.address.code).to eq('bcrt1pjn0awnucxufzd4590mttdawzwnap4ygt0zenu22uqwv997u7u6qsehs4cv') expect(action.result.amount).to be_nil expect(action.result.description).to be_nil expect(action.result.message).to be_nil expect(action.request).to eq( - { service: :lightning, method: :new_address, params: { type: :WITNESS_PUBKEY_HASH } } + { service: :lightning, method: :new_address, params: { type: :TAPROOT_PUBKEY } } ) - expect(action.response).to eq({ address: 'bc1qytzke5v5qa4wqzhct37gwnpqs08tuyq9stst5j' }) + expect(action.response).to eq({ address: 'bcrt1pjn0awnucxufzd4590mttdawzwnap4ygt0zenu22uqwv997u7u6qsehs4cv' }) expect(action.to_h.keys).to eq(%i[request response result]) @@ -127,13 +127,90 @@ result_to_h[:address][:created_at] = result_to_h[:address][:created_at].utc.to_s expect(result_to_h).to eq( - { _key: 'ab3ed16c2e149776c14373a3b4e67541dd530635e3f6460e85d233a36107e0ac', - address: { - _key: 'a1dd3ab47f04d1d9ba995ba00f66dd65073170c421de6da0ba9e28ae99bc02ec', - created_at: '2023-04-03 00:05:15 UTC', - code: 'bc1qytzke5v5qa4wqzhct37gwnpqs08tuyq9stst5j' - }, - uri: 'bitcoin:bc1qytzke5v5qa4wqzhct37gwnpqs08tuyq9stst5j' } + { _key: '4a5dcb68f8f429a46896d4d41cf16783ccc960ce432a302e9060abe92ad66589', + address: { _key: '458d33ffeedd41df17168ec7738a548c704794c5751adfbe665a287fcdf286b5', + created_at: '2023-04-04 12:46:51 UTC', + code: 'bcrt1pjn0awnucxufzd4590mttdawzwnap4ygt0zenu22uqwv997u7u6qsehs4cv', + specification: { format: 'taproot', code: 'P2TR', bip: 341 } }, + uri: 'bitcoin:bcrt1pjn0awnucxufzd4590mttdawzwnap4ygt0zenu22uqwv997u7u6qsehs4cv' } + ) + end + end + end + + context 'nil amount' do + context 'perform' do + it 'performs' do + action = described_class.create(amount: { millisatoshis: nil }) do |fn, from = :fetch| + VCR.reel.unsafe('I_KNOW_WHAT_I_AM_DOING').replay("#{vcr_key}/#{from}") { fn.call } + end + + expect(action.result.class).to eq(Lighstorm::Model::Bitcoin::Request) + + expect(action.result._key.size).to eq(64) + expect(action.result.address.code).to eq('bcrt1pjn0awnucxufzd4590mttdawzwnap4ygt0zenu22uqwv997u7u6qsehs4cv') + expect(action.result.amount).to be_nil + expect(action.result.description).to be_nil + expect(action.result.message).to be_nil + + expect(action.request).to eq( + { service: :lightning, method: :new_address, params: { type: :TAPROOT_PUBKEY } } + ) + expect(action.response).to eq({ address: 'bcrt1pjn0awnucxufzd4590mttdawzwnap4ygt0zenu22uqwv997u7u6qsehs4cv' }) + + expect(action.to_h.keys).to eq(%i[request response result]) + + result_to_h = action.result.to_h + + expect(result_to_h[:address][:created_at].class).to eq(Time) + result_to_h[:address][:created_at] = result_to_h[:address][:created_at].utc.to_s + + expect(result_to_h).to eq( + { _key: '4a5dcb68f8f429a46896d4d41cf16783ccc960ce432a302e9060abe92ad66589', + address: { _key: '458d33ffeedd41df17168ec7738a548c704794c5751adfbe665a287fcdf286b5', + created_at: '2023-04-04 12:46:51 UTC', + code: 'bcrt1pjn0awnucxufzd4590mttdawzwnap4ygt0zenu22uqwv997u7u6qsehs4cv', + specification: { format: 'taproot', code: 'P2TR', bip: 341 } }, + uri: 'bitcoin:bcrt1pjn0awnucxufzd4590mttdawzwnap4ygt0zenu22uqwv997u7u6qsehs4cv' } + ) + end + end + end + + context '1 millisatoshis' do + let(:params) do + { amount: { millisatoshis: 1 } } + end + + context 'perform' do + it 'performs' do + action = described_class.create(params) do |fn, from = :fetch| + VCR.reel.unsafe('I_KNOW_WHAT_I_AM_DOING').replay("#{vcr_key}/#{from}", params) { fn.call } + end + + expect(action.result.class).to eq(Lighstorm::Model::Bitcoin::Request) + + expect(action.result._key.size).to eq(64) + expect(action.result.amount.millisatoshis).to eq(params[:amount][:millisatoshis]) + expect(action.result.description).to be_nil + expect(action.result.message).to be_nil + expect(action.result.uri).to eq('bitcoin:bc1qkyzf4t2ujjt6p0u3r3zkyxz0tftx77akh64j08?amount=0.00000000001') + + expect(action.to_h.keys).to eq(%i[request response result]) + + result_to_h = action.result.to_h + + expect(result_to_h[:address][:created_at].class).to eq(Time) + result_to_h[:address][:created_at] = result_to_h[:address][:created_at].utc.to_s + + expect(result_to_h).to eq( + { _key: '10757472930cb58ab5d96566040cb439656d045d8ba49ebe0bb747f29278c136', + address: { _key: 'e3388f88a94b940c0f559e11b36851757bc57a3602490fb97851046f31ef718f', + created_at: '2023-04-03 23:21:59 UTC', + code: 'bc1qkyzf4t2ujjt6p0u3r3zkyxz0tftx77akh64j08', + specification: { format: 'segwit', code: 'P2WPKH', bip: 173 } }, + uri: 'bitcoin:bc1qkyzf4t2ujjt6p0u3r3zkyxz0tftx77akh64j08?amount=0.00000000001', + amount: { millisatoshis: 1 } } ) end end @@ -158,17 +235,18 @@ request = action.result expect(request.to_h).to eq( - { - _key: 'e107e5737d0a2347d6b471fbdd2222163b63e9c7c9a48d169681db9946be21cd', - address: { code: 'bcrt1quykkly6egez6dzgfz072h5806p4g2gajm873xn' }, - uri: 'bitcoin:bcrt1quykkly6egez6dzgfz072h5806p4g2gajm873xn?amount=5.0e-06&label=Pay+Alice&message=Hi+Alice%21', + { _key: 'e107e5737d0a2347d6b471fbdd2222163b63e9c7c9a48d169681db9946be21cd', + address: { + code: 'bcrt1quykkly6egez6dzgfz072h5806p4g2gajm873xn', + specification: { format: 'segwit', code: 'P2WPKH', bip: 173 } + }, + uri: 'bitcoin:bcrt1quykkly6egez6dzgfz072h5806p4g2gajm873xn?amount=0.000005&label=Pay+Alice&message=Hi+Alice%21', amount: { millisatoshis: 500_000 }, description: 'Pay Alice', - message: 'Hi Alice!' - } + message: 'Hi Alice!' } ) - preview = request.pay(fee: { satoshis_per_vitual_byte: 1 }, preview: true) + preview = request.pay(fee: { maximum: { satoshis_per_vitual_byte: 1 } }, preview: true) expect(preview).to eq( { service: :lightning, @@ -182,7 +260,7 @@ } } ) - action = request.pay(fee: { satoshis_per_vitual_byte: 1 }) do |fn, from = :fetch| + action = request.pay(fee: { maximum: { satoshis_per_vitual_byte: 1 } }) do |fn, from = :fetch| VCR.reel.unsafe('I_KNOW_WHAT_I_AM_DOING').replay("#{vcr_key}/#{from}", params) { fn.call } end @@ -223,7 +301,14 @@ amount: { millisatoshis: -500_000 }, fee: { millisatoshis: 154_000 }, description: 'Pay Alice', - to: { address: { code: 'bcrt1quykkly6egez6dzgfz072h5806p4g2gajm873xn' } } } + to: { + address: { + code: 'bcrt1quykkly6egez6dzgfz072h5806p4g2gajm873xn', + specification: { + format: 'segwit', code: 'P2WPKH', bip: 173 + } + } + } } ) end end diff --git a/spec/ports/dsl/lightning/invoice_spec.rb b/spec/ports/dsl/lightning/invoice_spec.rb index 1112736..d16cbe3 100644 --- a/spec/ports/dsl/lightning/invoice_spec.rb +++ b/spec/ports/dsl/lightning/invoice_spec.rb @@ -53,7 +53,7 @@ expect(action.result.class).to eq(Lighstorm::Model::Lightning::Invoice) Contract.expect( - action.to_h, 'f916b13ca90b63c39c8228dd88cb502ef8e2c24a69c77f8eff9495767de9d91b' + action.to_h, '27a60ae7a7ad33936d1d1bfbc54589a56b7c6cb1d4b7dd6fc120bed606a16423' ) do |actual, expected| expect(actual.hash).to eq(expected.hash) @@ -80,7 +80,7 @@ invoice_to_h = invoice.to_h Contract.expect( - invoice.to_h, '4b9790e53217a83aeb1614e67d4b75c86743e36cc559762f947fb5357dc36508' + invoice.to_h, 'e9cccab93d70f5b0da973c5f0ea0acdcfc8ea72bcdee01a87f54e342f16495d6' ) do |actual, expected| expect(actual.hash).to eq(expected.hash) expect(actual.contract).to eq(expected.contract) From 042078aa35a8d557cbfb160e61da615459316e46 Mon Sep 17 00:00:00 2001 From: icebaker Date: Tue, 4 Apr 2023 21:44:16 -0300 Subject: [PATCH 4/4] updating docs --- docs/README.md | 57 +++++++++++++------ .../bitcoin/address/actions/create_spec.rb | 2 +- .../bitcoin/address/actions/pay_spec.rb | 2 +- spec/ports/dsl/bitcoin/address_spec.rb | 2 +- 4 files changed, 43 insertions(+), 20 deletions(-) diff --git a/docs/README.md b/docs/README.md index 25e67cd..e0145f7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -425,7 +425,8 @@ balance.to_h ```ruby Lighstorm::Bitcoin::Request.create( - { address: { code: '175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W' }, + { address: { + code: 'bc1pd5jgrk4k0qmrl3ddr0qrap0nrafrdydpsznq9v3a5ny33fz6tklsqut72a' }, amount: { millisatoshis: 5000000000000 }, description: 'Luke-Jr', message: 'Donation for project xyz', @@ -435,7 +436,8 @@ Lighstorm::Bitcoin::Request.create( ```ruby action = Lighstorm::Bitcoin::Request.create( - { address: { code: '175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W' }, + { address: { + code: 'bc1pd5jgrk4k0qmrl3ddr0qrap0nrafrdydpsznq9v3a5ny33fz6tklsqut72a' }, amount: { millisatoshis: 5000000000000 }, description: 'Luke-Jr', message: 'Donation for project xyz' } @@ -446,16 +448,24 @@ action.response request = action.result -request.address.code # '175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W' +request.address.code # 'bc1pd5jgrk4k0qmrl3ddr0qrap0nrafrdydpsznq9v3a5ny33fz6tklsqut72a' request.amount.bitcoins # 50 request.description # 'Luke-Jr' request.message # 'Donation for project xyz' -request.uri # 'bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?amount=50&label=Luke-Jr&message=Donation+for+project+xyz' +request.uri # 'bitcoin:bc1pd5jgrk4k0qmrl3ddr0qrap0nrafrdydpsznq9v3a5ny33fz6tklsqut72a?amount=50&label=Luke-Jr&message=Donation+for+project+xyz' + +request.address.specification.format # 'taproot' +request.address.specification.bip # 341 +request.address.specification.code # 'P2TR' ``` If you don't provide a Bitcoin Address, a new one will be generated for your request, with your wallet as the destination: ```ruby +Lighstorm::Bitcoin::Request.create( + format: 'taproot', # 'taproot', 'segwit', or 'script' +) + action = Lighstorm::Bitcoin::Request.create action.request @@ -463,15 +473,17 @@ action.response request = action.result -request.address.code # 'bc1qytzke5v5qa4wqzhct37gwnpqs08tuyq9stst5j' -request.uri # 'bitcoin:bc1qytzke5v5qa4wqzhct37gwnpqs08tuyq9stst5j' +request.address.code # 'bc1pxxexzg7a4tszd0t782qlqk02533wmrkcfx2nk8cplnv74c8n5qwscya8dq' +request.uri # 'bitcoin:bc1pxxexzg7a4tszd0t782qlqk02533wmrkcfx2nk8cplnv74c8n5qwscya8dq' ``` ```ruby action = Lighstorm::Bitcoin::Request.create( { amount: { millisatoshis: 5000000000000 }, description: 'Luke-Jr', - message: 'Donation for project xyz' } + message: 'Donation for project xyz', + format: 'taproot', # 'taproot', 'segwit', or 'script' + } ) action.request @@ -479,11 +491,11 @@ action.response request = action.result -request.address.code # 'bc1qytzke5v5qa4wqzhct37gwnpqs08tuyq9stst5j' +request.address.code # 'bc1pxxexzg7a4tszd0t782qlqk02533wmrkcfx2nk8cplnv74c8n5qwscya8dq' request.amount.bitcoins # 50 request.description # 'Luke-Jr' request.message # 'Donation for project xyz' -request.uri # 'bitcoin:bc1qytzke5v5qa4wqzhct37gwnpqs08tuyq9stst5j?amount=50&label=Luke-Jr&message=Donation+for+project+xyz' +request.uri # 'bitcoin:bc1pxxexzg7a4tszd0t782qlqk02533wmrkcfx2nk8cplnv74c8n5qwscya8dq?amount=50&label=Luke-Jr&message=Donation+for+project+xyz' ``` #### Decode @@ -492,16 +504,20 @@ Learn about [BIP 21](https://bips.xyz/21). ```ruby request = Lighstorm::Bitcoin::Request.decode( - 'bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?amount=50&label=Luke-Jr&message=Donation%20for%20project%20xyz' + 'bitcoin:bc1pd5jgrk4k0qmrl3ddr0qrap0nrafrdydpsznq9v3a5ny33fz6tklsqut72a?amount=50&label=Luke-Jr&message=Donation%20for%20project%20xyz' ) request._key -request.address.code # '175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W' +request.address.code # 'bc1pd5jgrk4k0qmrl3ddr0qrap0nrafrdydpsznq9v3a5ny33fz6tklsqut72a' request.amount.bitcoins # 50 request.description # 'Luke-Jr' request.message # 'Donation for project xyz' -request.uri # 'bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?amount=50&label=Luke-Jr&message=Donation+for+project+xyz' +request.uri # 'bitcoin:bc1pd5jgrk4k0qmrl3ddr0qrap0nrafrdydpsznq9v3a5ny33fz6tklsqut72a?amount=50&label=Luke-Jr&message=Donation+for+project+xyz' + +request.address.specification.format # 'taproot' +request.address.specification.bip # 341 +request.address.specification.code # 'P2TR' ``` #### Pay @@ -510,7 +526,7 @@ Learn about [BIP 21](https://bips.xyz/21). ```ruby action = Lighstorm::Bitcoin::Request.decode( - 'bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?amount=50&label=Luke-Jr&message=Donation%20for%20project%20xyz' + 'bitcoin:bc1pd5jgrk4k0qmrl3ddr0qrap0nrafrdydpsznq9v3a5ny33fz6tklsqut72a?amount=50&label=Luke-Jr&message=Donation%20for%20project%20xyz' ).pay( amount: { millisatoshis: 5000000000000 }, description: 'Making a Donation', @@ -522,7 +538,7 @@ action = Lighstorm::Bitcoin::Request.decode( ```ruby action = Lighstorm::Bitcoin::Request.decode( - 'bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?amount=50&label=Luke-Jr&message=Donation%20for%20project%20xyz' + 'bitcoin:bc1pd5jgrk4k0qmrl3ddr0qrap0nrafrdydpsznq9v3a5ny33fz6tklsqut72a?amount=50&label=Luke-Jr&message=Donation%20for%20project%20xyz' ).pay(fee: { maximum: { satoshis_per_vitual_byte: 1 } }) action.request @@ -537,7 +553,7 @@ transaction.fee.millisatoshis # 154_000 transaction.description # 'Luke-Jr' transaction.hash # 5ee90f3d8f3efac87c80797773d696e59986477c9201e5cf15a8abac5f632dd4 -transaction.to.address.code # '175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W' +transaction.to.address.code # 'bc1pd5jgrk4k0qmrl3ddr0qrap0nrafrdydpsznq9v3a5ny33fz6tklsqut72a' ``` ### Address @@ -547,7 +563,10 @@ transaction.to.address.code # '175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W' The Lightning Network promotes the idea of creating a new Bitcoin address every time you need one, which helps maintain transaction privacy and fund security. This makes it harder for others to trace your activity, providing a more secure and private experience. ```ruby -Lighstorm::Bitcoin::Address.create(preview: true) +Lighstorm::Bitcoin::Address.create( + format: 'taproot', # 'taproot', 'segwit', or 'script' + preview: true +) action = Lighstorm::Bitcoin::Address.create @@ -558,7 +577,11 @@ address = action.result address._key address.created_at -address.code # 'bcrt1qpma0wpaf2wzlflvamgz2zvw3x0k4vfzwq45x9s' +address.code # 'bc1pd5jgrk4k0qmrl3ddr0qrap0nrafrdydpsznq9v3a5ny33fz6tklsqut72a' + +address.specification.format # 'taproot' +address.specification.bip # 341 +address.specification.code # 'P2TR' ``` ```ruby diff --git a/spec/controllers/bitcoin/address/actions/create_spec.rb b/spec/controllers/bitcoin/address/actions/create_spec.rb index 02665e3..a03a0f2 100644 --- a/spec/controllers/bitcoin/address/actions/create_spec.rb +++ b/spec/controllers/bitcoin/address/actions/create_spec.rb @@ -107,4 +107,4 @@ end end end -end \ No newline at end of file +end diff --git a/spec/controllers/bitcoin/address/actions/pay_spec.rb b/spec/controllers/bitcoin/address/actions/pay_spec.rb index 2670a0e..d6980db 100644 --- a/spec/controllers/bitcoin/address/actions/pay_spec.rb +++ b/spec/controllers/bitcoin/address/actions/pay_spec.rb @@ -194,4 +194,4 @@ end end end -end \ No newline at end of file +end diff --git a/spec/ports/dsl/bitcoin/address_spec.rb b/spec/ports/dsl/bitcoin/address_spec.rb index 95a2947..93227d1 100644 --- a/spec/ports/dsl/bitcoin/address_spec.rb +++ b/spec/ports/dsl/bitcoin/address_spec.rb @@ -81,4 +81,4 @@ end end end -end \ No newline at end of file +end