github.com/mavryk-network/mvgo@v1.19.9/contract/fa2.go (about)

     1  // Copyright (c) 2020-2022 Blockwatch Data Inc.
     2  // Author: alex@blockwatch.cc
     3  
     4  package contract
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"encoding/json"
    10  	"fmt"
    11  	"sort"
    12  
    13  	"github.com/mavryk-network/mvgo/codec"
    14  	"github.com/mavryk-network/mvgo/mavryk"
    15  	"github.com/mavryk-network/mvgo/micheline"
    16  	"github.com/mavryk-network/mvgo/rpc"
    17  )
    18  
    19  // Represents a generic FA2 (tzip12) token
    20  type FA2Token struct {
    21  	Address  mavryk.Address
    22  	TokenId  mavryk.Z
    23  	contract *Contract
    24  }
    25  
    26  func NewFA2Token(addr mavryk.Address, id int64, cli *rpc.Client) *FA2Token {
    27  	t := &FA2Token{Address: addr, contract: NewContract(addr, cli)}
    28  	t.TokenId.SetInt64(id)
    29  	return t
    30  }
    31  
    32  func (t FA2Token) Contract() *Contract {
    33  	return t.contract
    34  }
    35  
    36  func (t FA2Token) Equal(v FA2Token) bool {
    37  	return t.Address.Equal(v.Address) && t.TokenId.Equal(v.TokenId)
    38  }
    39  
    40  func (t FA2Token) ResolveMetadata(ctx context.Context) (*TokenMetadata, error) {
    41  	return ResolveTokenMetadata(ctx, t.contract, t.TokenId)
    42  }
    43  
    44  type FA2BalanceRequest struct {
    45  	Owner   mavryk.Address `json:"owner"`
    46  	TokenId mavryk.Z       `json:"token_id"`
    47  }
    48  
    49  type FA2BalanceResponse struct {
    50  	Request FA2BalanceRequest `json:"request"`
    51  	Balance mavryk.Z          `json:"balance"`
    52  }
    53  
    54  func (t FA2Token) GetBalance(ctx context.Context, owner mavryk.Address) (mavryk.Z, error) {
    55  	resp, err := t.GetBalances(ctx, []FA2BalanceRequest{{Owner: owner, TokenId: t.TokenId}})
    56  	if err != nil {
    57  		return mavryk.Z{}, err
    58  	}
    59  	return resp[0].Balance, nil
    60  }
    61  
    62  func (t FA2Token) GetBalances(ctx context.Context, req []FA2BalanceRequest) ([]FA2BalanceResponse, error) {
    63  	args := micheline.NewSeq()
    64  	for _, r := range req {
    65  		args.Args = append(args.Args, micheline.NewPair(
    66  			micheline.NewBytes(r.Owner.EncodePadded()),
    67  			micheline.NewNat(r.TokenId.Big()),
    68  		))
    69  	}
    70  	prim, err := t.contract.RunCallback(ctx, "balance_of", args)
    71  	if err != nil {
    72  		return nil, err
    73  	}
    74  	val := micheline.NewValue(
    75  		micheline.NewType(micheline.ITzip12.PrimOf("balance_of").Args[1].Args[0]),
    76  		prim,
    77  	)
    78  	resp := make([]FA2BalanceResponse, 0)
    79  	err = val.Unmarshal(&resp)
    80  	return resp, err
    81  }
    82  
    83  func (t FA2Token) AddOperator(owner, operator mavryk.Address) CallArguments {
    84  	return NewFA2ApprovalArgs().
    85  		AddOperator(owner, operator, t.TokenId).
    86  		WithSource(owner).
    87  		WithDestination(t.Address)
    88  }
    89  
    90  func (t FA2Token) RemoveOperator(owner, operator mavryk.Address) CallArguments {
    91  	return NewFA2ApprovalArgs().
    92  		RemoveOperator(owner, operator, t.TokenId).
    93  		WithSource(owner).
    94  		WithDestination(t.Address)
    95  }
    96  
    97  func (t FA2Token) Transfer(from, to mavryk.Address, amount mavryk.Z) CallArguments {
    98  	return NewFA2TransferArgs().
    99  		WithTransfer(from, to, t.TokenId, amount).
   100  		WithSource(from).
   101  		WithDestination(t.Address)
   102  }
   103  
   104  type FA2Approval struct {
   105  	Owner    mavryk.Address `json:"owner"`
   106  	Operator mavryk.Address `json:"operator"`
   107  	TokenId  mavryk.Z       `json:"token_id"`
   108  	Add      bool           `json:"-"`
   109  }
   110  
   111  func (p *FA2Approval) UnmarshalJSON(data []byte) error {
   112  	nested := make(map[string]json.RawMessage)
   113  	err := json.Unmarshal(data, &nested)
   114  	if err != nil {
   115  		return err
   116  	}
   117  	v, ok := nested["add_operator"]
   118  	if ok {
   119  		err = json.Unmarshal(v, p)
   120  		if err != nil {
   121  			return err
   122  		}
   123  		p.Add = true
   124  	} else {
   125  		v, ok = nested["remove_operator"]
   126  		if !ok {
   127  			return fmt.Errorf("invalid FA2 approval data")
   128  		}
   129  		err = json.Unmarshal(v, p)
   130  		if err != nil {
   131  			return err
   132  		}
   133  	}
   134  	return nil
   135  }
   136  
   137  type FA2ApprovalArgs struct {
   138  	TxArgs
   139  	Approvals []FA2Approval `json:"update_operators"`
   140  }
   141  
   142  var _ CallArguments = (*FA2ApprovalArgs)(nil)
   143  
   144  func (p FA2ApprovalArgs) Parameters() *micheline.Parameters {
   145  	params := &micheline.Parameters{
   146  		Entrypoint: "update_operators",
   147  		Value:      micheline.NewSeq(),
   148  	}
   149  	for _, v := range p.Approvals {
   150  		branch := micheline.D_LEFT // add_operator
   151  		if !v.Add {
   152  			branch = micheline.D_RIGHT // remove_operator
   153  		}
   154  		params.Value.Args = append(params.Value.Args, micheline.NewCode(
   155  			branch,
   156  			micheline.NewPair(
   157  				micheline.NewBytes(v.Owner.EncodePadded()),
   158  				micheline.NewPair(
   159  					micheline.NewBytes(v.Operator.EncodePadded()),
   160  					micheline.NewNat(v.TokenId.Big()),
   161  				),
   162  			),
   163  		))
   164  	}
   165  	return params
   166  }
   167  
   168  func NewFA2ApprovalArgs() *FA2ApprovalArgs {
   169  	return &FA2ApprovalArgs{
   170  		Approvals: make([]FA2Approval, 0),
   171  	}
   172  }
   173  
   174  func (a *FA2ApprovalArgs) WithSource(addr mavryk.Address) CallArguments {
   175  	a.Source = addr.Clone()
   176  	return a
   177  }
   178  
   179  func (a *FA2ApprovalArgs) WithDestination(addr mavryk.Address) CallArguments {
   180  	a.Destination = addr.Clone()
   181  	return a
   182  }
   183  
   184  func (p *FA2ApprovalArgs) AddOperator(owner, operator mavryk.Address, id mavryk.Z) *FA2ApprovalArgs {
   185  	if p.Approvals == nil {
   186  		p.Approvals = make([]FA2Approval, 0)
   187  	}
   188  	p.Approvals = append(p.Approvals, FA2Approval{
   189  		Owner:    owner.Clone(),
   190  		Operator: operator.Clone(),
   191  		TokenId:  id,
   192  		Add:      true,
   193  	})
   194  	return p
   195  }
   196  
   197  func (p *FA2ApprovalArgs) RemoveOperator(owner, operator mavryk.Address, id mavryk.Z) *FA2ApprovalArgs {
   198  	if p.Approvals == nil {
   199  		p.Approvals = make([]FA2Approval, 0)
   200  	}
   201  	p.Approvals = append(p.Approvals, FA2Approval{
   202  		Owner:    owner.Clone(),
   203  		Operator: operator.Clone(),
   204  		TokenId:  id.Clone(),
   205  		Add:      true,
   206  	})
   207  	return p
   208  }
   209  
   210  func (p FA2ApprovalArgs) Encode() *codec.Transaction {
   211  	return &codec.Transaction{
   212  		Manager: codec.Manager{
   213  			Source: p.Source,
   214  		},
   215  		Destination: p.Destination,
   216  		Parameters:  p.Parameters(),
   217  	}
   218  }
   219  
   220  type FA2Transfer struct {
   221  	From    mavryk.Address
   222  	To      mavryk.Address
   223  	TokenId mavryk.Z
   224  	Amount  mavryk.Z
   225  }
   226  
   227  func (t FA2Transfer) Prim() micheline.Prim {
   228  	return micheline.NewPair(
   229  		micheline.NewBytes(t.To.EncodePadded()),
   230  		micheline.NewPair(
   231  			micheline.NewNat(t.TokenId.Big()),
   232  			micheline.NewNat(t.Amount.Big()),
   233  		),
   234  	)
   235  }
   236  
   237  type FA2TransferList []FA2Transfer
   238  
   239  func (l FA2TransferList) Len() int { return len(l) }
   240  func (l FA2TransferList) Less(i, j int) bool {
   241  	return bytes.Compare(l[i].From.EncodePadded(), l[j].From.EncodePadded()) < 0
   242  }
   243  func (l FA2TransferList) Swap(i, j int) { l[i], l[j] = l[j], l[i] }
   244  
   245  // compatible with micheline.Value.Unmarshal()
   246  func (t *FA2TransferList) UnmarshalJSON(data []byte) error {
   247  	var xfer struct {
   248  		Transfers []struct {
   249  			From mavryk.Address `json:"from_"`
   250  			Txs  []struct {
   251  				To      mavryk.Address `json:"to_"`
   252  				TokenId mavryk.Z       `json:"token_id"`
   253  				Amount  mavryk.Z       `json:"amount"`
   254  			} `json:"txs"`
   255  		} `json:"transfer"`
   256  	}
   257  	if err := json.Unmarshal(data, &xfer); err != nil {
   258  		return err
   259  	}
   260  	if *t == nil {
   261  		*t = make(FA2TransferList, 0, len(xfer.Transfers))
   262  	}
   263  	for i := range xfer.Transfers {
   264  		for j := range xfer.Transfers[i].Txs {
   265  			// Note: token address is unknown here
   266  			tx := FA2Transfer{
   267  				From:    xfer.Transfers[i].From,
   268  				To:      xfer.Transfers[i].Txs[j].To,
   269  				TokenId: xfer.Transfers[i].Txs[j].TokenId,
   270  				Amount:  xfer.Transfers[i].Txs[j].Amount,
   271  			}
   272  			*t = append(*t, tx)
   273  		}
   274  	}
   275  	return nil
   276  }
   277  
   278  type FA2TransferArgs struct {
   279  	TxArgs
   280  	Transfers FA2TransferList
   281  }
   282  
   283  var _ CallArguments = (*FA2TransferArgs)(nil)
   284  
   285  func NewFA2TransferArgs() *FA2TransferArgs {
   286  	return &FA2TransferArgs{
   287  		Transfers: make(FA2TransferList, 0),
   288  	}
   289  }
   290  
   291  func (a *FA2TransferArgs) WithSource(addr mavryk.Address) CallArguments {
   292  	a.Source = addr.Clone()
   293  	return a
   294  }
   295  
   296  func (a *FA2TransferArgs) WithDestination(addr mavryk.Address) CallArguments {
   297  	a.Destination = addr.Clone()
   298  	return a
   299  }
   300  
   301  func (p *FA2TransferArgs) WithTransfer(from, to mavryk.Address, id, amount mavryk.Z) *FA2TransferArgs {
   302  	if p.Transfers == nil {
   303  		p.Transfers = make(FA2TransferList, 0)
   304  	}
   305  	p.Transfers = append(p.Transfers, FA2Transfer{
   306  		From:    from.Clone(),
   307  		To:      to.Clone(),
   308  		TokenId: id.Clone(),
   309  		Amount:  amount.Clone(),
   310  	})
   311  	return p
   312  }
   313  
   314  func (p *FA2TransferArgs) Optimize() *FA2TransferArgs {
   315  	// stable-sort by `from` address
   316  	sort.Stable(p.Transfers)
   317  	return p
   318  }
   319  
   320  func (t FA2TransferArgs) Parameters() *micheline.Parameters {
   321  	// collate by `from` address
   322  	var k int
   323  	seq := micheline.NewSeq()
   324  	for i, v := range t.Transfers {
   325  		if i == 0 || !v.From.Equal(t.Transfers[i-1].From) {
   326  			seq.Args = append(seq.Args,
   327  				micheline.NewPair(
   328  					micheline.NewBytes(v.From.EncodePadded()),
   329  					micheline.NewSeq(),
   330  				),
   331  			)
   332  			k = len(seq.Args) - 1
   333  		}
   334  		seq.Args[k].Args[1].Args = append(seq.Args[k].Args[1].Args, v.Prim())
   335  	}
   336  	return &micheline.Parameters{
   337  		Entrypoint: "transfer",
   338  		Value:      seq,
   339  	}
   340  }
   341  
   342  func (p FA2TransferArgs) Encode() *codec.Transaction {
   343  	return &codec.Transaction{
   344  		Manager: codec.Manager{
   345  			Source: p.Source,
   346  		},
   347  		Destination: p.Destination,
   348  		Parameters:  p.Parameters(),
   349  	}
   350  }
   351  
   352  // TODO: make it work for internal results as well (so we can use it for crawling)
   353  type FA2TransferReceipt struct {
   354  	tx *rpc.Transaction
   355  }
   356  
   357  func NewFA2TransferReceipt(tx *rpc.Transaction) (*FA2TransferReceipt, error) {
   358  	if tx.Parameters == nil {
   359  		return nil, fmt.Errorf("missing transaction parameters")
   360  	}
   361  	if tx.Parameters.Entrypoint != "transfer" {
   362  		return nil, fmt.Errorf("invalid transfer entrypoint name %q", tx.Parameters.Entrypoint)
   363  	}
   364  	return &FA2TransferReceipt{tx: tx}, nil
   365  }
   366  
   367  func (r FA2TransferReceipt) IsSuccess() bool {
   368  	return r.tx.Result().Status.IsSuccess()
   369  }
   370  
   371  func (r FA2TransferReceipt) Request() FA2TransferList {
   372  	typ := micheline.ITzip12.TypeOf("transfer")
   373  	val := micheline.NewValue(typ, r.tx.Parameters.Value)
   374  	xfer := make(FA2TransferList, 0)
   375  	// FIXME: works only for strictly compliant contracts (i.e. type + annots)
   376  	_ = val.Unmarshal(&xfer)
   377  	return xfer
   378  }
   379  
   380  func (r FA2TransferReceipt) Result() *rpc.Transaction {
   381  	return r.tx
   382  }
   383  
   384  func (r FA2TransferReceipt) Costs() mavryk.Costs {
   385  	return r.tx.Costs()
   386  }
   387  
   388  func (r FA2TransferReceipt) BalanceUpdates() []TokenBalance {
   389  	// TODO: read from ledger bigmap update
   390  	return nil
   391  }