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 }