非同期スマートコントラクト(NFT発行)の構築と呼び出しハンズオン
こんにちは、Komlock LabでWeb3エンジニアの阿部です!
スマートコントラクトの書き込み実行処理は時間がかかりますよね。
UX的にスマートコントラクトへの書き込み処理は非同期的で行なうケースが多いです。
ユーザーに対するNFTの発行が一例です。
このハンズオンは、
非同期スマートコントラクト(NFT発行)を作成し、AWSのサーバーレスアーキテクチャを活用して呼び出すシーケンスを学べるように設計しています。
実務での活用するシーケンスかと思いますので、役に立てていただければ幸いです!
想定読者
- hardhat, openzeppelinを利用してFNTスマートコントラクトを開発できる方
- まず基本的なスマートコントラクト開発を学びたい方はこちらの記事をご参照ください。
- 基本的なAWSリソースを利用したことがある方
- 簡単なIAM利用、S3利用をしたことがあればOKです!
概要
このハンズオンでは、以下の流れを学びます:
- AWSアカウントのセットアップ
- Terraform CLIのセットアップ
- IAMロールとポリシーの作成
- API Gatewayでリクエスト送信
- SNSでイベント発火
- SQSでイベントキャッチとLambda関数呼び出し
- Lambda関数でNFTスマートコントラクトを実行
- (本来はWebhook等でクライアントにコントラクト完了を通知することが多いですが、今回は割愛)
シーケンス図
AWS構成図
github レポジトリ
準備
Node.jsとnpmのインストール
Node.js, npm(推奨バージョン: LTS)をインストールしてください。
以下のコマンドでインストールされていることを確認してください。
node -v
npm -v
以下のようになっていたらOKです。
v23.0.0
11.0.0
Alchemyアカウントの作成とApp作成
AWSリソース構築フロー
ステップ 1: AWSアカウントのセットアップ
手順
- AWS公式サイトから無料アカウントを作成
- IAMユーザーを作成し、アクセスキーとシークレットキーを取得
- 必要なポリシー:
AdministratorAccess
(一時的に使用)
- 必要なポリシー:
- AWS公式サイトを参考にAWS CLIをインストール
- 以下のコマンドで設定:
aws configure
- IAMのアクセスキー、シークレットキー、リージョンを入力。
検証
以下のコマンドで認証情報が正しいことを確認:
aws sts get-caller-identity
以下のような表示になっていればOKです。
{
"UserId": "XXX",
"Account": "XXX",
"Arn": "arn:aws:iam::XXX:user/iam名"
}
ステップ 2: プロジェクトディレクトリの作成
mkdir async-nft
cd async-nft
ステップ 3: NFTスマートコントラクト作成とデプロイ
contractsディレクトリ作成とHardhatインストール
contractsディレクトリを作成し、Hardhatをインストールします。
mkdir async-nft/contracts
cd async-nft/contracts
npm init -y
npm install --save-dev hardhat
npm install --save-dev @nomicfoundation/hardhat-toolbox
Hardhatプロジェクトを初期化
npx hardhat init
「Create a basic sample project」を選択し、指示に従ってセットアップしてください。
必要なライブラリをinstall
npm install @openzeppelin/contracts
Hardhat設定の更新
Sepolia Testnet用にhardhat.config.jsを編集します。
require("@nomicfoundation/hardhat-toolbox");
module.exports = {
solidity: "0.8.28",
networks: {
sepolia: {
url: "https://eth-sepolia.g.alchemy.com/v2/YOUR_ALCHEMY_PROJECT_ID",
accounts: ["YOUR_PRIVATE_KEY"]
}
}
};
YOUR_ALCHEMY_PROJECT_ID: ALCHEMYで取得したプロジェクトID。
YOUR_PRIVATE_KEY: MetaMaskウォレットの秘密鍵。
不要なファイルを削除
rm YOUR_PATH/async-nft/contracts/contracts/Lock.sol
rm -rm YOUR_PATH/async-nft/contracts/ignition
rm -rf contracts/test
NFTスマートコントラクト作成
async-nft/contracts/contracts/NFT.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
contract MyNFT is ERC721 {
uint256 public tokenCounter;
constructor() ERC721("MyNFT", "MNFT") {
tokenCounter = 0;
}
function createNFT(address recipient) public returns (uint256) {
uint256 newTokenId = tokenCounter;
_safeMint(recipient, newTokenId);
tokenCounter++;
return newTokenId;
}
}
NFTコントラクトについて詳しく知りたい方は、openzeppelin/contracts/token/ERC721/ERC721.solをコードリーディングすると勉強になります。
デプロイスクリプト作成
async-nft/contracts/scripts/deploy.js
async function main() {
const [deployer] = await ethers.getSigners();
console.log("Deploying contracts with the account:", deployer.address);
const MyNFT = await ethers.getContractFactory("MyNFT");
const myNFT = await MyNFT.deploy();
await myNFT.waitForDeployment();
console.log("Contract deployed to:", myNFT.target);
}
main()
.then(() => process.exit(0))
.catch(error => {
console.error(error);
process.exit(1);
});
コントラクトのコンパイル
npx hardhat compile
コントラクトのデプロイ
npx hardhat run scripts/deploy.js --network sepolia
実行後以下のようにスマートコントラクトのアドレスが表示されるので、保管しておきます。
後のLambdaでのスマートコントラクト呼び出し設定の際に利用します。
Contract deployed to: YOUR_SMARTCONTRACT_ADDRESS
ステップ 4: Lambdaでスマートコントラクト呼び出すためのjsファイルを作成・圧縮
async-nft/lambda/index.js`を作成
const { Web3 } = require('web3');
exports.handler = async (event) => {
try {
// Web3インスタンス作成
const web3 = new Web3(process.env.ALCHEMY_ENDPOINT);
// コントラクト情報
const contractAddress = process.env.CONTRACT_ADDRESS;
const abi = JSON.parse(process.env.CONTRACT_ABI);
const contract = new web3.eth.Contract(abi, contractAddress);
console.log("Received event:", JSON.stringify(event, null, 2));
// メッセージを取得
const record = event.Records[0];
const sqsMessage = JSON.parse(record.body);
const messageData = JSON.parse(sqsMessage.Message);
// トランザクションデータ
const recipient = messageData.recipient;
console.log("recipient:", recipient);
const txData = contract.methods.createNFT(recipient).encodeABI();
// トランザクション署名用アカウント情報
const privateKey = `0x${process.env.WALLET_PRIVATE_KEY}`;
const account = web3.eth.accounts.privateKeyToAccount(privateKey);
// ガス関連情報取得
const gasPrice = await web3.eth.getGasPrice();
const estimatedGas = await contract.methods.createNFT(recipient).estimateGas({ from: account.address });
// トランザクションオブジェクトの作成
const tx = {
from: account.address,
to: contractAddress,
gas: estimatedGas,
gasPrice,
data: txData,
};
// トランザクションに署名
const signedTx = await web3.eth.accounts.signTransaction(tx, privateKey);
// トランザクションを送信
const receipt = await web3.eth.sendSignedTransaction(signedTx.rawTransaction);
console.log('Transaction successful with hash:', receipt.transactionHash);
return {
statusCode: 200,
body: JSON.stringify({ message: 'NFT successfully minted!', transactionHash: receipt.transactionHash }),
};
} catch (error) {
console.error('Error minting NFT:', error);
return {
statusCode: 500,
body: JSON.stringify({ message: 'Failed to mint NFT', error: error.message }),
};
}
};
async-nft/lambda/index.js`を圧縮
以下の手順でZIPファイルを作成します。
cd async-nft/lambda
npm install @aws-sdk/client-sns
npm install web3
zip -r lambda_function.zip index.js node_modules/
ステップ 5: LambdaでSNS呼び出すためのjsファイルを作成・圧縮
async-nft/lambda/sns_handler.js`を作成
const { SNS } = require('@aws-sdk/client-sns');
const sns = new SNS();
exports.handler = async (event) => {
const body = JSON.parse(event.body);
const message = {
recipient : body.recipient,
metadata : body.metadata,
};
await sns.publish({
TopicArn : process.env.SNS_TOPIC_ARN,
Message : JSON.stringify(message),
});
return {
statusCode : 200,
body : JSON.stringify({ message : 'Message sent to SNS successfully.' }),
};
};
async-nft/lambda/sns_handler.js`を圧縮
以下の手順でZIPファイルを作成します。
zip gateway_to_sns_lambda.zip sns_handler.js node_modules
ステップ 6: CroudFormation用のテンプレートを作成する
AWS CloudFormationは、AWSが提供するInfrastructure as Code (IaC)サービスで、AWSリソースをコード化して自動的にプロビジョニング、管理することができます。
- LocalStackを利用してローカル上で実行環境を構築し、エンドポイントのcallからスマートコントラクトが実行されるかを検証
- AWS上で実行環境を構築し、エンドポイントのcallからスマートコントラクトが実行されるかを検証
async-nft/template.yml`を作成
AWSTemplateFormatVersion: "2010-09-09"
Description: Template for async NFT minting using AWS serverless architecture.
Resources:
# NftLambdaFunction用のLambda実行ロール
# このロールは、Lambda関数が必要なAWSリソースにアクセスするための権限を持っています。
NftLambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: NftLambdaExecutionPolicy
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
- sqs:ReceiveMessage
- sqs:DeleteMessage
- sqs:GetQueueAttributes
- s3:GetObject
Resource: "*"
# ApiGatewayToSNSLambdaFunction用のLambda実行ロール
# このロールは、API GatewayからSNSにメッセージを送信するための権限を持っています。
ApiGatewayToSNSLambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: ApiGatewayToSNSLambdaExecutionPolicy
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
- sns:Publish
- s3:GetObject
Resource: "*"
# NFTの作成を行うLambda関数
# この関数は、SQSからメッセージを受け取り、NFTを作成します。
NftLambdaFunction:
Type: AWS::Lambda::Function
Properties:
Handler: index.handler
Runtime: nodejs18.x
Code:
S3Bucket: YOUR_BUCKET_NAME
S3Key: lambda_function.zip
MemorySize: 512
Timeout: 300
Role: !GetAtt NftLambdaExecutionRole.Arn
Environment:
Variables:
ALCHEMY_ENDPOINT: "https://eth-sepolia.g.alchemy.com/v2/YOUR_ALCHEMY_PROJECT_ID"
CONTRACT_ADDRESS: "YOUR_SMARTCONTRACT_ADDRESS"
WALLET_PRIVATE_KEY: "YOUR_PRIVATE_KEY"
CONTRACT_ABI: |
[
{
"constant": false,
"inputs": [{"name": "recipient", "type": "address"}],
"name": "createNFT",
"outputs": [{"name": "", "type": "uint256"}],
"type": "function"
}
]
# API GatewayからSNSにメッセージを送信するLambda関数
# この関数は、API Gatewayからのリクエストを受け取り、SNSにメッセージを送信します。
ApiGatewayToSNSLambdaFunction:
Type: AWS::Lambda::Function
Properties:
Handler: sns_handler.handler
Runtime: nodejs18.x
Code:
S3Bucket: YOUR_BUCKET_NAME
S3Key: gateway_to_sns_lambda.zip
MemorySize: 128
Timeout: 300
Role: !GetAtt ApiGatewayToSNSLambdaExecutionRole.Arn
Environment:
Variables:
SNS_TOPIC_ARN: !Ref NftSnsTopic
# SNSトピック
# メッセージを受け取り、SQSキューに転送します。
NftSnsTopic:
Type: AWS::SNS::Topic
Properties:
TopicName: nft-sns-topic
# SQSキュー
# SNSからのメッセージを受け取り、Lambda関数がポーリングします。
NftSqsQueue:
Type: AWS::SQS::Queue
Properties:
QueueName: nft-sqs-queue
VisibilityTimeout: 310
# SNSからSQSへのサブスクリプション
# SNSトピックからSQSキューへのメッセージ転送を設定します。
NftSnsSubscription:
Type: AWS::SNS::Subscription
Properties:
TopicArn: !Ref NftSnsTopic
Protocol: sqs
Endpoint: !GetAtt NftSqsQueue.Arn
# SNSがSQSにメッセージを送信するためのポリシー
# SNSトピックからSQSキューへのメッセージ送信を許可します。
NftSqsPolicy:
Type: AWS::SQS::QueuePolicy
Properties:
Queues:
- !Ref NftSqsQueue
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal: "*"
Action: sqs:SendMessage
Resource: !GetAtt NftSqsQueue.Arn
Condition:
ArnEquals:
aws:SourceArn: !Ref NftSnsTopic
# Lambda関数がSQSメッセージをポーリングするための設定
# SQSキューからメッセージを取得し、Lambda関数をトリガーします。
NftSQSEventSourceMapping:
Type: AWS::Lambda::EventSourceMapping
Properties:
BatchSize: 10
EventSourceArn: !GetAtt NftSqsQueue.Arn
FunctionName: !GetAtt NftLambdaFunction.Arn
# API Gatewayの設定
# REST APIを作成し、Lambda関数をトリガーします。
ApiGatewayResource:
Type: AWS::ApiGateway::RestApi
Properties:
Name: NFT API Gateway
# API Gatewayのデプロイメント
# APIをデプロイし、特定のステージで利用可能にします。
ApiGatewayDeployment:
Type: AWS::ApiGateway::Deployment
DependsOn:
- ApiGatewayResourceMethodTriggerNFTPost
Properties:
RestApiId: !Ref ApiGatewayResource
StageName: test
# API Gatewayのリソースパス設定
# 特定のパスでAPIを利用可能にします。
ApiGatewayResourceTriggerNFTPath:
Type: AWS::ApiGateway::Resource
Properties:
ParentId: !GetAtt ApiGatewayResource.RootResourceId
PathPart: trigger-nft
RestApiId: !Ref ApiGatewayResource
# API Gatewayのメソッド設定
# POSTメソッドを設定し、Lambda関数をトリガーします。
ApiGatewayResourceMethodTriggerNFTPost:
Type: AWS::ApiGateway::Method
Properties:
RestApiId: !Ref ApiGatewayResource
ResourceId: !Ref ApiGatewayResourceTriggerNFTPath
HttpMethod: POST
AuthorizationType: NONE
Integration:
Type: AWS_PROXY # Lambda統合のためAWS_PROXYを使用します。
IntegrationHttpMethod: POST
Uri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ApiGatewayToSNSLambdaFunction.Arn}/invocations
# API GatewayがSNSにメッセージを送信するためのIAMロール
# API GatewayがSNSにメッセージを送信する権限を持ちます。
ApiGatewayRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: apigateway.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: ApiGatewayPublishToSNSPolicy
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- sns:Publish
Resource: !Ref NftSnsTopic
# API GatewayがLambda関数を呼び出すための権限
# API GatewayがLambda関数をトリガーする権限を付与します。
ApiGatewayToSNSLambdaPermission:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName: !GetAtt ApiGatewayToSNSLambdaFunction.Arn
Principal: apigateway.amazonaws.com
YOUR_BUCKET_NAME: グローバルで任意の名前
YOUR_ALCHEMY_PROJECT_ID: ALCHEMYで取得したプロジェクトID。
YOUR_PRIVATE_KEY: MetaMaskウォレットの秘密鍵。
YOUR_SMARTCONTRACT_ADDRESS: 上記で保存したスマートコントラクトのアドレス
ステップ 7: LocalStackを利用してローカル環境にデプロイし、検証
必要なライブラリをinstall
brew install awscli-local
async-nft/docker-compose.ymlを作成
version: "3.8"
services:
localstack:
image: localstack/localstack
container_name: localstack_main
ports:
- "4566:4566" # LocalStack Gateway Port
environment:
- SERVICES=cloudformation,lambda,sns,sqs,apigateway,iam,s3
- DEBUG=1
volumes:
- "/var/run/docker.sock:/var/run/docker.sock"
- ./init-s3.sh:/usr/local/bin/init-s3.sh
- ./lambda/lambda_function.zip:/usr/local/bin/lambda_function.zip
- ./lambda/gateway_to_sns_lambda.zip:/usr/local/bin/gateway_to_sns_lambda.zip
- ./init-s3.sh:/etc/localstack/init/ready.d/init-s3.sh
async-nft/init-s3.shを作成
Container内のzipファイルをS3にアップロードするためのshellファイルを作成します。
#!/bin/bash
echo "Creating S3 bucket and uploading files to LocalStack..."
# S3バケットの作成
awslocal s3 mb s3://YOUR_BUCKET_NAME
# ファイルのアップロード
awslocal s3 cp /usr/local/bin/lambda_function.zip s3://YOUR_BUCKET_NAME/
awslocal s3 cp /usr/local/bin/gateway_to_sns_lambda.zip s3://YOUR_BUCKET_NAME/
Dockerコンテナーを起動
docker-compose up -d
Docker上でAWS環境を構築
以下のCloudFormationのコマンドを実行します。
awslocal cloudformation create-stack --stack-name my-stack --template-body file://template.yml --endpoint-url=http://localhost:4566
api idを取得
awslocal apigateway get-rest-apis
コマンドを実行すると以下が表示のように表示されます。
{
"items": [
{
"id": "YOUR_API_ID",
...
}
]
}
API gatewayのエンドポイントをcurlで呼び出し
curl -X POST -H "Content-Type: application/json" -d '{"message": "NFT minting request", "recipient": "YOUR_WALLET_ADDRESS"}' http://localhost:4566/restapis/mqsiz0fik0/test/_user_request_/trigger-nft
ログを確認
docker logs localstack_main
Transaction successful with hash: 0xADDRESS
etherscanで検索する
etherscanにて、トランザクションが作成されていることhashで検索することにより確認します。
ステップ 8: 本番環境環境にデプロイし、検証
必要なライブラリをinstall
brew install awscli
S3のbucketを作成し、zipファイルを配置
S3バケットの作成
- AWS Management Consoleにログインします。
- サービスメニューから「S3」を選択します。
- 「バケットを作成」をクリックします。
- 以下の情報を入力します:
-
バケット名: グローバルで一意の名前(例:
my-unique-bucket-name
)。 -
リージョン: データを保存するリージョン(例:
ap-northeast-1
)。
-
バケット名: グローバルで一意の名前(例:
- 必要に応じて以下の設定を行います:
- パブリックアクセス設定: デフォルトで「すべてのパブリックアクセスをブロック」が有効。
- バージョニング: バージョン管理を有効化する場合はチェック。
- 設定内容を確認し、「バケットを作成」をクリックします。
zipファイルのアップロード
- 作成したバケット名をクリックして開きます。
- 「アップロード」ボタンをクリックします。
- lambda_function.zip, gateway_to_sns_lambda.zipをドラッグ&ドロップするか、「ファイルの追加」をクリックして選択します。
- 「アップロード」をクリックしてファイルをアップロードします。
本番でAWS環境を構築
aws cloudformation create-stack \
--stack-name my-stack \
--template-body file://template.yml \
--capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM
api idを取得
awslocal apigateway get-rest-apis
コマンドを実行すると以下が表示のように表示されます。
{
"items": [
{
"id": "YOUR_API_ID",
...
}
]
}
API gatewayのエンドポイントをcurlで呼び出し
curl -X POST \
https://YOUR_API_ID.execute-api.ap-northeast-1.amazonaws.com/test/trigger-nft \
-H "Content-Type: application/json" \
-d '{
"recipient": "YOUR_WALLET_ADDRESS",
"metadata": {
"name": "My NFT",
"description": "This is a test NFT"
}
}'
ログを確認
CloudWatch > ロググループ > /aws/lambda/my-stack-NftLambdaFunction-xxx > ログストリーム に遷移
ログイベントの中で以下のメッセージを探す
Transaction successful with hash: 0xADDRESS
etherscanで検索する
etherscanにて、トランザクションが作成されていることhashで検索することにより確認します。
ステップ 9: 本番環境環境を削除する
CloudFormationのmy-stockを削除する
ハンズオンは以上になります!
お疲れさまでした!
こちらのハンズオンが皆さんのスキルアップの一助になれば幸いです!
もっとスキルアップをしたいという方は、是非是非Komlockが運営している、
Komlock Discordにご参加ください!
Web3の最新情報についてやり取りをしたり、Web3イベントに関しての共有を行なっています。
また、Komlock labはWeb3の未来を共創していきたいメンバーを募集しています!!
気軽にDM等でお声がけください。
Komlock labの企業アカウント
PR記事とCEOの創業ブログ
Discussion
2点気になったためコメントしておきます!
docker-compose up -d
の前にchmod +x init-s3.sh
しておくと、パーミッションでつまづくことがなくなるかと思いました。また
create-stack
しただけではAPIを叩くことはできず、awslocal apigateway create-deployment --rest-api-id YOUR_API_ID --stage-name test
を実行してあげる必要があるかと思いました。その他はスムーズにハンズオンできました!