Skip to content

Commit

Permalink
Add nip19 support
Browse files Browse the repository at this point in the history
  • Loading branch information
azuchi committed Aug 26, 2023
1 parent 62acd75 commit cefeec8
Show file tree
Hide file tree
Showing 6 changed files with 359 additions and 13 deletions.
64 changes: 51 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,6 @@ hrp, data, spec = Bech32.decode('BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4')
# spec is whether Bech32::Encoding::BECH32 or Bech32::Encoding::BECH32M
```

Decode Bech32-encoded Segwit address into `Bech32::SegwitAddr` instance.

```ruby
addr = 'BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4'
segwit_addr = Bech32::SegwitAddr.new(addr)

# generate script pubkey
segwit_addr.to_script_pubkey
=> 0014751e76e8199196d454941c45d1b3a323f1433bd6
```

#### Advanced

The maximum number of characters of Bech32 defined in [BIP-173](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki) is limited to 90 characters.
Expand All @@ -84,7 +73,20 @@ hrp = 'bc'
data = [0, 14, 20, 15, 7, 13, 26, 0, 25, 18, 6, 11, 13, 8, 21, 4, 20, 3, 17, 2, 29, 3, 12, 29, 3, 4, 15, 24, 20, 6, 14, 30, 22]

bech = Bech32.encode(hrp, data, Bech32::Encoding::BECH32)
=> bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4
=> 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'
```

### Segwit

Decode Bech32-encoded Segwit address into `Bech32::SegwitAddr` instance.

```ruby
addr = 'BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4'
segwit_addr = Bech32::SegwitAddr.new(addr)

# generate script pubkey
segwit_addr.to_script_pubkey
=> '0014751e76e8199196d454941c45d1b3a323f1433bd6'
```

Encode Segwit script into Bech32 Segwit address.
Expand All @@ -95,7 +97,43 @@ segwit_addr.script_pubkey = '0014751e76e8199196d454941c45d1b3a323f1433bd6'

# generate addr
segwit_addr.addr
=> bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4
=> 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'
```

### Nostr

Supports encoding/decoding of Nostr's [NIP-19](https://github.com/nostr-protocol/nips/blob/master/19.md) entities.

```ruby
# Decode bare entity
bech32 = 'npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg'
entity = Bech32::Nostr::NIP19.parse(bech32)
entity.hrp
=> 'npub'
entity.data
=> '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'

# Decode tlv entity
bech32 = 'nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksjlyr9p'
entity = Bech32::Nostr::NIP19.parse(bech32)
entity.hrp
=> 'nprofile'
entity.entries[0].value
=> '3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d'
entity.entries[1].value
=> 'wss://r.x.com'

# Encode bare entity
entity = Bech32::Nostr::BareEntity.new('npub', '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e')
entity.encode
=> 'npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg'

# Encode tlv entity
entry_relay = Bech32::Nostr::TLVEntry.new(Bech32::Nostr::TLVEntity::TYPE_RELAY, 'wss://relay.nostr.example')
entry_author = Bech32::Nostr::TLVEntry.new(Bech32::Nostr::TLVEntity::TYPE_AUTHOR, '97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322')
entity = Bech32::Nostr::TLVEntity.new(Bech32::Nostr::NIP19::HRP_EVENT, [entry_relay, entry_author])
entity.encode
=> 'nevent1qyvhwumn8ghj7un9d3shjtnwdaehgu3wv4uxzmtsd3jsygyhcu9ygdn2v56uz3dnx0uh865xmlwz675emfsccsxxguz6mx8rygstv78u'
```

### CLI
Expand Down
1 change: 1 addition & 0 deletions lib/bech32.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ module Encoding
end

autoload :SegwitAddr, 'bech32/segwit_addr'
autoload :Nostr, 'bech32/nostr'

SEPARATOR = '1'

Expand Down
8 changes: 8 additions & 0 deletions lib/bech32/nostr.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module Bech32
module Nostr
autoload :NIP19, 'bech32/nostr/nip19'
autoload :BareEntity, 'bech32/nostr/entity'
autoload :TLVEntity, 'bech32/nostr/entity'
end

end
147 changes: 147 additions & 0 deletions lib/bech32/nostr/entity.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
module Bech32
module Nostr
class BareEntity
attr_reader :hrp
attr_reader :data

# Initialize bare entity.
# @param [String] hrp human-readable part.
# @param [String] data Entity data(hex string).
def initialize(hrp, data)
raise ArgumentError, "HRP #{hrp} is unsupported." unless NIP19::BARE_PREFIXES.include?(hrp)
raise ArgumentError, "Data whose HRP is #{hrp} must be 32 bytes." unless [data].pack('H*').bytesize == 32
@hrp = hrp
@data = data
end

# Encode bare entity to bech32 string.
# @return [String] bech32 string.
def encode
Bech32.encode(hrp, Bech32.convert_bits([data].pack('H*').unpack('C*'), 8, 5), Bech32::Encoding::BECH32)
end
end

class TLVEntry

attr_reader :type
attr_reader :label
attr_reader :value

def initialize(type, value, label = nil)
raise ArgumentError, "Type #{type} unsupported." unless TLVEntity::TYPES.include?(type)

@type = type
@value = value
@label = label
end

# Convert to binary data.
# @return [String] binary data.
def to_payload
data = if value.is_a?(Integer)
[value].pack('N')
else
hex_string?(value) ? [value].pack('H*') : value
end
len = data.bytesize
[type, len].pack('CC') + data
end

private

# Check whether +str+ is hex string or not.
# @param [String] str string.
# @return [Boolean]
def hex_string?(str)
return false if str.bytes.any? { |b| b > 127 }
return false if str.length % 2 != 0
hex_chars = str.chars.to_a
hex_chars.all? { |c| c =~ /[0-9a-fA-F]/ }
end
end

class TLVEntity

TYPE_SPECIAL = 0
TYPE_RELAY = 1
TYPE_AUTHOR = 2
TYPE_KIND = 3

TYPES = [TYPE_SPECIAL, TYPE_RELAY, TYPE_AUTHOR, TYPE_KIND]

attr_reader :hrp
attr_reader :entries

# Initialize TLV entity.
# @param [String] hrp human-readable part.
# @param [Array<TLVEntry>] entries TLV entries.
# @return [TLVEntity]
def initialize(hrp, entries)
raise ArgumentError, "HRP #{hrp} is unsupported." unless NIP19::TLV_PREFIXES.include?(hrp)
entries.each do |e|
raise ArgumentError, "Entries must be TLVEntry. #{e.class} given." unless e.is_a?(TLVEntry)
end

@hrp = hrp
@entries = entries
end

# Parse TLV entity from data.
# @param [String] hrp human-readable part.
# @param [String] data Entity data(binary format).
# @return [TLVEntity]
def self.parse(hrp, data)
buf = StringIO.new(data)
entries = []
until buf.eof?
type, len = buf.read(2).unpack('CC')
case type
when TYPE_SPECIAL # special
case hrp
when NIP19::HRP_PROFILE
entries << TLVEntry.new(type, buf.read(len).unpack1('H*'), 'pubkey')
when NIP19::HRP_RELAY
entries << TLVEntry.new(type, buf.read(len), 'relay')
when NIP19::HRP_EVENT
entries << TLVEntry.new(type, buf.read(len).unpack1('H*'), 'id')
when NIP19::HRP_EVENT_COORDINATE
entries << TLVEntry.new(type, buf.read(len), 'identifier')
end
when TYPE_RELAY # relay
case hrp
when NIP19::HRP_PROFILE, NIP19::HRP_EVENT, NIP19::HRP_EVENT_COORDINATE
entries << TLVEntry.new(type, buf.read(len), 'relay')
else
raise ArgumentError, "Type: #{type} does not supported for HRP: #{hrp}"
end
when TYPE_AUTHOR # author
case hrp
when NIP19::HRP_EVENT, NIP19::HRP_EVENT_COORDINATE
entries << TLVEntry.new(type, buf.read(len).unpack1('H*'), 'author')
else
raise ArgumentError, "Type: #{type} does not supported for HRP: #{hrp}"
end
when TYPE_KIND # kind
case hrp
when NIP19::HRP_EVENT, NIP19::HRP_EVENT_COORDINATE
entries << TLVEntry.new(type, buf.read(len).unpack1('H*').to_i(16), 'kind')
else
raise ArgumentError, "Type: #{type} does not supported for HRP: #{hrp}"
end
else
raise ArgumentError, "Unknown TLV type: #{type}"
end
end

TLVEntity.new(hrp, entries)
end

# Encode tlv entity to bech32 string.
# @return [String] bech32 string.
def encode
data = entries.map(&:to_payload).join
Bech32.encode(hrp, Bech32.convert_bits(data.unpack('C*'), 8, 5), Bech32::Encoding::BECH32)
end
end
end
end
41 changes: 41 additions & 0 deletions lib/bech32/nostr/nip19.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
module Bech32
module Nostr
module NIP19

HRP_PUBKEY = 'npub'
HRP_PRIVATE_KEY = 'nsec'
HRP_NOTE_ID = 'note'
HRP_PROFILE = 'nprofile'
HRP_EVENT = 'nevent'
HRP_RELAY = 'nrelay'
HRP_EVENT_COORDINATE = 'naddr'

BARE_PREFIXES = [HRP_PUBKEY, HRP_PRIVATE_KEY, HRP_NOTE_ID]
TLV_PREFIXES = [HRP_PROFILE, HRP_EVENT, HRP_RELAY, HRP_EVENT_COORDINATE]
ALL_PREFIXES = BARE_PREFIXES + TLV_PREFIXES

module_function

# Decode nip19 string.
# @param [String] string Bech32 string.
# @return [BareEntity | TLVEntity]
def decode(string)
hrp, data, spec = Bech32.decode(string, string.length)

raise ArgumentError, 'Invalid nip19 string.' if hrp.nil?
raise ArgumentError, 'Invalid bech32 spec.' unless spec == Bech32::Encoding::BECH32

entity = Bech32.convert_bits(data, 5, 8, false).pack('C*')
raise ArgumentError, "Data whose HRP is #{hrp} must be 32 bytes." if BARE_PREFIXES.include?(hrp) && entity.bytesize != 32
if BARE_PREFIXES.include?(hrp)
BareEntity.new(hrp, entity.unpack1('H*'))
elsif TLV_PREFIXES.include?(hrp)
TLVEntity.parse(hrp, entity)
else
raise ArgumentError, "HRP #{hrp} is unsupported."
end
end

end
end
end
Loading

0 comments on commit cefeec8

Please sign in to comment.