1. Soạn hợp đồng thông minh ERC-721

1.1 Cấu trúc tổng thể của MyERC721Card

Dưới đây là mã nguồn đầy đủ của tập tin MyERC721Card.sol.

pragma solidity ^0.5.0;

// https://github.com/OpenZeppelin/openzeppelin-solidity/blob/v2.3.0/contracts/utils/Address.sol
/**
 * @dev Tập hợp các hàm liên quan đến kiểu địa chỉ,
 */
library Address {
    /**
     * @dev Trả về giá trị đúng nếu `tài khoản` là một hợp đồng.
     *
     * Đoạn kiểm thử này không hoàn chỉnh và có thể cho kết quả sai: trong quá trình
     * thực thi hàm khởi tạo của một hợp đồng, địa chỉ của nó có thể được báo cáo
     * là không chứa hợp đồng.
     *
     * > Sẽ không an toàn khi cho rằng một địa chỉ mà hàm này trả về giá trị
     * sai là tài khoản thuộc sở hữu ngoài (EOA) và không phải là hợp đồng.
     */
    function isContract(address tài khoản) internal view returns (bool) {
        // Phương thức này dựa trên extcodesize, nó trả về giá trị 0 cho các hợp đồng
        // trong quá trình khởi tạo, vì mã chỉ được lưu trữ vào cuối
        // quá trình thực thi hàm khởi tạo.

        uint256 size;
        // solhint-disable-next-line no-inline-assembly
        assembly { size := extcodesize(tài khoản) }
        return size > 0;
    }
}

// https://github.com/OpenZeppelin/openzeppelin-solidity/blob/v2.3.0/contracts/math/SafeMath.sol
/**
 * @dev Các lớp bọc cho các phép toán số học của Solidity với
 * kiểm tra thêm về tràn số.
 *
 * Các phép toán số học trong Solidity sẽ bao bọc khi tràn số. Điều này có thể dễ dàng dẫn đến
 * lỗi, vì các nhà phát triển thường giả định rằng tràn số sẽ gây ra một lỗi,
 * đây là hành vi tiêu chuẩn trong các ngôn ngữ lập trình cao cấp.
 * `SafeMath` khôi phục lại giả thuyết này bằng cách hoàn ngược giao dịch
 * khi một phép toán tràn số xảy ra.
 *
 * Sử dụng thư viện thay vì các phép toán chưa kiểm tra sẽ loại bỏ toàn bộ
 * các loại lỗi, do đó khuyến nghị luôn sử dụng thư viện.
 */
library SafeMath {
    /**
     * @dev Trả về tổng của hai số nguyên không dấu, hoàn ngược nếu có
     * tràn số.
     *
     * Tương đương với toán tử `+` trong Solidity.
     *
     * Yêu cầu:
     * - Phép cộng không được tràn số.
     */
    function add(uint256 a, uint256 b) internal pure returns (uint256) {
        uint256 c = a + b;
        require(c >= a, "SafeMath: phép cộng tràn số");

        return c;
    }

    /**
     * @dev Trả về hiệu của hai số nguyên không dấu, hoàn ngược nếu có
     * tràn số (khi kết quả âm).
     *
     * Tương đương với toán tử `-` trong Solidity.
     *
     * Yêu cầu:
     * - Phép trừ không được tràn số.
     */
    function sub(uint256 a, uint256 b) internal pure returns (uint256) {
        require(b <= a, "SafeMath: phép trừ tràn số");
        uint256 c = a - b;

        return c;
    }

    /**
     * @dev Trả về tích của hai số nguyên không dấu, hoàn ngược nếu có
     * tràn số.
     *
     * Tương đương với toán tử `*` trong Solidity.
     *
     * Yêu cầu:
     * - Phép nhân không được tràn số.
     */
    function mul(uint256 a, uint256 b) internal pure returns (uint256) {
        // Tối ưu hóa gas: điều này tiết kiệm gas hơn so với yêu cầu 'a' không bằng không, nhưng
        // lợi ích sẽ bị mất nếu 'b' cũng được kiểm thử.
        // See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522
        if (a == 0) {
            return 0;
        }

        uint256 c = a * b;
        require(c / a == b, "SafeMath: phép nhân tràn số");

        return c;
    }

    /**
     * @dev Trả về giá trị nguyên của phép chia hai số nguyên không dấu. Hoàn ngược nếu có
     * phép chia cho số 0. Kết quả được làm tròn về 0.
     *
     * Tương đương với toán tử `/` trong Solidity. Lưu ý: hàm này sử dụng một
     * mã vận hành `revert` (giữ lại toàn bộ gas còn lại) trong khi Solidity
     *  sử dụng một mã vận hành không hợp lệ để hoàn ngược (tiêu thụ toàn bộ gas còn lại).
     *
     * Yêu cầu:
     * - Số chia không được bằng 0.
     */
    function div(uint256 a, uint256 b) internal pure returns (uint256) {
        // Solidity chỉ tự động kiểm tra khi chia cho 0
        require(b > 0, "SafeMath: division by zero");
        uint256 c = a / b;
        // assert(a == b * c + a % b); // Không có trường hợp nào mà điều này không đúng

        return c;
    }

    /**
     * @dev Trả về phần dư của phép chia hai số nguyên không dấu. (unsigned integer modulo),
     * Hoàn ngược khi chia cho 0.
     *
     * Tương đương với toán tử `%` trong Solidity. Hàm này sử dụng một
     * mã vận hành `revert` (giữ lại toàn bộ gas còn lại) trong khi Solidity
     *  sử dụng một mã vận hành không hợp lệ để hoàn ngược (tiêu thụ toàn bộ gas còn lại).
     *
     * Yêu cầu:
     * - Số chia không được bằng 0.
     */
    function mod(uint256 a, uint256 b) internal pure returns (uint256) {
        require(b != 0, "SafeMath: số chia bằng 0");
        return a % b;
    }
}

// https://github.com/OpenZeppelin/openzeppelin-solidity/blob/v2.3.0/contracts/drafts/Counters.sol
/**
 * @title Counters
 * @author Matt Condon (@shrugs)
 * @dev Cung cấp các số đếm chỉ có thể được tăng hoặc giảm đi một đơn vị. Hàm này có thể được sử dụng, ví dụ, để theo dõi số lượng
 * phần tử trong một ánh xạ, phát hành id ERC721, hoặc đếm số id yêu cầu.
 *
 * Bao gồm `using Counters for Counters.Counter;`
 * Bởi vì không thể làm tràn một số nguyên 256 bit với giá trị gia tăng một đơn vị, `increment` có thể bỏ qua kiểm tra tràn số SafeMath,
 * do đó giúp tiết kiệm gas. Tuy nhiên, điều này giả định việc sử dụng chính xác, trong đó giá trị `_value` gốc không bao giờ
 * được truy cập trực tiếp.
 */
library Counters {
    using SafeMath for uint256;

    struct Counter {
        // Người dùng của thư viện không bao giờ được truy cập trực tiếp biến này: tương tác phải được giới hạn ở
        // các hàm của thư viện. Kể từ Solidity v0.5.2, không thể thực thi hàm này, mặc dù có đề xuất bổ sung
        // tính năng này: xem https://github.com/ethereum/solidity/issues/4637
        uint256 _value; // default: 0
    }

    function current(Counter storage counter) internal view returns (uint256) {
        return counter._value;
    }

    function increment(Counter storage counter) internal {
        counter._value += 1;
    }

    function decrement(Counter storage counter) internal {
        counter._value = counter._value.sub(1);
    }
}

/**
 * @dev Giao diện của tiêu chuẩn ERC165, như được xác định trong
 * [EIP](https://eips.ethereum.org/EIPS/eip-165).
 *
 * Người triển khai có thể tuyên bố hỗ trợ giao diện của hợp đồng, sau đó giao diện đó
 * có thể được truy vấn bởi người khác (`ERC165Checker`).
 *
 * Đối với việc triển khai, xem `ERC165`.
 */
interface IERC165 {
    /**
     * @dev Trả về giá trị đúng nếu hợp đồng này triển khai giao diện được xác định bởi
     * `interfaceId`. Xem phần tương ứng
     * [EIP section](https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified)
     * để tìm hiểu thêm về cách tạo các id này.
     *
     * Lệnh gọi hàm này phải sử dụng dưới 30 000 gas.
     */
    function supportsInterface(bytes4 interfaceId) external view returns (bool);
}

/**
 * @dev Triển khai giao diện `IERC165`.
 *
 * Các hợp đồng có thể kế thừa từ hợp đồng này và gọi `_registerInterface` để tuyên bố
 * hỗ trợ cho một giao diện.
 */
contract ERC165 is IERC165 {
    /*
     * bytes4(keccak256('supportsInterface(bytes4)')) == 0x01ffc9a7
     */
    bytes4 private constant _INTERFACE_ID_ERC165 = 0x01ffc9a7;

    /**
     * @dev Ánh xạ các id giao diện cho dù có hỗ trợ giao diện đó hay không.
     */
    mapping(bytes4 => bool) private _supportedInterfaces;

    constructor () internal {
        // Các hợp đồng có được chỉ cần đăng ký hỗ trợ giao diện của chính mình,
        // chúng tôi đăng ký hỗ trợ ERC165 tại đây
        _registerInterface(_INTERFACE_ID_ERC165);
    }

    /**
     * @dev Xem `IERC165.supportsInterface`.
     *
     * Độ phức tạp thời gian O(1), đảm bảo luôn sử dụng dưới 30 000 gas.
     */
    function supportsInterface(bytes4 interfaceId) external view returns (bool) {
        return _supportedInterfaces[interfaceId];
    }

    /**
     * @dev Đăng ký hợp đồng làm hợp đồng triển khai của giao diện như xác định trong
     * `interfaceId`. Hỗ trợ giao diện ERC165 thực tế diễn ra tự động và
     * không cần đăng ký giao diện của tiêu chuẩn này.
     *
     * Xem `IERC165.supportsInterface`.
     *
     * Yêu cầu:
     *
     * - `interfaceId` không được là giao diện không hợp lệ ERC165 (`0xffffffff`).
     */
    function _registerInterface(bytes4 interfaceId) internal {
        require(interfaceId != 0xffffffff, "ERC165: id giao diện không thành công");
        _supportedInterfaces[interfaceId] = true;
    }
}

/**
 * @dev Giao diện yêu cầu của hợp đồng tuân thủ ERC721.
 */
contract IERC721 là IERC165 {
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);

    /**
     * @dev Trả về số lượng NFT trong tài khoản của `owner`.
     */
    function balanceOf(address owner) public view returns (uint256 balance);

    /**
     * @dev Trả về chủ sở hữu của NFT được xác định bởi `tokenId`.
     */
    function ownerOf(uint256 tokenId) public view returns (address owner);

    /**
     * @dev Chuyển một NFT cụ thể (`tokenId`) từ một tài khoản (`from`) sang
     * tài khoản khác (`to`).
     *
     * 
     *
     * Yêu cầu:
     * - `from`, `to` không được là địa chỉ không hợp lệ.
     * - `tokenId` phải thuộc sở hữu của `from`.
     * - Nếu người gọi không phải là `from`, NFT này phải được phép chuyển
     * bằng lệnh `approve` hoặc `setApproveForAll`.
     */
    function safeTransferFrom(address from, address to, uint256 tokenId) public;
    /**
     * @dev Chuyển một NFT cụ thể (`tokenId`) từ một tài khoản (`from`) sang
     * một tài khoản khác (`to`).
     *
     * Yêu cầu:
     * - Nếu người gọi không phải là `from`, NFT này phải được phê duyệt để chuyển 
     * bằng lệnh `approve` hoặc `setApproveForAll`.
     */
    function transferFrom(address from, address to, uint256 tokenId) public;
    function approve(address to, uint256 tokenId) public;
    function getApproved(uint256 tokenId) public view returns (address operator);

    function setApprovalForAll(address operator, bool _approved) public;
    function isApprovedForAll(address owner, address operator) public view returns (bool);


    function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public;
} 

// https://github.com/OpenZeppelin/openzeppelin-solidity/blob/v2.3.0/contracts/token/ERC721/IERC721Receiver.sol
/**
 * @title ERC721 giao diện nhận token
 * @dev Giao diện cho bất kỳ hợp đồng nào muốn hỗ trợ safeTransfers
 * từ các hợp đồng tài sản ERC721.
 */
contract IERC721Receiver {
    /**
     * @notice Xử lý việc nhận NFT
     * @dev Hợp đồng thông minh ERC721 gọi hàm này trên người nhận
     * sau một `safeTransfer`. Hàm này PHẢI trả về bộ lọc hàm,
     * nếu không người gọi sẽ hoàn tác giao dịch. Bộ lọc hàm để
     * trả về có thể được lấy bằng hàm `this.onERC721Received.selector`. Hàm
     * này CÓ THỂ throw để hoàn ngược và từ chối giao dịch chuyển.
     * Lưu ý: địa chỉ hợp đồng ERC721 luôn luôn là người gửi thông báo.
     * @param operator The address which called `safeTransferFrom` function
     * @param from The address which previously owned the token
     * @param tokenId The NFT identifier which is being transferred
     * @param data Additional data with no specified format
     * @return bytes4 `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`
     */
    function onERC721Received(address operator, address from, uint256 tokenId, bytes memory data)
    public returns (bytes4);
}

// https://github.com/OpenZeppelin/openzeppelin-solidity/blob/v2.3.0/contracts/token/ERC721/ERC721.sol
contract ERC721 is ERC165, IERC721 {
    using SafeMath for uint256;
    using Address for address;
    using Counters for Counters.Counter;

    // Equals to `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`
    // which can be also obtained as `IERC721Receiver(0).onERC721Received.selector`
    bytes4 private constant _ERC721_RECEIVED = 0x150b7a02;

    // Mapping from token ID to owner
    mapping (uint256 => address) private _tokenOwner;

    // Mapping from token ID to approved address
    mapping (uint256 => address) private _tokenApprovals;

    // Mapping from owner to number of owned token
    mapping (address => Counters.Counter) private _ownedTokensCount;

    // Mapping from owner to operator approvals
    mapping (address => mapping (address => bool)) private _operatorApprovals;

    /*
     *     bytes4(keccak256('balanceOf(address)')) == 0x70a08231
     *     bytes4(keccak256('ownerOf(uint256)')) == 0x6352211e
     *     bytes4(keccak256('approve(address,uint256)')) == 0x095ea7b3
     *     bytes4(keccak256('getApproved(uint256)')) == 0x081812fc
     *     bytes4(keccak256('setApprovalForAll(address,bool)')) == 0xa22cb465
     *     bytes4(keccak256('isApprovedForAll(address,address)')) == 0xe985e9c
     *     bytes4(keccak256('transferFrom(address,address,uint256)')) == 0x23b872dd
     *     bytes4(keccak256('safeTransferFrom(address,address,uint256)')) == 0x42842e0e
     *     bytes4(keccak256('safeTransferFrom(address,address,uint256,bytes)')) == 0xb88d4fde
     *
     *     => 0x70a08231 ^ 0x6352211e ^ 0x095ea7b3 ^ 0x081812fc ^
     *        0xa22cb465 ^ 0xe985e9c ^ 0x23b872dd ^ 0x42842e0e ^ 0xb88d4fde == 0x80ac58cd
     */
    bytes4 private constant _INTERFACE_ID_ERC721 = 0x80ac58cd;

    constructor () public {
        // register the supported interfaces to conform to ERC721 via ERC165
        _registerInterface(_INTERFACE_ID_ERC721);
    }

    /**
     * @dev Lấy số dư của địa chỉ được chỉ định.
     * @param owner address to query the balance of
     * @return uint256 representing the amount owned by the passed address
     */
    function balanceOf(address owner) public view returns (uint256) {
        require(owner != address(0), "ERC721: truy vấn số dư cho địa chỉ không hợp lệ");

        return _ownedTokensCount[owner].current();
    }

    /**
     * @dev Nhận chủ sở hữu của ID token được chỉ định.
     * @param tokenId uint256 ID của token để truy vấn chủ sở hữu của
     * @return địa chỉ hiện được đánh dấu là chủ sở hữu của ID token đã cho
     */
    function ownerOf(uint256 tokenId) public view returns (address) {
        address owner = _tokenOwner[tokenId];
        require(owner != address(0), "ERC721: truy vấn chủ sở hữu cho mã thông báo không tồn tại");

        return owner;
    }

    /**
     * @dev Phê duyệt một địa chỉ khác để chuyển ID token đã cho
     * Địa chỉ không hợp lệ cho biết không có địa chỉ đã phê duyệt.
     * Chỉ có thể có một địa chỉ được phê duyệt cho mỗi token tại một thời điểm nhất định.
     * Chỉ có thể được gọi bởi chủ sở hữu token hoặc người vận hành đã được phê duyệt.
     * @param to address to be approved for the given token ID
     * @param tokenId uint256 ID of the token to be approved
     */
    function approve(address to, uint256 tokenId) public {
        address owner = ownerOf(tokenId);
        require(to != owner, "ERC721: phê duyệt cho chủ sở hữu hiện tại");

        require(msg.sender == owner || isApprovedForAll(owner, msg.sender),
            "ERC721: approve caller is not owner nor approved for all"
        );

        _tokenApprovals[tokenId] = to;
        emit Approval(owner, to, tokenId);
    }

    /**
     * @dev Lấy địa chỉ đã phê duyệt của ID token, hoặc địa chỉ là zero nếu chưa thiết lập địa chỉ
     * Hoàn ngược nếu ID token không tồn tại.
     * @param tokenId uint256 ID of the token to query the approval of
     * @return address currently approved for the given token ID
     */
    function getApproved(uint256 tokenId) public view returns (address) {
        require(_exists(tokenId), "ERC721: truy vấn được phê duyệt cho token không tồn tại");

        return _tokenApprovals[tokenId];
    }

    /**
     * @dev Thiết lập hoặc bỏ thiết lập phê duyệt người vận hành định trước
     * Người vận hành được phép chuyển tất cả token của người gửi thay mặt cho họ.
     * @param to operator address to set the approval
     * @param approved representing the trạng thái of the approval to be set
     */
    function setApprovalForAll(address to, bool approved) public {
        require(to != msg.sender, "ERC721: phê duyệt cho người gọi");

        _operatorApprovals[msg.sender][to] = approved;
        emit ApprovalForAll(msg.sender, to, approved);
    }

    /**
     * @dev Cho biết liệu người vận hành có được chủ sở hữu định trước phê duyệt hay không.
     * @param owner owner address which you want to query the approval of
     * @param operator operator address which you want to query the approval of
     * @return bool whether the given operator is approved by the given owner
     */
    function isApprovedForAll(address owner, address operator) public view returns (bool) {
        return _operatorApprovals[owner][operator];
    }

    /**
     * @dev Chuyển quyền sở hữu của một ID token cụ thể cho một địa chỉ khác.
     * Không khuyến nghị sử dụng phương pháp này, hãy sử dụng `safeTransferFrom` trong tất cả trường hợp có thể.
     * msg.sender bắt buộc phải là chủ sở hữu, được phê duyệt hoặc người vận hành.
     * @param from current owner of the token
     * @param to address to receive the ownership of the given token ID
     * @param tokenId uint256 ID of the token to be transferred
     */
    function transferFrom(address from, address to, uint256 tokenId) public {
        //solhint-disable-next-line max-line-length
        require(_isApprovedOrOwner(msg.sender, tokenId), "ERC721: transfer caller is not owner nor approved");

        _transferFrom(from, to, tokenId);
    }

    /**
     * @dev Chuyển quyền sở hữu của ID token đã cho một cách an toàn đến một địa chỉ khác
     * Nếu địa chỉ đích là một hợp đồng, nó phải triển khai hàm `onERC721Received`,
     * được gọi khi thực hiện chuyển giao an toàn và trả về giá trị magic
     * `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`; nếu không,
     * quá trình chuyển giao sẽ bị hoàn ngược.
     * msg.sender bắt buộc phải là chủ sở hữu hoặc người vận hành được phê duyệt.
     * @param from địa chỉ hiện tại của chủ sở hữu token
     * @param to địa chỉ nhận quyền sở hữu của ID token đã cho
     * @param tokenId uint256 ID của token sẽ được chuyển quyền sở hữu
     */
    function safeTransferFrom(address from, address to, uint256 tokenId) public {
        safeTransferFrom(from, to, tokenId, "");
    }

    /**
     * @dev Chuyển quyền sở hữu của ID token đã cho một cách an toàn đến một địa chỉ khác
     * Nếu địa chỉ đích là một hợp đồng, nó phải triển khai hàm `onERC721Received`,
     * được gọi khi thực hiện chuyển giao an toàn và trả về giá trị magic
     * `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`; otherwise,
     * quá trình chuyển giao sẽ bị hoàn ngược.
     * msg.sender bắt buộc phải là chủ sở hữu hoặc người vận hành được phê duyệt.
     * @param from current owner of the token
     * @param to address to receive the ownership of the given token ID
     * @param tokenId uint256 ID of the token to be transferred
     * @param _data bytes data to send along with a safe transfer check
     */
    function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory _data) public {
        transferFrom(from, to, tokenId);
        require(_checkOnERC721Received(from, to, tokenId, _data), "ERC721: transfer to non ERC721Receiver implementer");
    }

    /**
     * @dev Trả về kết quả token chỉ định có tồn tại hay không.
     * @param tokenId uint256 ID of the token to query the existence of
     * @return bool whether the token exists
     */
    function _exists(uint256 tokenId) internal view returns (bool) {
        address owner = _tokenOwner[tokenId];
        return owner != address(0);
    }

    /**
     * @dev Trả về thông tin về việc người được ủy quyền định trước có thể chuyển giao một ID token cụ thể hay không.
     * @param spender address of the spender to query
     * @param tokenId uint256 ID of the token to be transferred
     * @return bool whether the msg.sender is approved for the given token ID,
     * is an operator of the owner, or is the owner of the token
     */
    function _isApprovedOrOwner(address spender, uint256 tokenId) internal view returns (bool) {
        require(_exists(tokenId), "ERC721: truy vấn toán tử cho token không tồn tại");
        address owner = ownerOf(tokenId);
        return (spender == owner || getApproved(tokenId) == spender || isApprovedForAll(owner, spender));
    }

    /**
     * @dev Hàm nội bộ để tạo một token mới.
     * Hoàn ngược nếu ID token đã cho đã tồn tại.
     * @param to The address that will own the minted token
     * @param tokenId uint256 ID of the token to be minted
     */
    function _mint(address to, uint256 tokenId) internal {
        require(to != address(0), "ERC721: tạo đến địa chỉ không hợp lệ");
        require(!_exists(tokenId), "ERC721: token đã được tạo");

        _tokenOwner[tokenId] = to;
        _ownedTokensCount[to].increment();

        emit Transfer(address(0), to, tokenId);
    }

    /**
     * @dev Hàm nội bộ để đốt token cụ thể.
     * Hoàn ngược nếu token không tồn tại.
     * Đã lỗi thời, sử dụng _burn(uint256) để thay thế.
     * @param owner owner of the token to burn
     * @param tokenId uint256 ID of the token being burned
     */
    function _burn(address owner, uint256 tokenId) internal {
        require(ownerOf(tokenId) == owner, "ERC721: đốt token không thuộc sở hữu");

        _clearApproval(tokenId);

        _ownedTokensCount[owner].decrement();
        _tokenOwner[tokenId] = address(0);

        emit Transfer(owner, address(0), tokenId);
    }

    /**
     * @dev Hàm nội bộ để đốt token cụ thể.
     * Hoàn ngược nếu token không tồn tại.
     * @param tokenId uint256 ID of the token being burned
     */
    function _burn(uint256 tokenId) internal {
        _burn(ownerOf(tokenId), tokenId);
    }

    /**
     * @dev Hàm nội bộ để chuyển quyền sở hữu ID token cụ thể cho một địa chỉ khác.
     * Không giống như transferFrom, hàm này không áp đặt bất kỳ hạn chế nào đối với msg.sender.
     * @param from current owner of the token
     * @param to address to receive the ownership of the given token ID
     * @param tokenId uint256 ID of the token to be transferred
     */
    function _transferFrom(address from, address to, uint256 tokenId) internal {
        require(ownerOf(tokenId) == from, "ERC721: chuyển token không thuộc sở hữu");
        require(to != address(0), "ERC721: chuyển đến địa chỉ không hợp lệ");

        _clearApproval(tokenId);

        _ownedTokensCount[from].decrement();
        _ownedTokensCount[to].increment();

        _tokenOwner[tokenId] = to;

        emit Transfer(from, to, tokenId);
    }

    /**
     * @dev Hàm nội bộ để gọi `onERC721Received` trên địa chỉ đích.
     * Lệnh gọi không được thực hiện nếu địa chỉ đích không phải là hợp đồng.
     *
     * Hàm này đã lỗi thời.
     * @param from address representing the previous owner of the given token ID
     * @param to target address that will receive the tokens
     * @param tokenId uint256 ID of the token to be transferred
     * @param _data bytes optional data to send along with the call
     * @return bool whether the call correctly returned the expected magic value
     */
    function _checkOnERC721Received(address from, address to, uint256 tokenId, bytes memory _data)
        internal returns (bool)
    {
        if (!to.isContract()) {
            return true;
        }

        bytes4 retval = IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, _data);
        return (retval == _ERC721_RECEIVED);
    }

    /**
     * @dev Hàm riêng tư để xóa trạng thái chấp thuận hiện tại của ID token đã cho.
     * @param tokenId uint256 ID of the token to be transferred
     */
    function _clearApproval(uint256 tokenId) private {
        if (_tokenApprovals[tokenId] != address(0)) {
            _tokenApprovals[tokenId] = address(0);
        }
    }
}

contract MyERC721Card is ERC721{

    struct Card {
        string  name;  // Name of the Card
        uint256 level; // Level of the Card
    }

    Card[] public cards; // First Item has Index 0
    address public owner;

    constructor () public {
        owner = msg.sender; // owner of MyERC721Card contract who can create a new card
    }

    function mintCard(string memory name, address tài khoản) public {
        require(owner == msg.sender); // Chỉ Chủ sở hữu mới có thể tạo các mục
        uint256 cardId = cards.length; // Unique card ID
        cards.push(Card(name, 1));
        _mint(tài khoản, cardId); // Mint a new card
    }

}

MyERC721Card.sol bao gồm một giao diện(IERC165), ba thư viện(Address, SafeMathCounters) và bốn hợp đồng(ERC165, IERC721, IERC721ReceiverMyERC721Card).

  • Giao diện IERC165 xác định giao diện được mô tả trong tiêu chuẩn kỹ thuật của ERC-165.

  • Thư viện Address xác định phương pháp isContract để kiểm tra xem tài khoản có phải là hợp đồng hay không.

  • Thư viện SafeMath xác định các lớp bọc (wrapper) cho các phép toán số học trong Solidity, kèm theo kiểm tra tràn số để đảm bảo tính toàn vẹn của phép tính với kiểu uint256 trong Solidity.

  • Thư viện Counters xác định các bộ đếm chỉ có thể tăng hoặc giảm đi một đơn vị. Thư viện này được sử dụng để theo dõi số lượng phần tử khi phát hành các id ERC721.

  • Hợp đồng ERC165 triển khai giao diện IERC165.

  • Hợp đồng IERC721 xác định giao diện được mô tả trong tiêu chuẩn kỹ thuật của ERC-721, bao gồm cả ERC-165.

  • Hợp đồng IERC721Receiver xác định onERC721Received được sử dụng từ hợp đồng MyERC721Card.

  • Hợp đồng ERC721 triển khai IERC721ERC165.

  • Hợp đồng MyERC721Card triển khai một token không thể thay thế (NFT) theo kiểu thẻ với tên và cấp độ sử dụng hợp đồng ERC721 và chỉ chủ sở hữu hợp đồng MyERC721Card mới có thể tạo các thẻ mới.

1.2 Tìm hiểu một số phương pháp quan trọng

Hãy tìm hiểu chi tiết một số phương pháp quan trọng.

(1) constructor của ERC721 và _INTERFACE_ID_ERC721

constructor đăng ký _INTERFACE_ID_ERC721 là hàm băm 4 byte bắt nguồn từ các giao diện ERC-721 như sau.

    /*
     *     bytes4(keccak256('balanceOf(address)')) == 0x70a08231
     *     bytes4(keccak256('ownerOf(uint256)')) == 0x6352211e
     *     bytes4(keccak256('approve(address,uint256)')) == 0x095ea7b3
     *     bytes4(keccak256('getApproved(uint256)')) == 0x081812fc
     *     bytes4(keccak256('setApprovalForAll(address,bool)')) == 0xa22cb465
     *     bytes4(keccak256('isApprovedForAll(address,address)')) == 0xe985e9c
     *     bytes4(keccak256('transferFrom(address,address,uint256)')) == 0x23b872dd
     *     bytes4(keccak256('safeTransferFrom(address,address,uint256)')) == 0x42842e0e
     *     bytes4(keccak256('safeTransferFrom(address,address,uint256,bytes)')) == 0xb88d4fde
     *
     *     => 0x70a08231 ^ 0x6352211e ^ 0x095ea7b3 ^ 0x081812fc ^
     *        0xa22cb465 ^ 0xe985e9c ^ 0x23b872dd ^ 0x42842e0e ^ 0xb88d4fde == 0x80ac58cd
     */
    bytes4 private constant _INTERFACE_ID_ERC721 = 0x80ac58cd;

    constructor () public {
        // register the supported interfaces to conform to ERC721 via ERC165
        _registerInterface(_INTERFACE_ID_ERC721);
    }

Sau khi đăng ký, giao diện supportsInterface của hợp đồng ERC-721 và ERC-165 trả về giá trị true khi được gọi cho _INTERFACE_ID_ERC721 và cho biết hợp đồng này đang triển khai các giao diện ERC-721.

(2) function balanceOf(address owner) public view returns (uint256 balance);

balanceOf là một phương pháp bắt buộc của ERC-721. balanceOf trả về số lượng token NFT trong tài khoản của owner.

    function balanceOf(address owner) public view returns (uint256) {
        require(owner != address(0), "ERC721: balance query for the zero address");

        return _ownedTokensCount[owner].current();
    }

balanceOf chỉ trả về số lượng hiện có từ đối tượng Counterowner duy trì trong _ownedTokensCount.

    // Ánh xạ từ chủ sở hữu đến số token thuộc sở hữu
    mapping (address => Counters.Counter) private _ownedTokensCount;

(3) safeTransferFromtransferFrom

Các hàm này chuyển quyền sở hữu của một ID token cụ thể cho một địa chỉ khác. ERC-721 yêu cầu hai phương pháp safeTransferFrom, một phương pháp có data và một phương pháp không có data. Cả hai phương pháp hoạt động tương tự nhau, chỉ khác nhau ở chỗ phương pháp không có data chỉ đặt data"". safeTransferFrom gọi hàm transferFrom nếu có nhiều lần kiểm tra hơn như dưới đây và safeTransferFrom được ưu tiên sử dụng hơn so với transferFrom, đây là phương pháp bắt buộc của ERC-721.

    function safeTransferFrom(address from, address to, uint256 tokenId) public {
        safeTransferFrom(from, to, tokenId, "");
    }

    function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory _data) public {
        transferFrom(from, to, tokenId);
        require(_checkOnERC721Received(from, to, tokenId, _data), "ERC721: transfer to non ERC721Receiver implementer");
    }

    function transferFrom(address from, address to, uint256 tokenId) public {
        //solhint-disable-next-line max-line-length
        require(_isApprovedOrOwner(msg.sender, tokenId), "ERC721: transfer caller is not owner nor approved");

        _transferFrom(from, to, tokenId);
    }

safeTransferFrom kiểm tra xem địa chỉ to có thể nhận token hay không. _checkOnERC721Received có logic xác minh. Nếu địa chỉ to là một hợp đồng thì hợp đồng đó phải thực hiện giao diện onERC721Received của ERC-721 và trả về hàm băm 4 byte chính xác để nhận token ERC-721 như sau.

    function _checkOnERC721Received(address from, address to, uint256 tokenId, bytes memory _data)
        internal returns (bool)
    {
        if (!to.isContract()) {
            return true;
        }

        bytes4 retval = IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, _data);
        return (retval == _ERC721_RECEIVED);
    }

_transferFrom trên thực tế chuyển quyền sở hữu của tokenId đã cho như dưới đây.

    function _transferFrom(address from, address to, uint256 tokenId) internal {
        require(ownerOf(tokenId) == from, "ERC721: chuyển token không thuộc sở hữu");
        require(to != address(0), "ERC721: chuyển đến địa chỉ không hợp lệ");

        _clearApproval(tokenId);

        _ownedTokensCount[from].decrement();
        _ownedTokensCount[to].increment();

        _tokenOwner[tokenId] = to;

        emit Transfer(from, to, tokenId);
    }

(4) function _mint(address to, uint256 tokenId) internal

_mint không phải là một phần của ERC-721. Tuy nhiên, chúng tôi cần một cách để tạo ra các token ERC-721 mới và đã áp dụng _mint để tạo ra các token mới trong lần triển khai này như sau.

    function _mint(address to, uint256 tokenId) internal {
        require(to != address(0), "ERC721: tạo đến địa chỉ không hợp lệ");
        require(!_exists(tokenId), "ERC721: token đã được tạo");

        _tokenOwner[tokenId] = to;
        _ownedTokensCount[to].increment();

        emit Transfer(address(0), to, tokenId);
    }

_mint là một phương pháp nội bộ và có thể được gọi bên trong hợp đồng này. Trong MyERC721Card.sol, _mint chỉ được gọi từ phương pháp mintCard trong hợp đồng MyERC721Card. Chỉ có chủ sở hữu hợp đồng thông minh mới có thể gọi mintCard.

    function mintCard(string name, address tài khoản) public {
        require(owner == msg.sender); // Chỉ Chủ sở hữu mới có thể tạo các mục
        uint256 cardId = cards.length; // ID thẻ duy nhất
        cards.push(Card(name, 1));
        _mint(tài khoản, cardId); // Tạo thẻ mới
    }

Last updated