📝

NFTとOpenSea

2022/04/21に公開約6,600字

NFTに関して色々と調べていたのでその備忘録です

作ったもの

https://github.com/akihokurino/rust-opensea

代替性トークン、非代替性トークンに関して

NFTは非代替性トークンと言われているがその逆の代替性トークンがこれまでのERC20のトークンになります。

https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol
実際にERC20を実装したコントラクトを見ると分かる通り
mapping(address => uint256) private _balances;

各アドレスに対してトークンの量を設定する形になっており、このトークンの一つ一つに違いはなく、代替性があるということになります。

NFTは1つはERC721という仕様に準拠していますがそれを同じく見てみると

https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol
mapping(uint256 => address) private _owners;

今度はuint256(トークンのID)をアドレスに紐づける形になっており、特定のトークンのIDを誰が持っているのか判定できるようになっている。この場合、それぞれのトークンID同士はそれぞれ別物なので、非代替性であるということになります。

NFTにはもう1つERC1155という仕様に準拠したものも存在しますが、そちらは

https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC1155/ERC1155.sol
mapping(uint256 => mapping(address => uint256)) private _balances;

ERC721とまず違うのは特定のトークンのIDをアドレスに紐付けつつ、そのトークンをいくつ持っているかを設定する部分です。なので、ERC1155はERC20とERC721のハイブリッドと呼ばれていたりします。

ここまで見て、NFTと呼ばれるものは単純にトークンのIDと持ち主のアドレスを紐づけているだけで、それ以上でも以下でもないことがわかります。じゃあ、テレビとかでよくみるアート作品とかはどこにあるのかというと、

ERC721
ERC1155

シグネチャだけみればわかりますが、要するにトークンのIDを引数にしてそれに対応するmetadata.jsonのURLを取得し、そのmetadata.jsonの中身に画像のURL等が入ってきます。

例えばOpenSeaのmetadata.jsonだと

https://docs.opensea.io/docs/metadata-standards
このように規定されています。

実際にERC721とERC1155のNFTを作ってOpenSeaで取り扱う

OpenSeaとはNFTのマーケットプレイスです。

https://opensea.io/

OpenSeaではテストネットとしてRinkebyが使えるので、Rinkeby上で自作のコントラクトをデプロイしてOpenSea上で確認するようにします。

ERC721とERC1155のコントラクトを作成してデプロイする

https://github.com/akihokurino/rust-opensea/blob/master/ethereum/contracts/ERC721.sol
https://github.com/akihokurino/rust-opensea/blob/master/ethereum/contracts/ERC1155.sol

先ほども例に出したOpenZeppelinを使って軽く実装しているだけです。
ポイントは mint のメソッド(新規発行を行う)とmetadata.jsonを返す uri, tokenURI のメソッドになります。それ以外は不要です。

以前にも紹介しましたが、今回もコントラクトは truffle で作成し、テストした状態でデプロイを行います。個々のやり方に関しては省きます。

https://trufflesuite.com/

デプロイすると、デプロイ済みのコントラクトのアドレスとその時のABIが取得できるので、それを保存しておきます。
ABIとはApplication Binary Interfaceの略でそのバイナリにアクセスするためにものになります。

デプロイしたコントラクトに対してRustからmintする

言語はなにでもいいのですが、 rust-web3 を使ってみたかったのでRustで実装しています。

コマンドラインツールとして作っているので色々書いていますが、ポイントは

let base_url = env::var("ETHEREUM_URL").expect("ETHEREUM_URL must be set");
let transport = transports::Http::new(&base_url).ok().unwrap();
let cli = Web3::new(transport);

ETHEREUM_URL には infra で取得したRinkebyにアクセスするURLを指定します。
それを使ってWeb3インスランスを作成し、

let contract = Contract::from_json(
self.cli.eth(),
   parse_address(self.contract_address.clone()).unwrap(),
   include_bytes!("rust-token721.abi.json"),
)?;

先ほどデプロイしたコントラクトのアドレスとデプロイ時のコントラクトのABIを読み込んでcontractオブジェクトを作成し、

let prev_key = SecretKey::from_str(&self.wallet_secret.clone()).unwrap();
let gas_limit: i64 = 5500000;
let gas_price: i64 = 35000000000;

let c = self.contract()?;
let result = c
  .signed_call_with_confirmations(
    "mint",
    (
      parse_address(self.wallet_address.clone()).unwrap(), 
      name
    ),
    Options::with(|opt| {
      opt.gas = Some(U256::from(gas_limit));
      opt.gas_price = Some(U256::from(gas_price));
    }),
    1,
    SecretKeyRef::from(&prev_key),
  )
  .await?;

署名が必要なコントラクトへのアクセスはこんな感じで、最初に秘密鍵を取得し、 signed_call_with_confirmations を呼び、文字列でコールしたいメソッドを指定するのみです。
gas_limitgas_price はよしなに設定してください。
このコードで先ほどデプロイしたコントラクトのmintメソッドを呼び出し、自身で作ったコントラクト内で新しいNFTを発行できます。
もちろん、署名が必要なメソッドなので、この瞬間に自身のetherを消費します。なので、指定した秘密鍵に紐づくウォレットにはRinkebyで使えるetherを一定数入れておいてください。

上記はERC721へmintを行うコードですが、同じようにERC1155のコントラクトへもアクセスができます。
ABIはERC1155のものを読み込む必要があります。
ERC721と違うのは、先ほども話した通り、 mint 時にamountを指定する部分のみです。

let prev_key = SecretKey::from_str(&self.wallet_secret.clone()).unwrap();
let gas_limit: i64 = 5500000;
let gas_price: i64 = 35000000000;

let c = self.contract()?;
let result = c
  .signed_call_with_confirmations(
    "mint",
    (
      parse_address(self.wallet_address.clone()).unwrap(),
      name,
      amount,
    ),
    Options::with(|opt| {
      opt.gas = Some(U256::from(gas_limit));
      opt.gas_price = Some(U256::from(gas_price));
    }),
    1,
    SecretKeyRef::from(&prev_key),
  )
  .await?;

署名が不要なコントラクトへのアクセスはよりシンプルになって、

let c = self.contract()?;
let result = c.query("name", (), None, Options::default(), None);
let name: String = result.await?;

このようにアクセスします。

一番重要な部分だけ掲載して説明しましたが、このソースコード全体を通しては、コントラクトにmintする前段に特定トークンに紐づくmetadata.jsonを生成してアップロードしておくという処理が入っています。
metadata.jsonがないとmintを行ってもそのトークンIDに紐づくアセットをOpenSeaで見つけられない状況になってしまいます。

ここまで行いますと、指定した秘密鍵に紐づくウォレットのアカウントでOpenSeaにログインし自身で作ったNFTを確認することができます。

OpenSea上でNFTの売買を行う

OpenSea上のUIで普通に売り買いを行うことができます。
ですが、今回はそれをコード上で行います。
OpenSeaにはSDKが存在します。

https://github.com/ProjectOpenSea/opensea-js

これを使ってREADME通りにやれば終わるのですが、そこに書いてない注意事項としては、秘密鍵の設定になります。

NOTE: Using the sample Infura provider above won't let you authorize transactions, which are needed when approving and trading assets and currency. To make transactions, you need a provider with a private key or mnemonic set.

const provider = new HDWalletProvider("ウォレットの秘密鍵", "イーサリアムネットワークのURL");
const seaport = new OpenSeaPort(provider, {
  networkName: Network.Rinkeby, // イーサリアムネットワークに合わせる
  apiKey: "OpenSeaのAPIキー",
})

テストネットに繋げる場合は、APIキーは不要でundefinedにしておく必要があります。(空文字もだめ)
HDWalletProviderこちらのパッケージで先ほども登場した truffle 内の実装になります。
これを使うと、今回は秘密鍵を入れる説明になっていますが、ニーモニックを利用して初期化することも可能です。

ここまでくるとあとは説明通りに使うのみになります。

https://github.com/ProjectOpenSea/opensea-js#fetching-assets
NFTを取得してみたり、

https://github.com/ProjectOpenSea/opensea-js#fetching-orders
売り注文や買い注文のオーダーを取得して、

https://github.com/ProjectOpenSea/opensea-js#buying-items
実際にNFTを購入したり、

https://github.com/ProjectOpenSea/opensea-js#transferring-items-or-coins-gifting
別のアドレスに持っているNFTを送付したり。

OpenSeaのUIでできる基本的なことはSDKを利用して行うことができるようです。
ちなみに、Get系の簡単な操作はAPIでも可能です。

https://docs.opensea.io/reference/api-overview

ここで面白いなと思っているのが、OpenSeaはどうやってNFTの所有権の移転を行なっているのかという部分です。
疑問に思い、SDKの中身を少し覗いてみたのですが、そこでは wyvern というコントラクトが裏に存在し、そこへアクセスしてなにかを行なっているようです。

https://etherscan.io/address/0x7f268357a8c2552623316e2562d90e642bb538e5

wyvern の詳細に関しては次の機会に記事を書いてみようかなと思っています。

最後に

https://gigazine.net/news/20220221-phishing-attack-opensea-254-nft/

Discussion

ログインするとコメントできます