Class: Tapyrus::TIP0020::Metadata

Inherits:
Object
  • Object
show all
Defined in:
lib/tapyrus/tip0020.rb

Overview

Token metadata class based on TIP-0020 specification

Constant Summary collapse

CURRENT_VERSION =
"1.0"
MAX_NAME_LENGTH =
64
MAX_SYMBOL_LENGTH =
12
MAX_DESCRIPTION_LENGTH =
256
MIN_DECIMALS =
0
MAX_DECIMALS =
18
MAX_DATA_URI_SIZE =

32KB

32 * 1024
VALID_TOKEN_TYPES =
%i[reissuable non_reissuable nft].freeze
NFT_FIELDS =
%i[image animation_url external_url attributes].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(token_type:, version: CURRENT_VERSION, name:, symbol:, decimals: 0, description: nil, icon: nil, issuer: nil, website: nil, terms: nil, properties: nil, image: nil, animation_url: nil, external_url: nil, attributes: nil) ⇒ Metadata

Returns a new instance of Metadata.

Parameters:

  • token_type (Symbol)

    Token type (:reissuable, :non_reissuable, :nft)

  • version (String) (defaults to: CURRENT_VERSION)

    Schema version (default: "1.0")

  • name (String)

    Human-readable token name (max 64 characters, required)

  • symbol (String)

    Token symbol (max 12 characters, required)

  • decimals (Integer) (defaults to: 0)

    Number of decimal places for display (0-18, default: 0)

  • description (String) (defaults to: nil)

    Token description (max 256 characters)

  • icon (String) (defaults to: nil)

    HTTPS URL or Data URI for icon

  • issuer (Hash) (defaults to: nil)

    Issuer information object

  • website (String) (defaults to: nil)

    Official website URL (HTTPS required)

  • terms (String) (defaults to: nil)

    URL to terms of service document (HTTPS required)

  • properties (Hash) (defaults to: nil)

    Additional custom properties

  • image (String) (defaults to: nil)

    NFT image URL (HTTPS or Data URI) - only for NFT

  • animation_url (String) (defaults to: nil)

    NFT animation/video/audio URL (HTTPS or Data URI) - only for NFT

  • external_url (String) (defaults to: nil)

    External URL to view NFT (HTTPS required) - only for NFT

  • attributes (Array<Hash>) (defaults to: nil)

    NFT attributes array with trait_type, value, display_type - only for NFT



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/tapyrus/tip0020.rb', line 50

def initialize(
  token_type:,
  version: CURRENT_VERSION,
  name:,
  symbol:,
  decimals: 0,
  description: nil,
  icon: nil,
  issuer: nil,
  website: nil,
  terms: nil,
  properties: nil,
  image: nil,
  animation_url: nil,
  external_url: nil,
  attributes: nil
)
  @token_type = token_type
  @version = version
  @name = name
  @symbol = symbol
  @decimals = decimals
  @description = description
  @icon = icon
  @issuer = issuer
  @website = website
  @terms = terms
  @properties = properties
  @image = image
  @animation_url = animation_url
  @external_url = external_url
  @attributes = attributes
  validate!
end

Instance Attribute Details

#animation_urlObject

Returns the value of attribute animation_url.



19
20
21
# File 'lib/tapyrus/tip0020.rb', line 19

def animation_url
  @animation_url
end

#attributesObject

Returns the value of attribute attributes.



19
20
21
# File 'lib/tapyrus/tip0020.rb', line 19

def attributes
  @attributes
end

#decimalsObject

Returns the value of attribute decimals.



19
20
21
# File 'lib/tapyrus/tip0020.rb', line 19

def decimals
  @decimals
end

#descriptionObject

Returns the value of attribute description.



19
20
21
# File 'lib/tapyrus/tip0020.rb', line 19

def description
  @description
end

#external_urlObject

Returns the value of attribute external_url.



19
20
21
# File 'lib/tapyrus/tip0020.rb', line 19

def external_url
  @external_url
end

#iconObject

Returns the value of attribute icon.



19
20
21
# File 'lib/tapyrus/tip0020.rb', line 19

def icon
  @icon
end

#imageObject

Returns the value of attribute image.



19
20
21
# File 'lib/tapyrus/tip0020.rb', line 19

def image
  @image
end

#issuerObject

Returns the value of attribute issuer.



19
20
21
# File 'lib/tapyrus/tip0020.rb', line 19

def issuer
  @issuer
end

#nameObject

Returns the value of attribute name.



19
20
21
# File 'lib/tapyrus/tip0020.rb', line 19

def name
  @name
end

#propertiesObject

Returns the value of attribute properties.



19
20
21
# File 'lib/tapyrus/tip0020.rb', line 19

def properties
  @properties
end

#symbolObject

Returns the value of attribute symbol.



19
20
21
# File 'lib/tapyrus/tip0020.rb', line 19

def symbol
  @symbol
end

#termsObject

Returns the value of attribute terms.



19
20
21
# File 'lib/tapyrus/tip0020.rb', line 19

def terms
  @terms
end

#token_typeObject

Returns the value of attribute token_type.



19
20
21
# File 'lib/tapyrus/tip0020.rb', line 19

def token_type
  @token_type
end

#versionObject

Returns the value of attribute version.



19
20
21
# File 'lib/tapyrus/tip0020.rb', line 19

def version
  @version
end

#websiteObject

Returns the value of attribute website.



19
20
21
# File 'lib/tapyrus/tip0020.rb', line 19

def website
  @website
end

Class Method Details

.parse(json_str, token_type:) ⇒ Metadata

Parse from JSON string

Parameters:

  • json_str (String)

    JSON string

  • token_type (Symbol)

    Token type (:reissuable, :non_reissuable, :nft)

Returns:



243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
# File 'lib/tapyrus/tip0020.rb', line 243

def self.parse(json_str, token_type:)
  data = JSON.parse(json_str, symbolize_names: true)
  new(
    token_type: token_type,
    version: data[:version] || CURRENT_VERSION,
    name: data[:name],
    symbol: data[:symbol],
    decimals: data[:decimals] || 0,
    description: data[:description],
    icon: data[:icon],
    issuer: data[:issuer],
    website: data[:website],
    terms: data[:terms],
    properties: data[:properties],
    image: data[:image],
    animation_url: data[:animation_url],
    external_url: data[:external_url],
    attributes: data[:attributes]
  )
end

Instance Method Details

#canonicalizeString

Canonicalize metadata according to RFC 8785 (JCS)

Returns:

  • (String)

    canonicalized JSON string



159
160
161
# File 'lib/tapyrus/tip0020.rb', line 159

def canonicalize
  jcs_serialize(to_h)
end

#commitment(pubkey) ⇒ String

Calculate P2C commitment: c = SHA256(P || h)

Parameters:

  • pubkey (String)

    payment base public key (33 bytes compressed, hex string)

Returns:

  • (String)

    32-byte binary commitment

Raises:

  • (ArgumentError)


178
179
180
181
182
# File 'lib/tapyrus/tip0020.rb', line 178

def commitment(pubkey)
  pubkey_bin = pubkey.htb
  raise ArgumentError, "pubkey must be 33 bytes compressed public key" unless pubkey_bin.bytesize == 33
  Tapyrus.sha256(pubkey_bin + digest)
end

#commitment_hex(pubkey) ⇒ String

Calculate P2C commitment and return as hex string

Parameters:

  • pubkey (String)

    payment base public key (33 bytes compressed, hex string)

Returns:

  • (String)

    64-character hex string



187
188
189
# File 'lib/tapyrus/tip0020.rb', line 187

def commitment_hex(pubkey)
  commitment(pubkey).bth
end

#derive_color_id(pubkey: nil, out_point: nil) ⇒ Tapyrus::Color::ColorIdentifier

Create ColorIdentifier based on token type

Parameters:

  • pubkey (String) (defaults to: nil)

    payment base public key (33 bytes compressed, hex string) - required for :reissuable

  • out_point (Tapyrus::OutPoint) (defaults to: nil)

    out point - required for :non_reissuable and :nft

Returns:



223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'lib/tapyrus/tip0020.rb', line 223

def derive_color_id(pubkey: nil, out_point: nil)
  case token_type
  when :reissuable
    raise ArgumentError, "pubkey is required for reissuable token" unless pubkey
    p2c_pubkey = derive_p2c_pubkey(pubkey)
    script = Tapyrus::Script.to_p2pkh(Tapyrus::Key.new(pubkey: p2c_pubkey).hash160)
    Tapyrus::Color::ColorIdentifier.reissuable(script)
  when :non_reissuable
    raise ArgumentError, "out_point is required for non_reissuable token" unless out_point
    Tapyrus::Color::ColorIdentifier.non_reissuable(out_point)
  when :nft
    raise ArgumentError, "out_point is required for nft token" unless out_point
    Tapyrus::Color::ColorIdentifier.nft(out_point)
  end
end

#derive_p2c_address(pubkey) ⇒ String

Derive P2C address

Parameters:

  • pubkey (String)

    payment base public key (33 bytes compressed, hex string)

Returns:



214
215
216
217
# File 'lib/tapyrus/tip0020.rb', line 214

def derive_p2c_address(pubkey)
  p2c_pubkey = derive_p2c_pubkey(pubkey)
  Tapyrus::Key.new(pubkey: p2c_pubkey).to_p2pkh
end

#derive_p2c_pubkey(pubkey) ⇒ String

Derive P2C public key: P' = P + c * G

Parameters:

  • pubkey (String)

    payment base public key (33 bytes compressed, hex string)

Returns:

  • (String)

    P2C public key (33 bytes compressed, hex string)

Raises:

  • (ArgumentError)

    if derivation results in point at infinity



195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# File 'lib/tapyrus/tip0020.rb', line 195

def derive_p2c_pubkey(pubkey)
  c = commitment(pubkey)
  c_int = c.bth.to_i(16)

  # P + c * G
  group = ECDSA::Group::Secp256k1
  point_p = Tapyrus::Key.new(pubkey: pubkey).to_point
  point_cg = group.generator * c_int
  point_p_prime = point_p + point_cg

  raise ArgumentError, "P2C derivation resulted in point at infinity" if point_p_prime.infinity?

  # Compress the result
  ECDSA::Format::PointOctetString.encode(point_p_prime, compression: true).bth
end

#digestString

Calculate SHA256 hash of canonicalized metadata

Returns:

  • (String)

    32-byte binary hash



165
166
167
# File 'lib/tapyrus/tip0020.rb', line 165

def digest
  Tapyrus.sha256(canonicalize)
end

#digest_hexString

Calculate SHA256 hash and return as hex string

Returns:

  • (String)

    64-character hex string



171
172
173
# File 'lib/tapyrus/tip0020.rb', line 171

def digest_hex
  digest.bth
end

#to_hHash

Convert to Hash

Returns:

  • (Hash)

    metadata as hash



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/tapyrus/tip0020.rb', line 140

def to_h
  result = { version: version, name: name, symbol: symbol }
  result[:decimals] = decimals if decimals != 0
  result[:description] = description if description
  result[:icon] = icon if icon
  result[:issuer] = issuer if issuer
  result[:website] = website if website
  result[:terms] = terms if terms
  result[:properties] = properties if properties
  # NFT fields
  result[:image] = image if image
  result[:animation_url] = animation_url if animation_url
  result[:external_url] = external_url if external_url
  result[:attributes] = attributes if attributes
  result
end

#validate!Object

Validate metadata fields

Raises:

  • (ArgumentError)

    if validation fails



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
# File 'lib/tapyrus/tip0020.rb', line 87

def validate!
  raise ArgumentError, "token_type is required" if token_type.nil?
  unless VALID_TOKEN_TYPES.include?(token_type)
    raise ArgumentError, "token_type must be one of #{VALID_TOKEN_TYPES.join(", ")}"
  end
  validate_nft_fields!
  raise ArgumentError, "version is required" if version.nil? || version.empty?
  raise ArgumentError, "version must be #{CURRENT_VERSION}" unless version == CURRENT_VERSION
  raise ArgumentError, "name is required" if name.nil? || name.empty?
  raise ArgumentError, "name must be #{MAX_NAME_LENGTH} characters or less" if name.length > MAX_NAME_LENGTH
  raise ArgumentError, "symbol is required" if symbol.nil? || symbol.empty?
  if symbol.length > MAX_SYMBOL_LENGTH
    raise ArgumentError, "symbol must be #{MAX_SYMBOL_LENGTH} characters or less"
  end
  if decimals < MIN_DECIMALS || decimals > MAX_DECIMALS
    raise ArgumentError, "decimals must be between #{MIN_DECIMALS} and #{MAX_DECIMALS}"
  end
  if description && description.length > MAX_DESCRIPTION_LENGTH
    raise ArgumentError, "description must be #{MAX_DESCRIPTION_LENGTH} characters or less"
  end
  raise ArgumentError, "icon must be an HTTPS URL or Data URI" if icon && !valid_icon_format?(icon)
  raise ArgumentError, "website must be an HTTPS URL" if website && !valid_https_url?(website)
  raise ArgumentError, "terms must be an HTTPS URL" if terms && !valid_https_url?(terms)
  raise ArgumentError, "image must be an HTTPS URL or Data URI" if image && !valid_media_url?(image)
  if animation_url && !valid_media_url?(animation_url)
    raise ArgumentError, "animation_url must be an HTTPS URL or Data URI"
  end
  raise ArgumentError, "external_url must be an HTTPS URL" if external_url && !valid_https_url?(external_url)
  validate_issuer! if issuer
end

#validate_issuer!Object

Validate issuer object fields

Raises:

  • (ArgumentError)

    if validation fails



120
121
122
123
124
125
126
# File 'lib/tapyrus/tip0020.rb', line 120

def validate_issuer!
  return unless issuer.is_a?(Hash)
  issuer_url = issuer[:url] || issuer["url"]
  raise ArgumentError, "issuer.url must be an HTTPS URL" if issuer_url && !valid_https_url?(issuer_url)
  issuer_email = issuer[:email] || issuer["email"]
  raise ArgumentError, "issuer.email must be a valid email address" if issuer_email && !valid_email?(issuer_email)
end

#validate_nft_fields!Object

Validate NFT-specific fields are only used with NFT token type

Raises:

  • (ArgumentError)

    if NFT fields are used with non-NFT token type



130
131
132
133
134
135
136
# File 'lib/tapyrus/tip0020.rb', line 130

def validate_nft_fields!
  return if token_type == :nft
  nft_fields_present = NFT_FIELDS.select { |field| send(field) }
  unless nft_fields_present.empty?
    raise ArgumentError, "#{nft_fields_present.join(", ")} can only be used with NFT token type"
  end
end