github.com/mavryk-network/mvgo@v1.19.9/contract/token.go (about) 1 // Copyright (c) 2020-2022 Blockwatch Data Inc. 2 // Author: alex@blockwatch.cc 3 4 package contract 5 6 import ( 7 "context" 8 "encoding/json" 9 "fmt" 10 "strconv" 11 "time" 12 13 "github.com/mavryk-network/mvgo/mavryk" 14 "github.com/mavryk-network/mvgo/micheline" 15 ) 16 17 type TokenKind byte 18 19 const ( 20 TokenKindInvalid TokenKind = iota 21 TokenKindTez 22 TokenKindFA1 23 TokenKindFA1_2 24 TokenKindFA2 25 TokenKindNFT 26 TokenKindNoView 27 ) 28 29 func (k TokenKind) String() string { 30 switch k { 31 case TokenKindTez: 32 return "tez" 33 case TokenKindFA1: 34 return "fa1" 35 case TokenKindFA1_2: 36 return "fa1_2" 37 case TokenKindFA2: 38 return "fa2" 39 case TokenKindNFT: 40 return "nft" 41 case TokenKindNoView: 42 return "noview" 43 default: 44 return "" 45 } 46 } 47 48 func (k TokenKind) IsValid() bool { 49 return k != TokenKindInvalid 50 } 51 52 const TOKEN_METADATA = "token_metadata" 53 54 // Represents Tzip12 token metadata used by FA1 and FA2 tokens 55 // mixed with TZip21 metadata for NFTs 56 type TokenMetadata struct { 57 // TZip12 normative (only decimals is required) 58 Name string `json:"name"` 59 Symbol string `json:"symbol"` 60 Decimals int `json:"decimals"` 61 62 // Tzip21 63 Description string `json:"description,omitempty"` 64 ShouldPreferSymbol bool `json:"shouldPreferSymbol,omitempty"` 65 IsBooleanAmount bool `json:"isBooleanAmount,omitempty"` 66 IsTransferable bool `json:"isTransferable,omitempty"` 67 ArtifactUri string `json:"artifactUri,omitempty"` 68 DisplayUri string `json:"displayUri,omitempty"` 69 ThumbnailUri string `json:"thumbnailUri,omitempty"` 70 Minter string `json:"minter,omitempty"` 71 Creators []string `json:"creators,omitempty"` 72 Contributors []string `json:"contributors,omitempty"` 73 Publishers []string `json:"publishers,omitempty"` 74 Date time.Time `json:"date,omitempty"` 75 Type string `json:"type,omitempty"` 76 Tags []string `json:"tags,omitempty"` 77 Genres []string `json:"genres,omitempty"` 78 Language string `json:"language,omitempty"` 79 Identifier string `json:"identifier,omitempty"` 80 Rights string `json:"rights,omitempty"` 81 RightUri string `json:"rightUri,omitempty"` 82 ExternalUri string `json:"externalUri,omitempty"` 83 Formats []Tz21Format `json:"formats,omitempty"` 84 Attributes []Tz21Attribute `json:"attributes,omitempty"` 85 86 // internal 87 uri string `json:"-"` 88 raw json.RawMessage `json:"-"` 89 } 90 91 type Tz21Format struct { 92 Uri string `json:"uri"` 93 Hash string `json:"hash"` 94 MimeType string `json:"mimeType"` 95 FileSize int64 `json:"fileSize"` 96 FileName string `json:"fileName"` 97 Duration string `json:"duration"` 98 Dimensions Tz21Dimension `json:"dimensions"` 99 DataRate Tz21DataRate `json:"dataRate"` 100 } 101 102 type Tz21Attribute struct { 103 Name string `json:"name"` 104 Value string `json:"value"` 105 Type string `json:"type,omitempty"` 106 } 107 108 type Tz21Dimension struct { 109 Value string `json:"value"` 110 Unit string `json:"unit"` 111 } 112 113 type Tz21DataRate struct { 114 Value string `json:"value"` 115 Unit string `json:"unit"` 116 } 117 118 // (pair (nat %token_id) (map %token_info string bytes)) 119 func (t *TokenMetadata) UnmarshalPrim(prim micheline.Prim) error { 120 if len(prim.Args) < 2 { 121 return fmt.Errorf("invalid metadata prim %s", prim.Dump()) 122 } 123 t.IsTransferable = true // default 124 err := prim.Args[1].Walk(func(p micheline.Prim) error { 125 if p.IsSequence() { 126 return nil 127 } 128 if !p.IsElt() { 129 return fmt.Errorf("unexpected map item %q", p.Dump()) 130 } 131 field := p.Args[0].String 132 data := p.Args[1].Bytes 133 // unpack packed bytes (some people do that, yes) 134 if p.Args[1].IsPacked() { 135 p, _ := p.Args[1].Unpack() 136 if p.Type == micheline.PrimBytes { 137 data = p.Bytes 138 } else { 139 data = []byte(p.String) 140 } 141 } 142 switch field { 143 case "": 144 t.uri = string(data) 145 case "name": 146 t.Name = string(data) 147 case "description": 148 t.Description = string(data) 149 case "symbol": 150 t.Symbol = string(data) 151 case "icon", "logo", "thumbnailUri", "thumbnail_uri": 152 t.ThumbnailUri = string(data) 153 case "artifactUri", "artifact_uri": 154 t.ArtifactUri = string(data) 155 case "displayUri", "display_uri": 156 t.DisplayUri = string(data) 157 case "decimals": 158 d, err := strconv.Atoi(string(data)) 159 if err != nil { 160 return fmt.Errorf("%q: %v", field, err) 161 } 162 t.Decimals = d 163 case "shouldPreferSymbol", "should_prefer_symbol": 164 b, err := strconv.ParseBool(string(data)) 165 if err != nil { 166 return fmt.Errorf("%q: %v", field, err) 167 } 168 t.ShouldPreferSymbol = b 169 case "isBooleanAmount", "is_boolean_amount": 170 b, err := strconv.ParseBool(string(data)) 171 if err != nil { 172 return fmt.Errorf("%q: %v", field, err) 173 } 174 t.IsBooleanAmount = b 175 case "isTransferable", "is_transferable": 176 b, err := strconv.ParseBool(string(data)) 177 if err != nil { 178 return fmt.Errorf("%q: %v", field, err) 179 } 180 t.IsTransferable = b 181 case "nonTransferable": // non-standard 182 b, err := strconv.ParseBool(string(data)) 183 if err != nil { 184 return fmt.Errorf("%q: %v", field, err) 185 } 186 t.IsTransferable = !b 187 default: 188 log.Errorf("token metadata: unsupported field %q\n", field) 189 } 190 return micheline.PrimSkip 191 }) 192 return err 193 } 194 195 func (t *TokenMetadata) UnmarshalJSON(data []byte) error { 196 type Alias *TokenMetadata 197 err := json.Unmarshal(data, Alias(t)) 198 if err != nil { 199 return err 200 } 201 t.raw = json.RawMessage(data) 202 return nil 203 } 204 205 func (t TokenMetadata) URI() string { 206 return t.uri 207 } 208 209 func (t TokenMetadata) Raw() []byte { 210 if t.raw != nil { 211 return t.raw 212 } 213 buf, _ := json.Marshal(t) 214 return buf 215 } 216 217 func ResolveTokenMetadata(ctx context.Context, contract *Contract, tokenid mavryk.Z) (*TokenMetadata, error) { 218 var ( 219 store micheline.Prim 220 err error 221 ) 222 223 // we need contract script and storage 224 if err = contract.Resolve(ctx); err != nil { 225 return nil, err 226 } 227 228 // lookup well known (pre-tz16 or wrong) tokens 229 if m, ok := wellKnown[contract.Address().String()]; ok { 230 return m, nil 231 } 232 233 // prefer off-chain view via run_code, but don't fail if not present 234 tz16, _ := contract.ResolveMetadata(ctx) 235 if tz16 != nil && tz16.HasView(TOKEN_METADATA) { 236 view := tz16.GetView(TOKEN_METADATA) 237 args := micheline.NewNat(tokenid.Big()) 238 store, err = view.Run(ctx, contract, args) 239 if err != nil { 240 return nil, err 241 } 242 } else { 243 // read token_metadata bigmap 244 bigmaps := contract.script.Bigmaps() 245 id, ok := bigmaps[TOKEN_METADATA] 246 if !ok { 247 return nil, fmt.Errorf("%s/%d: missing token metadata, have %v", contract.addr, tokenid.Int64(), bigmaps) 248 } 249 hash := (micheline.Key{ 250 Type: micheline.NewType(micheline.NewPrim(micheline.T_NAT)), 251 IntKey: tokenid.Big(), 252 }).Hash() 253 store, err = contract.rpc.GetActiveBigmapValue(ctx, id, hash) 254 if err != nil { 255 return nil, err 256 } 257 } 258 259 // parse storage: (pair (nat %token_id) (map %token_info string bytes)) 260 meta := &TokenMetadata{} 261 if err := meta.UnmarshalPrim(store); err != nil { 262 return nil, err 263 } 264 265 // should forward? 266 if meta.uri != "" { 267 if err := contract.ResolveTz16Uri(ctx, meta.uri, meta, nil); err != nil { 268 return nil, err 269 } 270 } 271 272 // fill empty token name from contract metadata 273 if meta.Name == "" && tz16 != nil { 274 meta.Name = tz16.Name 275 } 276 277 return meta, nil 278 } 279 280 type TokenBalance struct { 281 Owner mavryk.Address 282 Token mavryk.Address 283 TokenId mavryk.Z 284 Balance mavryk.Z 285 }