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  }