diff --git a/lib/bitcoin/descriptor.rb b/lib/bitcoin/descriptor.rb index c5ecbbc..fc9889c 100644 --- a/lib/bitcoin/descriptor.rb +++ b/lib/bitcoin/descriptor.rb @@ -14,39 +14,41 @@ module Descriptor autoload :Combo, 'bitcoin/descriptor/combo' autoload :Multi, 'bitcoin/descriptor/multi' autoload :SortedMulti, 'bitcoin/descriptor/sorted_multi' + autoload :Raw, 'bitcoin/descriptor/raw' + autoload :Addr, 'bitcoin/descriptor/addr' autoload :Checksum, 'bitcoin/descriptor/checksum' module_function - # generate P2PK output for the given public key. + # Generate P2PK output for the given public key. # @param [String] key private key or public key with hex format # @return [Bitcoin::Descriptor::Pk] def pk(key) Pk.new(key) end - # generate P2PKH output for the given public key. + # Generate P2PKH output for the given public key. # @param [String] key private key or public key with hex format. # @return [Bitcoin::Descriptor::Pkh] def pkh(key) Pkh.new(key) end - # generate P2PKH output for the given public key. + # Generate P2PKH output for the given public key. # @param [String] key private key or public key with hex format. # @return [Bitcoin::Descriptor::Wpkh] def wpkh(key) Wpkh.new(key) end - # generate P2SH embed the argument. + # Generate P2SH embed the argument. # @param [Bitcoin::Descriptor::Base] exp script expression to be embed. # @return [Bitcoin::Descriptor::Sh] def sh(exp) Sh.new(exp) end - # generate P2WSH embed the argument. + # Generate P2WSH embed the argument. # @param [Bitcoin::Descriptor::Expression] exp script expression to be embed. # @return [Bitcoin::Descriptor::Wsh] def wsh(exp) @@ -61,7 +63,7 @@ def combo(key) Combo.new(key) end - # generate multisig output for given keys. + # Generate multisig output for given keys. # @param [Integer] threshold the threshold of multisig. # @param [Array[String]] keys an array of keys. # @return [Bitcoin::Descriptor::Multi] multisig script. @@ -69,7 +71,7 @@ def multi(threshold, *keys) Multi.new(threshold, keys) end - # generate sorted multisig output for given keys. + # Generate sorted multisig output for given keys. # @param [Integer] threshold the threshold of multisig. # @param [Array[String]] keys an array of keys. # @return [Bitcoin::Descriptor::SortedMulti] @@ -77,6 +79,20 @@ def sortedmulti(threshold, *keys) SortedMulti.new(threshold, keys) end + # Generate raw output script about +hex+. + # @param [String] hex Hex string of bitcoin script. + # @return [Bitcoin::Descriptor::Raw] + def raw(hex) + Raw.new(hex) + end + + # Generate raw output script about +hex+. + # @param [String] addr Bitcoin address. + # @return [Bitcoin::Descriptor::Addr] + def addr(addr) + Addr.new(addr) + end + # Parse descriptor string. # @param [String] string Descriptor string. # @return [Bitcoin::Descriptor::Expression] @@ -102,6 +118,10 @@ def parse(string) threshold = args[0].to_i keys = args[1..-1] exp == 'multi' ? multi(threshold, *keys) : sortedmulti(threshold, *keys) + when 'raw' + raw(args_str) + when 'addr' + addr(args_str) else raise ArgumentError, "Parse failed: #{string}" end diff --git a/lib/bitcoin/descriptor/addr.rb b/lib/bitcoin/descriptor/addr.rb new file mode 100644 index 0000000..0e42762 --- /dev/null +++ b/lib/bitcoin/descriptor/addr.rb @@ -0,0 +1,31 @@ +module Bitcoin + module Descriptor + class Addr < Expression + include Bitcoin::Util + + attr_reader :addr + + def initialize(addr) + raise ArgumentError, "Address must be string." unless addr.is_a?(String) + raise ArgumentError, "Address is not valid." unless valid_address?(addr) + @addr = addr + end + + def type + :addr + end + + def to_script + Bitcoin::Script.parse_from_addr(addr) + end + + def top_level? + true + end + + def args + addr + end + end + end +end \ No newline at end of file diff --git a/lib/bitcoin/descriptor/combo.rb b/lib/bitcoin/descriptor/combo.rb index 8616ce3..0770994 100644 --- a/lib/bitcoin/descriptor/combo.rb +++ b/lib/bitcoin/descriptor/combo.rb @@ -21,6 +21,10 @@ def ==(other) return false unless other.is_a?(Combo) type == other.type && to_scripts == other.to_scripts end + + def top_level? + true + end end end end \ No newline at end of file diff --git a/lib/bitcoin/descriptor/expression.rb b/lib/bitcoin/descriptor/expression.rb index 66ac80d..dd9d103 100644 --- a/lib/bitcoin/descriptor/expression.rb +++ b/lib/bitcoin/descriptor/expression.rb @@ -15,6 +15,26 @@ def to_script raise NotImplementedError end + # Whether this is top level or not. + # @return [Boolean] + def top_level? + raise NotImplementedError + end + + # Get args for this expression. + # @return [String] args + def args + raise NotImplementedError + end + + # Get descriptor string. + # @param [Boolean] checksum If true, append checksum. + # @return [String] Descriptor string. + def to_s(checksum: false) + desc = "#{type.to_s}(#{args})" + checksum ? Checksum.descsum_create(desc) : desc + end + # Convert to bitcoin script as hex string. # @return [String] def to_hex diff --git a/lib/bitcoin/descriptor/key_expression.rb b/lib/bitcoin/descriptor/key_expression.rb index 5e404a3..9572525 100644 --- a/lib/bitcoin/descriptor/key_expression.rb +++ b/lib/bitcoin/descriptor/key_expression.rb @@ -11,9 +11,12 @@ def initialize(key) @key = key end - def to_s(checksum: false) - desc = "#{type.to_s}(#{key})" - checksum ? Checksum.descsum_create(desc) : desc + def args + key + end + + def top_level? + false end end end diff --git a/lib/bitcoin/descriptor/multi.rb b/lib/bitcoin/descriptor/multi.rb index fda637c..5b108e4 100644 --- a/lib/bitcoin/descriptor/multi.rb +++ b/lib/bitcoin/descriptor/multi.rb @@ -29,9 +29,12 @@ def to_hex super end - def to_s(checksum: false) - desc = "#{type.to_s}(#{threshold},#{keys.join(',')})" - checksum ? Checksum.descsum_create(desc) : desc + def args + "#{threshold},#{keys.join(',')}" + end + + def top_level? + false end private diff --git a/lib/bitcoin/descriptor/raw.rb b/lib/bitcoin/descriptor/raw.rb new file mode 100644 index 0000000..bbb1c26 --- /dev/null +++ b/lib/bitcoin/descriptor/raw.rb @@ -0,0 +1,32 @@ +module Bitcoin + module Descriptor + class Raw < Expression + + attr_reader :hex + + # Constructor + # @param [String] hex + def initialize(hex) + raise ArgumentError, "Raw script must be string." unless hex.is_a?(String) + raise ArgumentError, "Raw script is not hex." unless hex.valid_hex? + @hex = hex + end + + def type + :raw + end + + def to_script + Bitcoin::Script.parse_from_payload(hex.htb) + end + + def args + hex + end + + def top_level? + true + end + end + end +end \ No newline at end of file diff --git a/lib/bitcoin/descriptor/script_expression.rb b/lib/bitcoin/descriptor/script_expression.rb index bfc7844..f3bbeb1 100644 --- a/lib/bitcoin/descriptor/script_expression.rb +++ b/lib/bitcoin/descriptor/script_expression.rb @@ -9,16 +9,14 @@ def initialize(script) @script = script end - def to_s(checksum: false) - desc = "#{type.to_s}(#{script.to_s})" - checksum ? Checksum.descsum_create(desc) : desc + def args + script.to_s end private def validate!(script) - raise ArgumentError, 'Can only have combo() at top level.' if script.is_a?(Combo) - raise ArgumentError, 'Can only have sh() at top level.' if script.is_a?(Sh) + raise ArgumentError, "Can only have #{script.type.to_s}() at top level." if script.is_a?(Expression) && script.top_level? end end end diff --git a/lib/bitcoin/descriptor/sh.rb b/lib/bitcoin/descriptor/sh.rb index ef23976..13375c6 100644 --- a/lib/bitcoin/descriptor/sh.rb +++ b/lib/bitcoin/descriptor/sh.rb @@ -11,6 +11,10 @@ def to_script script.to_script.to_p2sh end + def top_level? + true + end + private def validate!(script) diff --git a/lib/bitcoin/descriptor/wsh.rb b/lib/bitcoin/descriptor/wsh.rb index 071df1f..7fc0336 100644 --- a/lib/bitcoin/descriptor/wsh.rb +++ b/lib/bitcoin/descriptor/wsh.rb @@ -11,6 +11,10 @@ def to_script Script.to_p2wsh(script.to_script) end + def top_level? + false + end + def validate!(script) super(script) raise ArgumentError, 'A function is needed within P2WSH.' unless script.is_a?(Expression) diff --git a/spec/bitcoin/descriptor_spec.rb b/spec/bitcoin/descriptor_spec.rb index ec45508..ab56723 100644 --- a/spec/bitcoin/descriptor_spec.rb +++ b/spec/bitcoin/descriptor_spec.rb @@ -285,4 +285,29 @@ expect(described_class.parse(desc_combo).to_s(checksum: true)).to eq("#{desc_combo}#p5326pcv") end end + + describe "BIP-385" do + it do + desc_raw = 'raw(deadbeef)' + raw = described_class.parse(desc_raw) + expect(raw.to_hex).to eq("deadbeef") + expect(raw("deadbeef")).to eq(raw) + expect(raw.to_s(checksum: true)).to eq("#{desc_raw}#89f8spxm") + + expect{raw('asdf')}.to raise_error(ArgumentError, "Raw script is not hex.") + expect{sh(raw('deadbeef'))}.to raise_error(ArgumentError, "Can only have raw() at top level.") + expect{wsh(raw('deadbeef'))}.to raise_error(ArgumentError, "Can only have raw() at top level.") + + p2sh = "3PUNyaW7M55oKWJ3kDukwk9bsKvryra15j" + desc_addr = "addr(#{p2sh})" + addr = described_class.parse(desc_addr) + expect(addr.to_hex).to eq("a914eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee87") + expect(addr('3PUNyaW7M55oKWJ3kDukwk9bsKvryra15j')).to eq(addr) + expect(addr.to_s(checksum: true)).to eq("#{desc_addr}#6vhk2xgr") + + expect{addr('asdf')}.to raise_error(ArgumentError, "Address is not valid.") + expect{sh(addr(p2sh))}.to raise_error(ArgumentError, "Can only have addr() at top level.") + expect{wsh(addr(p2sh))}.to raise_error(ArgumentError, "Can only have addr() at top level.") + end + end end