zilpay / zil-pay

ZilPay browser extension enables browsing Zilliqa blockchain enabled websites.
https://zilpay.io
Other
3 stars 1 forks source link

Support NFTs (ZRC-1) in the wallet. #33

Closed VexyCats closed 3 years ago

VexyCats commented 4 years ago

When adding tokens to the new design, I was tempted to insert an NFT address. Of course it doesn't work - says a bad contract address.

So lets get NFTs working on the zilpay wallet.

I've included the smart contract code at the bottom, for ZRC-1 so you can see what is an NFT.

Heres the basics:

  1. Each NFT is unique, so a user can have 50 tokens - but they all live on one contract address.
  2. One contract has many tokens - and each has an owner.
  3. Each token returns a string where you can find the metadata about the token. (Its name, title, image).

To display the NFT in zilpay - zilpay should:

  1. Take the contract address and fetch the state.

  2. Search the state for 3 things: token_owners (array), token_uris (array), total supply (uint256). And the contract init values (name, symbol, owner of contract).

  3. Search for the current users address within the token_owners array to see which tokens they own. A. Optional: Don't search the array but instead allow the user to input the token id of the token that they own. B. (Best option) Expose a method on Zilpay that can be called to add a token to their list of owned tokens. The dapp can then provide the information needed and the token automatically appears in their wallet if they own it

  4. Query the token_uri array for the URL for that token. Then fetch the JSON from that URL and display the "image" field in the wallet.

let url = token_uri[token_id];
let metadata= await fetch(URL);
let name = metadata.name;
console.log(metadata.image) //https://imgur.com/1/myimage
  1. Only needs to check ownership of the token before transferring

Then a user can see their NFTs and can also transfer their NFTs by calling the transfer method in the contract code below.

ZRC-1 used by Mintable and suggested ZRC-1 standard:

scilla_version 0

(***************************************************)
(*               Associated library                *)
(***************************************************)
import BoolUtils
library NonfungibleToken

(* User-defined ADTs *)
type Dummy =
| Dummy

type Operation =
| Add
| Sub

(* Global variables *)
let zero = Uint256 0
let one = Uint256 1
let verdad = Dummy
let add_operation = Add
let sub_operation = Sub

(* Library functions *)
let one_msg = 
  fun (msg : Message) => 
    let nil_msg = Nil {Message} in
    Cons {Message} msg nil_msg

let two_msgs =
  fun (msg1 : Message) =>
  fun (msg2 : Message) =>
    let msgs_tmp = one_msg msg2 in
    Cons {Message} msg1 msgs_tmp

let get_bal =
  fun (some_bal: Option Uint256) =>
    match some_bal with
    | Some bal => bal
    | None => zero
    end

(* Error exception *)
type Error =
  | CodeNotContractOwner
  | CodeIsSelf
  | CodeTokenExists
  | CodeIsNotMinter
  | CodeNotApproved
  | CodeNotTokenOwner
  | CodeNotFound
  | CodeNotApprovedForAll
  | CodeNotOwnerOrOperator
  | CodeNotApprovedSpenderOrOperator

let make_error =
  fun (result : Error) =>
    let result_code = 
      match result with
      | CodeNotContractOwner             => Int32 -1
      | CodeIsSelf                       => Int32 -2
      | CodeTokenExists                  => Int32 -3
      | CodeIsNotMinter                  => Int32 -4
      | CodeNotApproved                  => Int32 -5
      | CodeNotTokenOwner                => Int32 -6
      | CodeNotFound                     => Int32 -7
      | CodeNotApprovedForAll            => Int32 -8
      | CodeNotOwnerOrOperator           => Int32 -9
      | CodeNotApprovedSpenderOrOperator => Int32 -10
      end
    in
    { _exception : "Error"; code : result_code }

(***************************************************)
(*             The contract definition             *)
(***************************************************)

contract NonfungibleToken
(
  contract_owner: ByStr20,
  name : String,
  symbol: String,
  dex_address: ByStr20
)

(* Mutable fields *)

(* Mapping of minters available *)
field minters: Map ByStr20 Dummy =
    let emp_map = Emp ByStr20 Dummy in
    builtin put emp_map contract_owner verdad

(* Mapping between token_id to token_owner *)
field token_owners: Map Uint256 ByStr20 = Emp Uint256 ByStr20

(* Mapping from owner to number of owned tokens *)
field owned_token_count: Map ByStr20 Uint256 = Emp ByStr20 Uint256

(* Mapping between token_id to approved address                              *)
(* @dev: There can only be one approved address per token at any given time. *)
field token_approvals: Map Uint256 ByStr20 = Emp Uint256 ByStr20

(* Mapping of token_owner to operator  *)
field operator_approvals: Map ByStr20 (Map ByStr20 Dummy)
                            = Emp ByStr20 (Map ByStr20 Dummy)

(* Mapping from token_id to token_uri *)
field token_uris: Map Uint256 String = Emp Uint256 String

(* Total token count *)
field total_supply: Uint256 = Uint256 0

(* Emit Errors *)
procedure ThrowError(err : Error)
  e = make_error err;
  throw e
end

procedure IsContractOwner()
  is_contract_owner = builtin eq contract_owner _sender;
  match is_contract_owner with
  | True => 
  | False =>
    err = CodeNotContractOwner;
    ThrowError err
  end
end

procedure IsSelf(address_a: ByStr20, address_b: ByStr20)
  is_self = builtin eq address_a address_b;
  match is_self with
  | False =>
  | True =>
    err = CodeIsSelf;
    ThrowError err
  end
end

procedure IsTokenExists(token_id: Uint256)
  token_exist <- exists token_owners[token_id];
  match token_exist with
  | False =>
  | True =>
    err = CodeTokenExists;
    ThrowError err
  end
end

procedure IsMinter(address: ByStr20)
  is_minter <- exists minters[_sender];
  match is_minter with
  | True =>
  | False =>
    err = CodeIsNotMinter;
    ThrowError err
  end
end

procedure IsTokenOwner(token_id: Uint256, address: ByStr20)
  some_token_owner <- token_owners[token_id];
  match some_token_owner with
  | Some addr => 
    is_token_owner = builtin eq addr address;
    match is_token_owner with
    | True =>
    | False =>
      err = CodeNotTokenOwner;
      ThrowError err
    end
  | None =>
    err = CodeNotFound;
    ThrowError err
  end
end

procedure IsApprovedForAll(token_owner: ByStr20, operator: ByStr20)
  is_operator_approved <- exists operator_approvals[token_owner][operator];
  match is_operator_approved with
  | True =>
  | False =>
    err = CodeNotApprovedForAll;
    ThrowError err
  end
end

procedure IsOwnerOrOperator(token_owner: ByStr20)
  is_owner = builtin eq _sender token_owner;
  is_approved_for_all <- exists operator_approvals[token_owner][_sender];
  is_authorised = orb is_owner is_approved_for_all;
  match is_authorised with
  | True =>
  | False =>
    err = CodeNotOwnerOrOperator;
    ThrowError err
  end
end

procedure IsApprovedSpenderOrOperator(token_id: Uint256, token_owner: ByStr20)
  some_token_approval <- token_approvals[token_id];
  is_approved = match some_token_approval with
    | None => False
    | Some approved_address => 
      builtin eq _sender approved_address
    end;
  is_operator <- exists operator_approvals[token_owner][_sender];
  is_authorised = orb is_approved is_operator;
  match is_authorised with
  | True =>
  | False =>
    err = CodeNotApprovedSpenderOrOperator;
    ThrowError err
  end
end

procedure UpdateTokenCount(operation: Operation, address: ByStr20)
  match operation with
  | Add =>
    some_to_count <- owned_token_count[address];
    new_to_count = 
      let current_count = get_bal some_to_count in
      builtin add current_count one;
    owned_token_count[address] := new_to_count
  | Sub =>
    some_from_count <- owned_token_count[address];
    new_from_count = 
      let current_count = get_bal some_from_count in
        let is_zero = builtin eq current_count zero in
          match is_zero with
          | True => zero
          | False => builtin sub current_count one
          end;
    owned_token_count[address] := new_from_count
  end
end

(* Getter transitions *)

(* @dev: Get number of NFTs assigned to a token_owner *)
transition BalanceOf(address: ByStr20)
  some_bal <- owned_token_count[address];
  balance = get_bal some_bal;
  msg_to_sender = { _tag : "BalanceOfCallBack"; _recipient : _sender; _amount : Uint128 0;
                   balance : balance};
  msgs = one_msg msg_to_sender;
  send msgs
end

(* @dev: Get total supply of NFTs minted *)
transition TotalSupply()
  current_supply <- total_supply;
  msg_to_sender = { _tag : "TotalSupplyCallBack"; _recipient : _sender; _amount : Uint128 0;
                   total_supply : current_supply};
  msgs = one_msg msg_to_sender;
  send msgs
end

(* @dev: Get name of the NFTs *)
transition Name()
  msg_to_sender = { _tag : "NameCallBack"; _recipient : _sender; _amount : Uint128 0;
                   name : name};
  msgs = one_msg msg_to_sender;
  send msgs
end

(* @dev: Get name of the NFTs *)
transition Symbol()
  msg_to_sender = { _tag : "SymbolCallBack"; _recipient : _sender; _amount : Uint128 0;
                   symbol : symbol};
  msgs = one_msg msg_to_sender;
  send msgs
end

(* @dev: Get approved_addr for token_id *)
transition GetApproved(token_id: Uint256)
  some_token_approval <- token_approvals[token_id];
  match some_token_approval with
  | Some addr => 
    msg_to_sender = { _tag : "GetApprovedCallBack"; _recipient : _sender; _amount : Uint128 0; 
                      approved_addr : addr; token_id : token_id};
    msgs = one_msg msg_to_sender;
    send msgs
  | None => 
    err = CodeNotApproved;
    ThrowError err
  end
end

(* @dev: Get the token_uri of a certain token_id *)
transition GetTokenURI(token_id: Uint256)
  some_token_uri <- token_uris[token_id];
  match some_token_uri with
  | Some token_uri =>
    msg_to_sender = { _tag : "GetTokenURICallBack"; _recipient : _sender; _amount : Uint128 0; 
                      token_uri : token_uri};
    msgs = one_msg msg_to_sender;
    send msgs
  | None =>
    err = CodeNotFound;
    ThrowError err
  end
end

(* @dev: Check if a token_id is owned by a token_owner *)
transition CheckTokenOwner(token_id: Uint256, address: ByStr20)
  IsTokenOwner token_id address;
  msg_to_sender = { _tag : "IsOwnerCallBack"; _recipient : _sender; _amount : Uint128 0};
  msgs = one_msg msg_to_sender;
  send msgs
end

(* @dev: Check if address is operator for token_owner *)
transition CheckApprovedForAll(token_owner: ByStr20, operator: ByStr20)
  IsApprovedForAll token_owner operator;
  msg_to_sender = { _tag : "IsApprovedForAllCallBack"; _recipient : _sender; _amount : Uint128 0};
  msgs = one_msg msg_to_sender;
  send msgs
end

(* Interface transitions *)

(* @dev:    Add or remove approved minters. Only contract_owner can approve minters. *)
(* @param:  minter      - Address of the minter to be approved or removed            *)
transition ConfigureMinter(minter: ByStr20)
  IsContractOwner;
  some_minter <- minters[minter];
  match some_minter with
  | Some Dummy => 
    (* Remove minter *)
    delete minters[minter];
    e = {_eventname: "RemovedMinterSuccess"; minter: minter};
    event e
  | None =>
    (* Add minter *)
    minters[minter] := verdad;
    e = {_eventname: "AddMinterSuccess"; minter: minter};
    event e
  end
end

(* @dev:    Mint new tokens. Only contract_owner can mint.         *)
(* @param:  to        - Address of the token recipient             *)
(* @param:  token_id  - ID of the new token to be minted           *)
(* @param:  token_uri -  URI of the the new token to be minted     *)
transition Mint(to: ByStr20, token_id: Uint256, token_uri: String)
  IsTokenExists token_id;
  IsMinter _sender;
  (* Mint new token *)
  token_owners[token_id] := to;
  (* Add to owner count *)
  UpdateTokenCount add_operation to;
  (* Add token_uri for token_id *)
  token_uris[token_id] := token_uri;
  (* Add to total_supply *)
  current_supply <- total_supply;
  new_supply = builtin add current_supply one;
  total_supply := new_supply; 
     token_approvals[token_id] := dex_address;
        (* Emit event *)
        e = {_eventname: "AddApprovalSuccess"; initiator: _sender; approved_addr: dex_address; token: token_id};
        event e;
  e = {_eventname: "MintSuccess"; by: _sender; recipient: to;
        token_id: token_id; token_uri: token_uri};
  event e;
  msg_to_recipient = { _tag : "RecipientAcceptMint"; _recipient : to; _amount : Uint128 0 };
  msg_to_sender = { _tag : "MintCallBack"; _recipient : _sender; _amount : Uint128 0;
                    recipient : to; token_id : token_id; token_uri : token_uri };
  msgs = two_msgs msg_to_recipient msg_to_sender;
  send msgs
end

(* @dev:    Burn existing tokens. Only token_owner or an operator can burn a NFT. *)
(* @param:  token_id - Unique ID of the NFT to be destroyed                       *)
transition Burn(token_id: Uint256)
  (* Check if token exists *)
  some_token_owner <- token_owners[token_id];
  match some_token_owner with
  | None =>
    err = CodeNotFound;
    ThrowError err
  | Some token_owner =>
    IsOwnerOrOperator token_owner;
    (* Destroy existing token *)
    delete token_owners[token_id];
    delete token_approvals[token_id];
    delete token_uris[token_id];
    (* Deduct from owned_token_count *)
    UpdateTokenCount sub_operation token_owner;
    (* Deduct from total_supply *)
    current_supply <- total_supply;
    new_supply = builtin sub current_supply one;
    total_supply := new_supply;
    e = {_eventname: "BurnSuccess"; initiator: _sender; burn_address: token_owner; token: token_id};
    event e;
    msg_to_sender = { _tag : "BurnCallBack"; _recipient : _sender; _amount : Uint128 0;
                      initiator : _sender; burn_address : token_owner; token_id : token_id };
    msgs = one_msg msg_to_sender;
    send msgs
  end
end

(* @dev: Approves OR remove an address ability to transfer a given token_id *)
(* There can only be one approved_spender per token at any given time       *)
(* param: to       - Address to be approved for the given token_id          *)
(* param: token_id - Unique ID of the NFT to be approved                    *)
transition SetApprove(to: ByStr20, token_id: Uint256)
  some_token_owner <- token_owners[token_id];
  match some_token_owner with
  | None =>
    err = CodeNotFound;
    ThrowError err
  | Some token_owner =>
    IsOwnerOrOperator token_owner;
    is_approved <- exists token_approvals[token_id];
    match is_approved with
    | True =>
      (* Remove approved_spender *)
      delete token_approvals[token_id];
      e = {_eventname: "RemoveApprovalSuccess"; initiator: _sender; removed_spender: to; token_id: token_id};
      event e;
      msg_to_sender = { _tag : "RemoveApprovalSuccessCallBack"; _recipient : _sender; _amount : Uint128 0; 
                        removed_spender : to; token_id : token_id };
      msgs = one_msg msg_to_sender;
      send msgs
    | False =>
      (* Add approved_spender *)
      token_approvals[token_id] := to;
      e = {_eventname: "AddApprovalSuccess"; initiator: _sender; approved_addr: to; token: token_id};
      event e;
      msg_to_sender = { _tag : "AddApprovalSuccessCallBack"; _recipient : _sender; _amount : Uint128 0; 
                        approved_spender : to; token_id : token_id };
      msgs = one_msg msg_to_sender;
      send msgs
    end
  end
end

(* @dev: Sets or unsets an operator for the _sender                *)
(* @param: to       - Address to be set or unset as an operator    *)
transition SetApprovalForAll(to: ByStr20)
  IsSelf to _sender;
  is_operator <- exists operator_approvals[_sender][to];
  match is_operator with
  | False =>
    (* Add operator *)
    operator_approvals[_sender][to] := verdad;
    e = {_eventname: "AddApprovalForAllSuccess"; initiator: _sender; operator: to};
    event e
  | True =>
    (* Remove operator *)
    delete operator_approvals[_sender][to];
    e = {_eventname: "RemoveApprovalForAllSuccess"; initiator: _sender; operator: to};
    event e
  end;
  new_status = negb is_operator;
  msg_to_sender = { _tag : "SetApprovalForAllSuccessCallBack"; _recipient : _sender; _amount : Uint128 0; 
                    operator : to; status : new_status};
  msgs = one_msg msg_to_sender;
  send msgs
end

(* @dev: Transfer the ownership of a given token_id to another address. token_owner only transition. *)
(* @param: to       - Recipient address for the token                                                *)
(* @param: token_id - Unique ID of the NFT to be transferred                                         *)
transition Transfer(to: ByStr20, token_id: Uint256)
  IsSelf to _sender;
  IsTokenOwner token_id _sender;
  (* Change token_owner for that token_id *)
  token_owners[token_id] := to;
  (* Delete tokenApproval entry for that token_id *)
  delete token_approvals[token_id];
  (* Subtract one from previous token owner count *)
  UpdateTokenCount sub_operation _sender;
  (* Add one to the new token owner count *)
  UpdateTokenCount add_operation to;
  e = {_eventname: "TransferSuccess"; from: _sender; recipient: to; token: token_id};
  event e;
  msg_to_recipient = { _tag : "RecipientAcceptTransfer"; _recipient : to; _amount : Uint128 0; 
                      from : _sender; recipient : to; token_id : token_id };
  msg_to_sender = { _tag : "TransferSuccessCallBack"; _recipient : _sender; _amount : Uint128 0; 
                    from : _sender; recipient : to; token_id : token_id };
  msgs = two_msgs msg_to_recipient msg_to_sender;
  send msgs
end

(* @dev: Transfer the ownership of a given token_id to another address. approved_spender or operator only transition. *)
(* @param: to       - Recipient address for the NFT                                                                   *)
(* @param: token_id - Unique ID of the NFT to be transferred                                                          *)
transition TransferFrom(to: ByStr20, token_id: Uint256)
  some_token_owner <- token_owners[token_id];
  match some_token_owner with
  | None =>
    err = CodeNotFound;
    ThrowError err
  | Some token_owner =>
    IsSelf to token_owner;
    IsApprovedSpenderOrOperator token_id token_owner;
    (* Change token_owner for that token_id *)
    token_owners[token_id] := to;
    (* Delete tokenApproval entry for that token_id *)
    delete token_approvals[token_id];
    (* Subtract one from previous token owner count *)
    UpdateTokenCount sub_operation token_owner;
    (* Add one to the new token owner count *)
    UpdateTokenCount add_operation to;
    e = {_eventname: "TransferFromSuccess"; from: token_owner; recipient: to; token: token_id};
    event e;
    msg_to_recipient = { _tag : "RecipientAcceptTransferFrom"; _recipient : to; _amount : Uint128 0; 
                        from : token_owner; recipient : to; token_id : token_id };
    msg_to_sender = { _tag : "TransferFromSuccessCallBack"; _recipient : _sender; _amount : Uint128 0; 
                      from : token_owner; recipient : to; token_id : token_id };
    msgs = two_msgs msg_to_recipient msg_to_sender;
    send msgs
  end
end
hicaru commented 4 years ago

thanks for your issue, this first token support release and this is supporting only ZRC2. now it is still in developing.

hicaru commented 3 years ago

This version of NFT has critical bugs, ZIlPay wallet will not support. ZilPay wallet will support it when bug will be fix

VexyCats commented 3 years ago

What critical bugs? Care to elaborate?