Class: MixinBot::MixAddress

Inherits:
Object
  • Object
show all
Defined in:
lib/mixin_bot/address.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(**args) ⇒ MixAddress

Returns a new instance of MixAddress.

Raises:



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/mixin_bot/address.rb', line 21

def initialize(**args)
  args = args.with_indifferent_access

  if args[:address]
    @address = args[:address]
    decode
  elsif args[:payload]
    @payload = args[:payload]
    decode
  else
    @version = args[:version] || MIX_ADDRESS_VERSION

    if args[:members].present?
      @xin_members, @uuid_members = args[:members].partition do |member|
        member.start_with?(MAIN_ADDRESS_PREFIX)
      end
    else
      @uuid_members = args[:uuid_members] || []
      @xin_members = args[:xin_members] || []
    end

    @uuid_members = @uuid_members.sort
    @xin_members = @xin_members.sort

    @threshold = args[:threshold]
    encode
  end

  raise ArgumentError, 'invalid address' unless valid?
end

Instance Attribute Details

#addressObject

Returns the value of attribute address.



11
12
13
# File 'lib/mixin_bot/address.rb', line 11

def address
  @address
end

#payloadObject

Returns the value of attribute payload.



11
12
13
# File 'lib/mixin_bot/address.rb', line 11

def payload
  @payload
end

#thresholdObject

Returns the value of attribute threshold.



11
12
13
# File 'lib/mixin_bot/address.rb', line 11

def threshold
  @threshold
end

#uuid_membersObject

Returns the value of attribute uuid_members.



11
12
13
# File 'lib/mixin_bot/address.rb', line 11

def uuid_members
  @uuid_members
end

#versionObject

Returns the value of attribute version.



11
12
13
# File 'lib/mixin_bot/address.rb', line 11

def version
  @version
end

#xin_membersObject

Returns the value of attribute xin_members.



11
12
13
# File 'lib/mixin_bot/address.rb', line 11

def xin_members
  @xin_members
end

Class Method Details

.from_members(members:, threshold:) ⇒ Object



17
18
19
# File 'lib/mixin_bot/address.rb', line 17

def self.from_members(members:, threshold:)
  new(members:, threshold:)
end

.parse(string) ⇒ Object



13
14
15
# File 'lib/mixin_bot/address.rb', line 13

def self.parse(string)
  new(address: string)
end

Instance Method Details

#decodeObject

Raises:



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/mixin_bot/address.rb', line 120

def decode
  if address.present?
    raise ArgumentError, 'invalid address' unless address&.start_with? MIX_ADDRESS_PREFIX

    data = address[MIX_ADDRESS_PREFIX.length..]
    data = Base58.base58_to_binary data, :bitcoin
    raise ArgumentError, 'invalid address, length invalid' if data.length < 3 + 16 + 4

    self.payload = data[...-4]
    checksum = SHA3::Digest::SHA256.digest(MIX_ADDRESS_PREFIX + payload)[0...4]
    raise ArgumentError, 'invalid address, checksum invalid' unless checksum == data[-4..]
  else
    checksum = SHA3::Digest::SHA256.digest(MIX_ADDRESS_PREFIX + payload)[0...4]
    data = payload + checksum
    data = Base58.binary_to_base58 data, :bitcoin
    self.address = "#{MIX_ADDRESS_PREFIX}#{data}"
  end

  self.version = payload[0].ord
  raise ArgumentError, 'invalid address, version invalid' unless version.is_a?(Integer)

  self.threshold = payload[1].ord
  raise ArgumentError, 'invalid address, threshold invalid' unless threshold.is_a?(Integer)

  members_count = payload[2].ord
  raise ArgumentError, 'invalid address, members count invalid' unless members_count.is_a?(Integer)

  if payload[3...].length == members_count * UUID_ADDRESS_LENGTH
    uuid_members = payload[3...].chars.each_slice(UUID_ADDRESS_LENGTH).map(&:join)
    self.uuid_members = uuid_members.map(&->(member) { MixinBot::UUID.new(raw: member).unpacked })
    self.xin_members = []
  else
    xin_members = payload[3...].chars.each_slice(MAIN_ADDRESS_LENGTH).map(&:join)
    self.xin_members = xin_members.map(&->(member) { MainAddress.new(public_key: member).address })
    self.uuid_members = []
  end
end

#encodeObject

Raises:



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/mixin_bot/address.rb', line 85

def encode
  raise ArgumentError, 'members should be an array' unless uuid_members.is_a?(Array) || xin_members.is_a?(Array)
  raise ArgumentError, 'members should not be empty' if uuid_members.empty? && xin_members.empty?
  raise ArgumentError, 'members length should less than 256' if uuid_members.length + xin_members.length > 255

  member_count = uuid_members.length + xin_members.length
  # UUID mix (Go NewUUIDMixAddress): threshold must be in 1..len(members).
  # XIN-only mix (Go NewMainnetMixAddress): threshold may exceed member count (sparse), e.g. storage 1-of-64 style.
  if uuid_members.present? && xin_members.empty?
    raise ArgumentError, "invalid threshold: #{threshold}" unless threshold.positive? && threshold <= member_count
  elsif xin_members.present? && uuid_members.empty?
    raise ArgumentError, "invalid threshold: #{threshold}" unless threshold.positive?
    raise ArgumentError, 'too many XIN members' if member_count > 64
  elsif threshold > member_count
    raise ArgumentError, "invalid threshold: #{threshold}"
  end

  prefix =
    [version].pack('C*') +
    [threshold].pack('C*') +
    [uuid_members.length + xin_members.length].pack('C*')
  msg =
    uuid_members&.map(&->(member) { MixinBot::UUID.new(hex: member).packed })&.join.to_s +
    xin_members&.map(&->(member) { MainAddress.new(address: member).public_key })&.join.to_s

  self.payload = prefix + msg

  checksum = SHA3::Digest::SHA256.digest(MIX_ADDRESS_PREFIX + payload)
  data = payload + checksum[0...4]
  data = Base58.binary_to_base58 data, :bitcoin
  self.address = "#{MIX_ADDRESS_PREFIX}#{data}"

  address
end

#request_or_generate_ghost_keys(output_index = 0, api: MixinBot.api) ⇒ Object



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/mixin_bot/address.rb', line 65

def request_or_generate_ghost_keys(output_index = 0, api: MixinBot.api)
  if xin_members.present?
    key = JOSE::JWA::Ed25519.keypair
    gk = { 'mask' => key[0].unpack1('H*'), 'keys' => [] }
    xin_members.each do |member|
      payload = MixinBot.utils.parse_main_address(member)
      spend_key = payload[0...32]
      view_key = payload[-32..]
      ghost = MixinBot.utils.derive_ghost_public_key(key[1], view_key, spend_key, output_index)
      gk['keys'] << ghost.unpack1('H*')
    end
    gk
  else
    hint = SecureRandom.uuid
    api.create_safe_keys(
      { receivers: (uuid_members + xin_members).sort, index: output_index, hint: }
    )['data'].first
  end
end

#to_safe_recipientObject



56
57
58
59
60
61
62
63
# File 'lib/mixin_bot/address.rb', line 56

def to_safe_recipient
  {
    members: uuid_members + xin_members,
    threshold:,
    amount:,
    mix_address: address
  }
end

#valid?Boolean

Returns:

  • (Boolean)


52
53
54
# File 'lib/mixin_bot/address.rb', line 52

def valid?
  address.present? && (uuid_members.present? || xin_members.present?) && threshold.present?
end