🔖

Slash Extensionを使ってみよう!!【基本編③】

2023/03/23に公開

slash3.001.jpeg

皆さん、こんにちは!

今回も、Slashを使ってみようシリーズになります!

前回の記事は下記からアクセスできます!

https://qiita.com/mashharuki/items/d2b3c35873e475895bfd

今回はもっと開発寄りの内容になります。

「Slashの決済機能便利だけど、同時に別のスマートコントラクトの処理を呼び出せればいいな」 と思ったそこのあなた!!

実はそういう使い方もSlashでは可能なんです!

今日は、そんなSlashの使い幅が大きく広がる Slash Extension についてまとめていきます!

Slash Extensionを導入するまでにやるべきことは以下の3ステップです。

作ったアプリのイメージ

今回は、サンプルとしてSlashによる決済が行われるタイミングでNFTのミントをしてくれるミントサイトを用意しました!
ベースはUNCHAINの学習コンテンツで開発したものでそれにSlash Extensionの機能を追加した形になります。

Live demo

https://slash-extension-sample-app.vercel.app/

Mumbai Networkにデプロイしたスマートコントラクト

Verify済みになります!

https://mumbai.polygonscan.com/address/0x80e1dCC9F79477a0296235e2231871CCc984152f#code

ソースコード

https://github.com/mashharuki/SlashExtensionSampleApp

ソースコードの解説

次にソースコードの解説になります。
ちょっと量が多いのでバックエンドとフロントエンドに分けてまとめていきます。

バックエンド(スマートコントラクト側)

まず、スマートコントラクト側の実装になります。
Slash Extensionの機能を利用するには、Slashが用意してくれている ISlashCustomPluginというインターフェースを継承してコントラクトを開発してあげる必要があります。

詳しいことは本家の方をご覧ください。

https://slash-fi.gitbook.io/docs/advanced-features/develop-your-own-extension

コードレベルで言うと次のようなものです。

  • receivePayment
  • supportSlashExtensionInterface

という2つのメソッドが定義されています。

pragma solidity ^0.8.0;

/**
 * Slash カスタム用プラグインコントラクト
 */
interface ISlashCustomPlugin {
    /**
     * @dev receive payment from SlashCore Contract
     * @param receiveToken: payment receive token
     * @param amount: payment receive amount
     * @param paymentId: PaymentId generated by the merchant when creating the payment URL
     * @param optional: Optional parameter passed at the payment
     * @param reserved: Reserved parameter
     */
    function receivePayment(
        address receiveToken,
        uint256 amount,
        bytes calldata paymentId,
        string calldata optional,
        bytes calldata reserved
    ) external payable;

    /**
     * @dev Check if the contract is Slash Plugin
     *
     * Requirement
     * - Implement this function in the contract
     * - Return 1 (v1 extension), 2 (v2 extension)
     */
    function supportSlashExtensionInterface() external returns (uint8);
}

今回は、通常のNFTコントラクトのメソッドに加えてこのインタフェースを利用したコントラクトを作りました!
本体となるNFTコントラクトのソースコードは以下の通りです。

一番キーになってくるのは receivePaymentメソッド です!!

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "hardhat/console.sol";

import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "./interface/ISlashCustomPlugin.sol";
import "./lib/UniversalERC20.sol";

contract NFTCollectible is ERC721Enumerable, Ownable, ISlashCustomPlugin {

    using UniversalERC20 for IERC20;

    using SafeMath for uint256;
    using Counters for Counters.Counter;

    Counters.Counter private _tokenIds;
    uint public constant MAX_SUPPLY = 30;
    uint public constant PRICE = 0.0001 ether;
    uint public constant MAX_PER_MINT = 3;

    string public baseTokenURI;

    constructor(string memory baseURI) ERC721("My NFT Collectible", "NFTC") {
        setBaseURI(baseURI);
    }

    // mint 10 NFTs fro free sell
    function reserveNFTs() public onlyOwner {
        uint totalMinted = _tokenIds.current();
        require(totalMinted.add(10) < MAX_SUPPLY, "Not enough NFTs");
        // free mint
        for (uint i = 0; i < 10; i++) {
            _mintSingleNFT();
        }
    }

    // getter func for baseURI
    function _baseURI() internal view virtual override returns (string memory) {
        return baseTokenURI;
    }

    // setter func for baseURI
    function setBaseURI(string memory _baseTokenURI) public onlyOwner {
        baseTokenURI = _baseTokenURI;
    }

    /**
     * receivePayment 支払い時に実行されるコントラクト
     */
    function receivePayment(
        address receiveToken,
        uint256 amount,
        bytes calldata,
        string calldata,
        bytes calldata  reserved
    ) external payable override {
        require(amount > 0, "invalid amount");
        
        IERC20(receiveToken).universalTransferFrom(msg.sender, owner(), amount);
        
        // mint NFT to tx.origin
        uint newTokenID = _tokenIds.current();
        // call _safeMint func
        _safeMint(tx.origin, newTokenID);
        _tokenIds.increment();
    }

    /**
     * mint NFT func 
     * @param _count count of NFT
     */
    function mintNFTs(uint _count) public payable {
        // get token IDs
        uint totalMinted = _tokenIds.current();
        // check fro mint NFT
        require(totalMinted.add(_count) <= MAX_SUPPLY, "Not enough NFTs!");
        require(_count > 0 && _count <= MAX_PER_MINT, "Cannot mint specified number of NFTs.");
        require(msg.value >= PRICE.mul(_count), "Not enough ether to purchase NFTs.");

        for (uint i = 0; i < _count; i++) {
            _mintSingleNFT();
        }
    }

    function _mintSingleNFT() private {
        uint newTokenID = _tokenIds.current();
        // call _safeMint func
        _safeMint(msg.sender, newTokenID);
        _tokenIds.increment();
    }

    // getter owner's all tokensId
    function tokensOfOwner(address _owner) external view returns (uint[] memory) {

        uint tokenCount = balanceOf(_owner);
        uint[] memory tokensId = new uint256[](tokenCount);

        for (uint i = 0; i < tokenCount; i++) {
            tokensId[i] = tokenOfOwnerByIndex(_owner, i);
        }

        return tokensId;
    }

    function withdraw() public payable onlyOwner {
        uint balance = address(this).balance;
        require(balance > 0, "No ether left to withdraw");
        // send ETH to msg.sender
        (bool success, ) = (msg.sender).call{value: balance}("");
        require(success, "Transfer failed.");
    }

    /**
     * V2に対応したプラグインコントラクト用のインターフェースを継承しているかどうかチェック
     */
    function supportSlashExtensionInterface()
        external
        pure
        override
        returns (uint8)
    {
        return 2;
    }
}

receivePayment メソッドは、決済コントラクトの処理が走ると実行されるメソッドでこのメソッド内に決済と同時に実行させたい処理を記述します!

今回は、NFTをmintする処理を記述しています。 これによって、決済した時にNFTがミントされます!

セキュリティ的には tx.originを使うことはあまり推奨されていませんが、msg.senderにしてしまうと決済コントラクトに対してミントされてしまうのでこの場合は、トランザクションの大元の起点となるEOAであるユーザーのウォレットアドレスを指定したいので、tx.originを指定します。

/**
 * receivePayment 支払い時に実行されるコントラクト
 */
function receivePayment(
    address receiveToken,
    uint256 amount,
    bytes calldata,
    string calldata,
    bytes calldata  reserved
) external payable override {
    require(amount > 0, "invalid amount");
    
    IERC20(receiveToken).universalTransferFrom(msg.sender, owner(), amount);
    
    // mint NFT to tx.origin
    uint newTokenID = _tokenIds.current();
    // call _safeMint func
    _safeMint(tx.origin, newTokenID);
    _tokenIds.increment();
}

あと一番最後に supportSlashExtensionInterfaceメソッドを記載してあげます。 今回は、V2に適応したコントラクトですので uint8型の2という値を返してあげるとてもシンプルな実装でOKです!

/**
 * V2に対応したプラグインコントラクト用のインターフェースを継承しているかどうかチェック
 */
function supportSlashExtensionInterface()
    external
    pure
    override
    returns (uint8)
{
    return 2;
}

あとは、このコントラクトを通常通りブロックチェーン上にデプロイしてあげるだけです。

バックエンド側のソースコードの解説は以上です!

フロントエンド側

次にフロントエンド側です。

フロントエンド側の実装については前回解説したReactアプリに導入する方法とほとんど同じです!
決済URLを事前に用意してあげてそれを変数に格納してあげる必要があります。

最もコアになるApp.jsのポイントなるソース部分だけみていきます。

357 lines (329 sloc)  11.1 KB

import './css/App.css';
import React, { useEffect, useState } from "react";
import Box from "@mui/material/Box";

/////// 省略  /////////////

// 決済用URL (ここを適宜変更する。)
const PAYMENT_URL = "https://testnet.slash.fi/payment-merchant/2334ea3bc896476c4e8b525952d411f2";

/////// 省略  /////////////

/**
 * NFTMintボタンコンポーネント
 * Slashの決済画面に遷移するリンクボタンになります。
 */
const mintNftButton = () => {
    return (
        <a
          href={PAYMENT_URL}
          target="_blank"
          className="App-link"
          rel="noopener noreferrer"
          type='button'
        >
          <img 
            src={paymentButton} 
            alt="paymentButton" 
            height={50} 
          />
        </a>
    );
};

フロントエンド側の解説は以上です。

received addressの設定

Slash Extensionを使えるようにするにはコントラクトを開発してデプロイするだけでなく、さらにもう一ステップやるべきことが残っています。

デプロイしたスマートコントラクトのアドレスを received addressに設定する ことです。

このステップはスマートコントラクトがデプロイ済みであることが条件となります。

通常、決済コントラクトを作成すると受信アドレス(received address)は作成するときに署名したMetamaskのウォレットアドレスになっています。

Slash Extensionを利用する際には、このアドレスを デプロイしたスマートコントラクトのアドレスに変更します。

changeボタンをクリックして変更先のアドレスを指定しましょう。

私の場合は、Mumbai NetworkにデプロイしたのでMumbai Networkでのreceived addressをスマートコントラクトのアドレスに変更しました。

これでようやく準備完了です!

動かし方(フロントエンド)

① ソースコードをgitでcloneしてくる。

git clone https://github.com/mashharuki/SlashSampleReactApp.git

② モジュールのインポート

cd frontend && npm i 

③ 環境変数の設定

frontendフォルダ配下に.envファイルを作成します。

REACT_APP_MORALIS_API_KEY=

このアプリでは、NFTのデータを取得するのに Morailsが提供しているAPIを利用しているので環境変数の設定が必要となります。

NFTのデータの取得以外にも便利なAPIを沢山公開してくれているのでご興味があれば是非こちらも試してみてください。

アカウントを登録して、ダッシュボード画面からAPI keyを生成することができるのでそこの値をコピペしましょう!

https://moralis.io/?utm_source=gads&utm_campaign=17600841074&utm_medium=140711338200&network=g&device=c&gclid=CjwKCAjw_MqgBhAGEiwAnYOAevxnU5d_MTq03jm9w-YB4vN52LC6V8ckfBcDg-sSMZD1W0vFKWZq5hoCly4QAvD_BwE

こんな感じで確認できます!

スクリーンショット 2023-03-16 22.29.16.png

④ アプリ起動

npm run start

問題なく起動できれば、localhost:3000でアプリにアクセスできるはずです。

事前にfaucetで少額の暗号資産を入手しておきましょう!

https://github.com/arddluma/awesome-list-testnet-faucets

アプリの操作イメージ

起動するとまず最初に下のようなミント画面に遷移できます!

Crypto pay ボタンを押してNFTをミントしましょう!
Slashの決済画面に遷移します。

支払う暗号資産を選択しましょう!
問題なければトランザクションが処理されるはずです。

決済が完了したらブロックチェーンエクスプローラーまでのURLが表示されるので確認しにいきましょう!
今回実装した内容が問題なければ決済処理の他にNFTをミントする処理が実行されているはずです。

ちゃんとNFTがミントできていそうです!!!!!

アプリのトップ画面に戻ります!

発行枚数が増えていました!!!!!

このアプリでは発行したNFTも確認できる機能もあるのでViewページに遷移してみます!

ちゃんと表示されていました!!

ちなみに、メタデータの構造もOpenSeaの規格に従って作ってあるのでOpenseaでも確認することができます!!

https://testnets.opensea.io/ja/assets/mumbai/0x80e1dcc9f79477a0296235e2231871ccc984152f/0

Maticは無いけど、USDTならある! という時にNFTをミントすることだってできるのです!!!
※ ただし、ガス代はかかってしまいますので小額は必要です。

Slashチームによると将来的にはガスレスで対応したいと考えているとのことでしたので、楽しみですね!

これで、Slash Extensionの機能を導入したアプリを開発することができました!
決済と同時に他の処理を呼べれば面白いことがたくさんできそうですよね!!

今日のメイントピックは以上です!

その他ピッチ資料などが公開されていましたのでご興味があればぜひこちらも!
決済ソリューション以外にも開発が進められているみたいなので楽しみですね!!

https://drive.google.com/file/d/1ib1-RokKEaXtJnmyzxMKgLI6Ndz2kqXH/view

参考文献

https://slash-fi.gitbook.io/docs/advanced-features/develop-your-own-extension

https://app.unchain.tech/learn/Polygon-Generative-NFT/

Discussion