Slash Extensionを使ってみよう!!【応用編】
皆さん、こんにちは!
今回も、Slashを使ってみようシリーズになります!
前回の記事は下記からアクセスできます!
はじめに
今回は、前回に続いて 「Slash Extension」 の機能についての記事になります。
「Slash Extension」 を使えば決済と同時に任意の処理を実行させることができることを共有しました。
ただ、前回の状態だと引数を渡せないので例えばトークンIDを指定してNFTを他の人に渡したりすることができませんでした。
今回は、APIサーバーを実装して引数を渡したり金額を設定したりできるような使い方をまとめていきます!!
さらに開発寄りになりますがここをクリアできればかなり自由にSlashを使いこなせるようになるはずです!!
ちょっと長くなりそうなので目次です。
目次
今回作ったアプリのイメージ
ソースコード
実際にデプロイしたスマートコントラクトについては以下から確認できます!!
今回は、3つのブロックチェーンにデプロイしてみました!
Mumbai Networkにデプロイしたスマートコントラクト
Goerliにデプロイしたスマートコントラクト
Avalanche Fuji chainにデプロイしたスマートコントラクト
実際にNFTを複数移転させたトランザクション
OpenSea (testnet)上のデータ
APIサーバーの解説
前回と異なり、今回はAPIサーバーが関わってきます!
加盟店側やシステム提供者側であらかじめ値段や法定通貨の種類を設定しておきたい場合・任意の値を渡してその値を引数についてスマートコントラクトの関数を実行させたい場合には、このAPIサーバー側で決済用のURLを生成する処理を実装する必要があります。
Slashの公式ドキュメントを元にGo言語でAPIサーバーを書いてみました!
かなりシンプルな実装です!
package main
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"math/rand"
"net/http"
"net/url"
"os"
"strconv"
"time"
"github.com/joho/godotenv"
"github.com/gin-gonic/gin"
)
/**
* 乱数列を生成するメソッド
*/
func generateCode() string {
// 生成する文字列の候補
candidates := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
// 文字列の長さ
length := 20
// 現在時刻からシード値を生成する
rand.Seed(time.Now().UnixNano())
// ランダムな文字列を生成する
result := make([]byte, length)
for i := range result {
result[i] = candidates[rand.Intn(len(candidates))]
}
return string(result)
}
/**
* APIサーバーのmainファイル
*/
func main() {
r := gin.Default()
err := godotenv.Load(".env")
if err != nil {
fmt.Printf("failed to load .env file: %v", err)
}
/**
* テスト用API
*/
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
/**
* Slashの決済用のURLを生成するAPI
*/
r.POST("/getUrl", func(c *gin.Context) {
c.Header("Access-Control-Allow-Origin", "*")
// api-endpoint
API_ENDPOINT := "https://testnet.slash.fi/api/v1/payment/receive"
// Authentication Token
AUTH_TOKEN := os.Getenv("SLASH_AUTH_TOKEN")
// Hash Token
HASH_TOKEN := os.Getenv("SLASH_HASH_TOKEN")
// Code set by the merchant to uniquely identify the payment
// 決済ごとにユニークな値を生成する必要あり
ORDER_CODE := generateCode()
// 確認のため出力
fmt.Printf("AUTH_TOKEN: %s\n", AUTH_TOKEN)
fmt.Printf("HASH_TOKEN: %s\n", HASH_TOKEN)
fmt.Printf("ORDER_CODE: %s\n", ORDER_CODE)
// Set amount and Generate verify token
// get ext_reserved
ext_reserved := c.PostForm("extReserved")
// this number must be Zero or more
amount_to_be_charged := c.PostForm("amount")
// float型に変換
fAmount, err := strconv.ParseFloat(amount_to_be_charged, 64)
if err != nil {
fmt.Printf("%s\n", err.Error())
return
}
fmt.Printf("amount_to_be_charged: %f\n", fAmount)
fmt.Printf("ext_reserved: %s\n", ext_reserved)
// verify_tokenを生成する。
raw := fmt.Sprintf("%s::%f::%s", ORDER_CODE, fAmount, HASH_TOKEN)
verify_token := sha256.Sum256([]byte(raw))
// data to be sent to api
data := url.Values{}
data.Add("identification_token", AUTH_TOKEN)
data.Add("order_code", ORDER_CODE)
data.Add("verify_token", hex.EncodeToString(verify_token[:]))
data.Add("amount", fmt.Sprintf("%f", fAmount))
data.Add("amount_type", "JPY")
data.Add("ext_description", "This is payment for NFT")
data.Add("ext_reserved", fmt.Sprintf("%s", ext_reserved))
// sending post request and saving response as response object
res, _ := http.PostForm(API_ENDPOINT, data)
defer res.Body.Close()
body, _ := ioutil.ReadAll(res.Body)
fmt.Printf("url: %s\n", body)
var responseObj interface{}
_ = json.Unmarshal(body, &responseObj)
url := responseObj.(map[string]interface{})["url"]
if url != nil {
fmt.Printf("url: %s\n", url)
} else {
err := responseObj.(map[string]interface{})["errors"]
fmt.Println(err)
}
c.JSON(200, gin.H{
"url": url,
})
})
// 0.0.0.0:8080 でサーバーを立てます。
r.Run()
}
開発用なので、途中経過も確認するため色々変数の値を出力するようにしています!!
- 動かし方
cd api && go run main.go
試しに叩くには下記コマンドを打ってみると良いです!
curl -Ss -XPOST http://localhost:8080/getUrl --data-urlencode 'amount=10' --data-urlencode 'ext_reserved=0x0000000000000000000000000000000000000000000000000000000000000003'
正常にAPIサーバーが動いていれば、下記のように決済URLが生成されます。
今回の実装例では、フロントエンド側から extReserved
とamount
という二つの値を渡しています。
それぞれのパラメータの詳細は次の通りです。
- extReserved: スマートコントラクトに渡す変数の型と値をエンコードしたもの
- amount: 料金
バックエンドの解説
続いてバックエンド側になります!
ベース部分は前回のブログで紹介したソースとほぼ変わりませんが、決済時に渡された任意の値を取得するところの実装が異なってきています。
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
//// 省略 /////
contract NFTCollectible is ERC721Enumerable, Ownable, ISlashCustomPlugin {
using UniversalERC20 for IERC20;
using SafeMath for uint256;
using Counters for Counters.Counter;
struct PurchaseInfo {
uint mintCounts;
}
//// 省略 /////
/**
* 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);
// decode
PurchaseInfo memory info = abi.decode(reserved, (PurchaseInfo));
// mint NFT
mintNFTs(info.mintCounts);
}
/**
* 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(tx.origin, newTokenID);
_tokenIds.increment();
}
//// 省略 /////
/**
* V2に対応したプラグインコントラクト用のインターフェースを継承しているかどうかチェック
*/
function supportSlashExtensionInterface()
external
pure
override
returns (uint8)
{
return 2;
}
}
receivePayment
の中身の処理がとても重要で、フロントエンド側でスマートコントラクトに渡したい変数の型と値をエンコードしたものがreserved
として渡されます。
この中身の値を取得するために、デコードしてあげる必要があります!
今回ではデコードした値からmintCounts
という変数を取得して一回の決済で幾つのNFTを発行するのかという値を取得しています。
// decode
PurchaseInfo memory info = abi.decode(reserved, (PurchaseInfo));
// mint NFT
mintNFTs(info.mintCounts);
また、デコードした値を詰めるための独自のStructを用意してあげる必要があるので、ここは各々渡したい値にマッチするように定義してあげましょう!
今回の場合だと以下のようになります!
struct PurchaseInfo {
uint mintCounts;
}
デプロイ方法
ブロックチェーンへのデプロイは以下のコマンドを実行することで可能です!
※ 自身の秘密鍵の情報を環境変数として.env
ファイルに記載していることが前提です。
cd backend && npm run deploy:fuji
フロントエンドの解説
最後にフロントエンドです!
前回よりも少し複雑になります。
前回は、ボタンを押したらSlashの決済画面に遷移していましたが今回は以下のようなステップを踏みます!
- ボタンを押す
- 決済用URL発行APIが呼び出される。
- APIの実行結果として決済URLが返却される。
- URLをフロントに表示する。
- URLをクリックして決済画面に遷移する。
import './css/App.css';
/**
* Appコンポーネント
*/
function App() {
/// 省略 ///
const [baseApiUrl, setBaseApiUrl] = useState("http://localhost:8080");
const [basePrice, setBasePrice] = useState(100);
const [paymentUrl, setPaymentUrl] = useState("");
/// 省略 ///
/**
* 決済用のURLを取得するためのメソッド
*/
const getUrl = async() => {
// create encode data
const extReserved = {
types: ['uint'],
params: [count]
}
const encodedExtReserved = ethers.utils.defaultAbiCoder.encode(
extReserved.types,
extReserved.params
);
console.log(`extReserved: ${encodedExtReserved}`)
// 決済用のURLを取得するためのAPIを呼び出す。
await superAgent
.post(baseApiUrl + "/getUrl")
.type('form')
.send({
amount: `${basePrice * count}`,
extReserved: encodedExtReserved
})
.set({
Accept: 'application/json',
})
.end((err, res) => {
if (err) {
console.log("決済用URL取得中にエラー発生", err)
return err;
}
console.log("決済用URL取得成功!:", res.body.url);
setPaymentUrl(res.body.url);
setMintingFlg(true)
});
};
/**
* NFTMintボタンコンポーネント
*/
const mintNftButton = () => {
return (
<>
<Box sx={{ flexGrow: 1, overflow: "hidden", mt: 1, my: 1}}>
<Stack
direction="row"
justifyContent="center"
alignItems="center"
spacing={1}
>
<IconButton
aria-label="remove"
size='large'
onClick={() => setCount(count - 1)}
>
<RemoveCircleIcon/>
</IconButton>
<TextField
id="outlined-number"
type="number"
InputLabelProps={{
shrink: true,
}}
value={count}
/>
<IconButton
aria-label="add"
size='large'
onClick={() => setCount(count + 1)}
>
<AddCircleIcon/>
</IconButton>
</Stack>
</Box>
<Button
onClick={getUrl}
>
<img
src={paymentButton}
alt="paymentButton"
height={50}
/>
</Button>
</>
);
};
/// 省略 ///
return (
<div className="main-app">
{ viewFlg ? (
<>
<h1>NFT View</h1>
<Box sx={{ flexGrow: 1, mt: 2, my: 1}}>
<Grid
container
justifyContent="center"
alignItems="center"
>
<View
address={contractAddr}
networkId={networkId}
baseURI={baseURI}
/>
</Grid>
</Box>
<button
className="opensea-button cta-button"
onClick={() => { setViewFlg(false) }}
>
NFTを発行する
</button>
<Footer/>
</>
) : (
<>
<h1>Let's Mint NFT! 1個1,00円です。</h1>
<Box sx={{ flexGrow: 1, overflow: "hidden", mt: 4, my: 2}}>
<strong>
contract address :
{(networkId === "0x13881") && (
<a href={POLYGONSCAN_LINK}>
{contractAddr}
</a>
)}
{(networkId === "0x51") && (
<a href={BLOCKSCOUT_LINK}>
{contractAddr}
</a>
)}
{(networkId === "0x150") && (
<a href={BLOCKSCOUT_LINK2}>
{contractAddr}
</a>
)}
{(networkId === "0x5") && (
<a href={ETHERSCAN_LINK}>
{contractAddr}
</a>
)}
{(networkId === "0xa869") && (
<a href={SNOWTRACE_LINK}>
{contractAddr}
</a>
)}
</strong>
</Box>
<Grid
container
direction="row"
justifyContent="center"
alignItems="center"
>
<Box sx={{ flexGrow: 1, overflow: "hidden", px: 3, mt: 10}}>
<StyledPaper sx={{my: 1, mx: "auto", p: 0, paddingTop: 2, borderRadius: 4}}>
<Box sx={{ flexGrow: 1, overflow: "hidden", mt: 1, my: 1}}>
<strong>発行状況:{supply} / {MAX_SUPPLY}</strong>
</Box>
<img src={SquirrelsSvg} alt="Polygon Squirrels" height="40%" /><br/>
{ mintingFlg ?
(
<div>
<div className="spin-color">
Click this link !
</div>
<a href={paymentUrl}>magic link</a>
</div>
) :(
<>
{ currentAccount ? mintNftButton() : connectWalletButton()}
</>
)
}
</StyledPaper>
<button
className="opensea-button cta-button"
onClick={() => { setViewFlg(true) }}
>
NFTを確認する
</button>
<Footer/>
</Box>
</Grid>
</>
)}
</div>
);
}
export default App;
実際に決済用のURLを発行する処理は、getUrl
というメソッドが担当していて、画面からだとCryptoPayボタンを押した時に実行されます。
// 決済用のURLを取得するためのAPIを呼び出す。
await superAgent
.post(baseApiUrl + "/getUrl")
.type('form')
.send({
amount: `${basePrice * count}`,
extReserved: encodedExtReserved
})
.set({
Accept: 'application/json',
})
.end((err, res) => {
if (err) {
console.log("決済用URL取得中にエラー発生", err)
return err;
}
console.log("決済用URL取得成功!:", res.body.url);
setPaymentUrl(res.body.url);
setMintingFlg(true)
});
重要になってくるのが、encodedExtReserved
という値でこれが任意の変数の型と値をエンコードしたものとなっています。
この処理の直前で生成しています。
これは、ethers.jsというライブラリを使った場合の生成方法です。
// create encode data
const extReserved = {
types: ['uint'],
params: [count]
}
const encodedExtReserved = ethers.utils.defaultAbiCoder.encode(
extReserved.types,
extReserved.params
);
ちょっと複雑ですが一度仕組みを理解してしまえば簡単に導入できそうですね!
ここからは、実際の画面イメージと合わせながら解説していきます。
- トップ画面
最初のトップ画面です。
CryptoPay ボタンをクリックすると裏でAPIを呼び出して決済URLが生成されます!
magic link をクリックするとSlashの決済ページに遷移します!
前回と異なるのは金額や法定通貨がすでに指定されている点です!!
magic linkという文言で決済用URLが表示されるようにしたのでそれをクリックしましょう!
うまくいけば決済画面に遷移します。
すでに値が指定されていますね!!
支払うトークンを選択して通常通り決済処理を進めます。
処理されるとブロックエクスプローラーまでのURLが発行されるのでトランザクションの内容を見に行きましょう!
今回は、一度決済でNFTを2つミントさせるように引数を渡しました。
正常に動作していれば決済と同時にNFTが2つミントされているはずです!
引数に渡した回数だけNFTがミントされていました!!
トップ画面に戻ります!
増えてました!!
一応、OpenSeaも確認してみます!!
大丈夫そうですね!!
まとめ
今日のメイントピックは以上です!
応用編ということで少し難易度が上がっていますが、やり方さえ把握してしまえば色々組み込めそうだなって思いました!
ここまでドキュメントや仕組みを整備しているSlashのチームは本当にすごいですね・・・。
余談ですが、先日CEOの佐藤さんから新しいプロジェクトの発足も発表されていていました!!
その他ピッチ資料などが公開されていましたのでご興味があればぜひこちらも!
決済ソリューション以外にも開発が進められているみたいなので楽しみですね!!
参考文献
Discussion
NFTの初心者ですが、これから色々な記事を読んで勉強させてもらいます。よろしくお願いします😆
ありがとうございます!