NFTs

대체 불가능 토큰(Non-Fungible Token, NFT)은 각 디지털 아트의 진위를 증명하는 블록체인에 저장된 디지털 자산입니다. 각 단위는 상호 교환이 불가능하여 기본 자산이 고유의 형태로 소유 가능하게 유지되며, 블록체인 네트워크는 진위 여부를 인증하고 소유권을 증명하는 중재자 역할을 합니다. 세번째공간 플랫폼에서 판매되는 모든 디지털 아트와 제품은 NFT로 표시되며, 세번째공간이 기획하거나 참여하는 전시회 및 이벤트에 출품되는 디지털 아트는 현재 소유자와 창작자의 공개 키로 관리됩니다.

세번째공간은 Klaytn 네트워크에서 KIP-17 및 KIP-7, Ethereum 네트워크에서 ERC-721 및 ERC-20 표준을 구현하며 Bitcoin에서는 오디널스 프로토콜을 이용합니다.

Klaytn 네트워크

KIP-17은 대체 불가능 토큰(NFT)의 표준 인터페이스로, 이 계약은 세번째공간이 Klaytn에서 NFT 토큰을 발행하고 전송하는 데 사용됩니다. KIP-17은 Ethereum의 ERC-721에서 파생되어 대부분의 기능이 ERC-721과 호환되도록 구현되었습니다. 주요 기능은 다음과 같습니다:

  • 모든 토큰은 발행/전송/소각에 대한 이벤트 로그를 추적해야 합니다. 이는 전송 관련 모든 작업에 대해 Transfer 이벤트가 내보내져야 함을 의미합니다.

  • KIP-17은 ERC-721의 지갑 인터페이스(IERC721 Token Receiver)를 지원합니다.

  • 추가 확장 기능으로 발행 확장, URI 확장과 함께 발행, 소각 확장, 푸시 확장이 정의되어 있습니다.

발행(Mint)

모든 NFT의 이미지, 비디오, 메타데이터는 클라우드 스토리지에 업로드되며, 발행된 NFT는 Klaytn 네트워크에 의해 보호됩니다. 메타데이터 확장은 NFT로 표현된 자산의 세부 정보를 조사하고 JSON 스키마 정보를 위한 tokenURI를 사용합니다.

열거 확장은 NFT를 게시하고 검색하는 데 사용되며, IKIP17MetadataMintable은 TokenURI를 통해 토큰을 발행하는 데 사용됩니다.

세번째공간은 KLIP 지갑의 검증된 서비스 제공업체입니다. 모바일 호환 NFT를 발행하려면 세번째공간은 Minting API와 App2App API를 사용하여 토큰을 발행하고 전송해야 합니다.

Klaytn NFT의 메타데이터는 다음 중 일부 또는 전부를 포함할 수 있습니다:

  • 이름(name), 설명(description), 이미지(image), 애니메이션 URL(animation_url), 배경 색상(background_color), 전송 가능 여부(sendable), 친구에게만 전송 가능(send_friend_only), 그룹 이름(group_name), 그룹 아이콘(group_icon), 속성(attributes: 아티스트 이름, 크기, 제작 연도, 에디션, 아트 설명)

전송(Transfer)

지갑 인터페이스: 토큰의 소유자 또는 권한 있는 사용자는 NFT를 다른 주소로 전송할 수 있습니다.

일시 정지 확장: 일시 정지 기능은 계약이 일시적으로 전송되지 않도록 할 수 있습니다.

소각(Burn)

토큰의 소유자 또는 권한 있는 사용자는 NFT를 소각하여 영구적으로 제거할 수 있습니다.

지갑(Wallets)

KIP-17 토큰은 Kaikas와 같은 웹 기반 Klaytn 지갑 또는 모바일 앱 카카오톡의 암호화폐 지갑인 KLIP 지갑에 저장할 수 있습니다. 사용자는 계정에 가입하고 공용 주소를 입력하여 Kaikas 또는 KLIP 지갑을 연결할 수 있습니다.

이더리움 네트워크

세번째공간은 ERC-721 표준을 따라 NFT를 발행하고 전송합니다. 활용하는 오픈소스 코드는 다음과 같습니다:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import './interfaces/IThreeSpace.sol';
import './interfaces/IERC20BurnableUpgradeable.sol';
import './extensions/ERC721NameChangeUpgradeable.sol';
import './extensions/ERC721TradableUpgradeable.sol';
import '@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol';
import '@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721BurnableUpgradeable.sol';
import '@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol';
import '@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol';
import '@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721URIStorageUpgradeable.sol';
import '@openzeppelin/contracts-upgradeable/utils/CountersUpgradeable.sol';
import '@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol';

/**
 * @title ThreeSpace
 */
contract ThreeSpace is
  Initializable,
  ERC721EnumerableUpgradeable,
  ERC721URIStorageUpgradeable,
  ERC721BurnableUpgradeable,
  ERC721TradableUpgradeable,
  ERC721NameChangeUpgradeable,
  AccessControlUpgradeable,
  IThreeSpace
{
  using CountersUpgradeable for CountersUpgradeable.Counter;

  bytes32 public constant MINTER_ROLE = keccak256('MINTER_ROLE');

  CountersUpgradeable.Counter private _tokenIdCounter;

  /// @custom:oz-upgrades-unsafe-allow constructor
  constructor() initializer {}

  function initialize(
    address proxyRegistryAddress_,
    address nameChangeToken_,
    uint256 nameChangePrice_
  ) external initializer {
    __ERC721_init('ThreeSpace', 'SPACE');
    __ERC721Enumerable_init();
    __ERC721URIStorage_init();
    __AccessControl_init();
    __ERC721Burnable_init();
    __ERC721Tradable_init('ThreeSpace', proxyRegistryAddress_);
    __ERC721NameChange_init(nameChangeToken_, nameChangePrice_);

    _setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
    _setupRole(MINTER_ROLE, msg.sender);
  }

  function updateNameChangeToken(address token) external {
    require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), 'Only Admin can update');
    _nameChangeToken = token;
  }

  function updateNameChangePrice(uint256 price) external {
    require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), 'Only Admin can update');
    _nameChangePrice = price;
  }

  function changeName(uint256 tokenId, string memory newName) public override {
    super.changeName(tokenId, newName);

    IERC20BurnableUpgradeable(nameChangeToken()).burn(nameChangePrice());
  }

  function safeMint(address to, string memory uri) public onlyRole(MINTER_ROLE) {
    _safeMint(to, _tokenIdCounter.current());
    _setTokenURI(_tokenIdCounter.current(), uri);
    _tokenIdCounter.increment();
  }

  function _beforeTokenTransfer(
    address from,
    address to,
    uint256 tokenId
  ) internal override(ERC721Upgradeable, ERC721EnumerableUpgradeable, ERC721TradableUpgradeable) {
    super._beforeTokenTransfer(from, to, tokenId);
  }

  function _burn(uint256 tokenId)
    internal
    override(ERC721Upgradeable, ERC721URIStorageUpgradeable)
  {
    super._burn(tokenId);
  }

  function tokenURI(uint256 tokenId)
    public
    view
    override(ERC721Upgradeable, ERC721URIStorageUpgradeable)
    returns (string memory)
  {
    return super.tokenURI(tokenId);
  }

  function supportsInterface(bytes4 interfaceId)
    public
    view
    override(
      ERC721Upgradeable,
      ERC721EnumerableUpgradeable,
      AccessControlUpgradeable,
      ERC721TradableUpgradeable
    )
    returns (bool)
  {
    return super.supportsInterface(interfaceId);
  }

  function isApprovedForAll(address owner, address operator)
    public
    view
    override(ERC721Upgradeable, ERC721TradableUpgradeable)
    returns (bool)
  {
    return super.isApprovedForAll(owner, operator);
  }

  function _msgSender()
    internal
    view
    override(ContextUpgradeable, ERC721TradableUpgradeable)
    returns (address sender)
  {
    return super._msgSender();
  }
}

비트코인 네트워크

세번째공간은 사용자가 데이터를 비트코인 블록체인에 직접 임베드할 수 있도록 오디널스(Ordinals) 프로토콜을 통해 네트워크의 불변성과 보안을 활용합니다. 비트코인 코어 노드에 연결된 Python 애플리케이션을 실행할 수 있는 간단한 Flask API 서비스를 제공합니다.

from flask import Flask, request, jsonify 
from bitcoin_rpc import get_rpc_client 
import binascii

app = Flask(__name__)

@app.route('/create_address', methods=['GET'])
def create_address():
    client = get_rpc_client()
    address = client.getnewaddress()
    return jsonify({"address": address})

@app.route('/inscribe', methods=['POST'])
def inscribe():
    data = request.json.get('data')
    address = request.json.get('address')

    hex_data = binascii.hexlify(data.encode('utf-8')).decode('utf-8')

    client = get_rpc_client()

    utxo = client.listunspent(1, 9999999, [address])[0]
    txid = utxo['txid']
    vout = utxo['vout']
    amount = utxo['amount']

    rawtx = client.createrawtransaction([{"txid": txid, "vout": vout}], {"data": hex_data})

    fundedtx = client.fundrawtransaction(rawtx)

    signed

tx = client.signrawtransactionwithwallet(fundedtx['hex'])

    txid = client.sendrawtransaction(signedtx['hex'])
    return jsonify({"txid": txid})

if __name__ == '__main__':
    app.run(port=5000, debug=True)

보관 (지갑)

세번째에공간에서 NFT를 수집하고 전송하려면 사용자는 두 가지 옵션이 있습니다: 1) 가입하고 계정 설정에서 개인 지갑의 공개 주소를 입력하거나, 2) Metamask 및 기타 타사 이더리움 지갑을 연결하여 마켓플레이스에서 이더리움 및 NFT와 직접 교환을 수행합니다.

Last updated