所要時間:20〜30分

Sequence Transactions API はサーバーレスの Cloudflare Worker 上で実装でき、ゲームやアプリのユーザーは確認サインやガス代の支払いなしでシームレスに利用できます。また、リレイヤーによるトランザクション速度やスループット、再編成を心配する必要がなくなり、Cloudflare による自動スケーリングも利用できます。

以下のステップで、ホスト型ミンター API を 4 ステップで構築する方法をご案内します。

  1. Wrangler Cli で Cloudflare 環境をセットアップしテストをデプロイする
  2. Sequence Builder で ERC1155 コントラクトをデプロイ、スポンサー登録、およびメタデータの更新
  3. EthAuthProof を使って EOA DDoS を防ぐ
  4. コレクティブルをウォレットにミントする

結果として、以下の仕様を持つセキュアな API が完成します。

  • HTTPS GET:blockNumber を返す
  • HTTPS POST(proof, address):コレクティブルをミントし、トランザクションハッシュを返す

この実装を完了するには、wrangler cli、npm、Sequence Builder の基本知識が必要です。

1. Wrangler Cli で Cloudflare 環境をセットアップしテストをデプロイする

まず、mkdirでディレクトリを作成し、cdでそのディレクトリに移動し、pnpm initpackage.jsonを作成します。

次に、プロジェクトに wrangler cli がインストールされていることを確認し、wrangler キーワードをローカル bash セッションのエイリアスとして設定します。

pnpm install wrangler --save-dev
alias wrangler='./node_modules/.bin/wrangler'

Cloudflareサイトでアカウントを作成し、ログインしてCloudflareダッシュボードにアクセスし、Cloudflareプラットフォームをローカル開発環境と接続します。

wrangler login

ログイン後、ディレクトリ内でプロジェクトを初期化し、気に入ったランダム生成のプロジェクトフォルダ名を選択して、git で管理されている TypeScript の "Hello World" Worker アプリケーションを初期化するプロンプトに従ってください。

wrangler init

このステップを完了するには、wrangler init後にエンターを4回押し、最後の2つの質問にはNoと答えてgitバージョン管理とデプロイをスキップしてください。

これにより、クラウドにコードをデプロイできるスターターリポジトリがクローンされます。

ローカル API テスト
ガイドの任意のタイミングで、プロジェクトフォルダ内で wrangler dev コマンドを使ってローカルテストを実行できます。

デプロイテスト

最後に、ランダムに生成されたプロジェクトフォルダにcdで移動し、wrangler deployコマンドを実行します。

これによりURLが表示されるので、ブラウザでhttps://<app>.<account>.workers.devにアクセスし、「Hello World!」の結果を確認できます。

2. Sequence Builder で ERC1155 コントラクトをデプロイ、スポンサー登録、およびメタデータの更新

Transactions API を利用するには、Sequence Builder でプロジェクトの課金プランを Developer にアップグレードする必要があります。こちらの手順 をご参照ください。

まず このガイド に従ってコントラクトをデプロイします。

次に、Sequence Builder でコントラクトのロールアクセスを2つの手順でミンターウォレットアドレスからのみリクエストを受け付けるように設定します。

Sequence Builderで minter permissionSequence Wallet Transactions API Address に付与してください。

利用する transactions API アドレスを知るには、まず以下のいずれかの方法で取得します。

  1. このアプリ を使い、ネットワークを選択して generate local wallet ボタンでウォレットキーを生成することで取得できます(デモ目的でのみご利用ください)。
  2. 推奨: 以下のコードスニペットを使い、EOA ウォレットの秘密鍵から生成されたアカウントアドレスをローカルで出力することもできます。
import { Session } from "@0xsequence/auth";
import { ethers } from "ethers";

(async () => {
  // Generate a new EOA
  // const wallet = ethers.Wallet.createRandom()
  // const privateKey = wallet.privateKey

  // Or, use an existing EOA private key
  const privateKey = "";

  // Open a Sequence session, this will find or create
  // a Sequence wallet controlled by your server EOA
  const session = await Session.singleSigner({
    signer: privateKey,
    projectAccessKey: "access_key",
  });

  const signer = session.account.getSigner(1);
  console.log(`Your transactions API wallet address: ${signer.account.address}`);
})();

プロジェクトを開き、Contracts ページに移動し、Linked contracts を選択、Write Contract タブで grantRole メソッドを展開します。

以下の情報で入力してください。

bytes32 role: 0x9f2df0fed2c77648de5860a4cc508cd0818c85b8b8a1ab4ceeef8d981c8956a6

address account: <Generated Sequence Transactions API Wallet Address>

ここで入力する role 文字列は、solidity では keccak256("MINTER_ROLE")、javascript では ethers.keccak256(ethers.toUtf8Bytes("MINTER_ROLE")) の結果です。

これにより、特定のアドレスのみがコントラクトからミントできるようになり、それ以外はエラーとなります。

write をクリックし、スポンサー付きトランザクションに署名してロールの更新を完了してください。

メタデータの更新

次に、このガイド に従って、コントラクトのメディアやアセットを用いてメタデータを更新します。

コントラクトのスポンサー登録

最後に、コントラクトをスポンサーするには このガイド をご参照ください。

3. EthAuthProof を使って EOA DDoS を防ぐ

コントラクトのデプロイが完了したら、Cloudflare Worker のディレクトリとプロジェクトに戻り、ethers0xsequence をインストールして Sequence API にアクセスし、リクエストが信頼できる Sequence ウォレットから来ているかどうかの証明を検証します。

pnpm install 0xsequence @0xsequence/network

次に、POST か GET かを判定後、ミドルウェアを追加してください。POST リクエストの場合は、渡された proofStringaddress、および環境変数が有効かどうかを検証します。

src/index.ts に配置するコードの雛形はこのようになります。callContractgetBlockNumber はモック化されており、コントラクト呼び出し前に verify を呼ぶ検証ステップが含まれています。

import { sequence } from "0xsequence";
import { networks, findSupportedNetwork } from "@0xsequence/network";

export interface Env {
  PKEY: string; // Private key for EOA wallet
  CONTRACT_ADDRESS: string; // Deployed ERC1155 or ERC721 contract address
  PROJECT_ACCESS_KEY: string; // From sequence.build
  CHAIN_HANDLE: string; // Standardized chain name – See https://docs.sequence.xyz/multi-chain-support
}

// use the sequence api to verify proof came from a sequence wallet
const verify = async (
  chainId: string,
  walletAddress: string,
  ethAuthProofString: string
): Promise<Boolean> => {
  const api = new sequence.api.SequenceAPIClient("https://api.sequence.app");
  const { isValid } = await api.isValidETHAuthProof({
    chainId,
    walletAddress,
    ethAuthProofString,
  });
  return isValid;
};

async function handleRequest(
  request: Request,
  env: Env,
  ctx: ExecutionContext
): Promise<Response> {

  if (request.method === "OPTIONS") {
		return new Response(null, {
			headers: {
				// Allow requests from any origin - adjust this as necessary
				"Access-Control-Allow-Origin": "*",
				
				// Allows the headers Content-Type, your-custom-header
				"Access-Control-Allow-Headers": "Content-Type, your-custom-header",
				
				// Allow POST method - add any other methods you need to support
				"Access-Control-Allow-Methods": "POST",
				
				// Optional: allow credentials
				"Access-Control-Allow-Credentials": "true",
				
				// Preflight cache period
				"Access-Control-Max-Age": "86400", // 24 hours
			}
		});
	}

  if (env.PKEY === undefined || env.PKEY === "") {
    return new Response("Make sure PKEY is configured in your environment", {
      status: 400,
    });
  }

  if (env.CONTRACT_ADDRESS === undefined || env.CONTRACT_ADDRESS === "") {
    return new Response(
      "Make sure CONTRACT_ADDRESS is configured in your environment",
      { status: 400 }
    );
  }

  if (env.PROJECT_ACCESS_KEY === undefined || env.PROJECT_ACCESS_KEY === "") {
    return new Response(
      "Make sure PROJECT_ACCESS_KEY is configured in your environment",
      { status: 400 }
    );
  }

  if (env.CHAIN_HANDLE === undefined || env.CHAIN_HANDLE === "") {
    return new Response(
      "Make sure CHAIN_HANDLE is configured in your environment",
      { status: 400 }
    );
  }

  const chainConfig = findSupportedNetwork(env.CHAIN_HANDLE);

  if (chainConfig === undefined) {
    return new Response("Unsupported network or unknown CHAIN_HANDLE", {
      status: 400,
    });
  }

  // POST request
  if (request.method === "POST") {
    // parse the request body as JSON
    const body = await request.json();
    const { proof, address, tokenId }: any = body;
    try {
      // check that the proof is valid
      if (await verify(env.CHAIN_HANDLE, address, proof)) {
        try {
          // mocked call
          const res = await callContract(request, env, address, tokenId);
          return new Response(`${res.hash}`, { status: 200 });
        } catch (err: any) {
          console.log(err);
          return new Response(`Something went wrong: ${JSON.stringify(err)}`, {
            status: 400,
          });
        }
      } else {
        return new Response(`Unauthorized`, { status: 401 });
      }
    } catch (err: any) {
      return new Response(`Unauthorized ${JSON.stringify(err)}`, {
        status: 401,
      });
    }
  }
  // GET request
  else {
    try {
      // mocked call
      const res = await getBlockNumber(env.CHAIN_HANDLE, request);
      return new Response(`Block Number: ${res}`);
    } catch (err: any) {
      return new Response(`Something went wrong: ${JSON.stringify(err)}`, {
        status: 500,
      });
    }
  }
}

const getBlockNumber = async (
  chainId: string,
  request: Request
): Promise<number> => {
  return chainId;
};

const callContract = async (
  request: Request,
  env: Env,
  address: string,
  tokenId: number
): Promise<ethers.providers.TransactionResponse> => {
  return { hash: "0x" } as any;
};

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext) {
    // Process the request and create a response
    const response = await handleRequest(request, env, ctx);

    // Set CORS headers
    response.headers.set("Access-Control-Allow-Origin", "*");
    response.headers.set(
      "Access-Control-Allow-Methods",
      "GET, POST, PUT, DELETE, OPTIONS"
    );
    response.headers.set("Access-Control-Allow-Headers", "Content-Type");

    // return response
    return response;
  },
};

Cloudflare 環境変数の追加

次に、wrangler.toml[vars] セクションを更新してビルド用の環境変数を渡します。

[vars]
PKEY = "" # Private key for EOA wallet
CONTRACT_ADDRESS = "" # // Deployed ERC1155 or ERC721 contract address
PROJECT_ACCESS_KEY = "" # From sequence.build
CHAIN_HANDLE = "" # // Standardized chain name – See https://docs.sequence.xyz/multi-chain-support

Wrangler テンプレートで Window オブジェクトを実装する

なお、デプロイしようとすると、web3モジュールで必要な window オブジェクトが見つからないというエラーが発生します。

これを防ぐには、wrangler.toml ファイルに以下の行を追加し、環境を対応させてください。

...
node_compat = true # add this line
...

デプロイのテスト

wrangler deploy を使用して再デプロイできます。

次に、以下のように curl リクエストでエンドポイントをテストします。

curl -X POST https://your-worker.your-subdomain.workers.dev \
-H "Content-Type: application/json" \
-d '{"proof": "<some_proof>", "address": "<some_address>", "tokenId": 0 }'

... invalid proof string ...

# and if you replace with actual proof (from a wallet client login) and address on polygon, it should return
success

このdappを利用し、以下の手順でウォレットアドレスの証明を取得します。

ETHAuthProof Viewer dapp の利用

ページにアクセスしたら、最初にネットワークを選択してください。

次に、「接続してProof(証明)を生成」または「ローカルウォレットを生成」のいずれかを選択できます。

connect ボタンを押し、その後 copy to clipboard をクリックしてください。

なお、この ETHAuthProof を他人と共有しないことをおすすめします。これを共有すると、他人があなたのウォレットの所有者であることを証明し、特定のAPIとやり取りできてしまいます。

最後に、この手順で取得したアプリの url、ビューワーdappからコピーした値を <some_proof>、あなたのウォレットアドレスを <some_address> にそれぞれ置き換えてください。これで、モックされた 0x 文字列が返されます。

curl -X POST https://your-worker.your-subdomain.workers.dev \
-H "Content-Type: application/json" \
-d '{"proof": "<some_proof>", "address": "<some_address>", "tokenId": 0 }'

4. コレクティブルをウォレットにミントする

スポンサー付きコントラクトアドレスからコレクティブルをデプロイ・ミントするには、以下のパッケージをインストールします。

pnpm install @0xsequence/auth ethers

そして、以前モックしていた callContractgetBlockNumber メソッドを次のように実装します:

import { ethers } from 'ethers'
import { Session, SessionSettings } from '@0xsequence/auth'

...

const getBlockNumber = async (chainId: string, request: Request): Promise<number> => {
	const nodeUrl = `https://nodes.sequence.app/${chainId}`
	const provider = new ethers.providers.JsonRpcProvider({ url: nodeUrl, skipFetchSetup: true })
	return await provider.getBlockNumber()
}

const callContract = async (request: Request, env: Env, address: string, tokenId: number): Promise<ethers.providers.TransactionResponse> => {

	const nodeUrl = `https://nodes.sequence.app/${env.CHAIN_HANDLE}`
	const relayerUrl = `https://${env.CHAIN_HANDLE}-relayer.sequence.app`
	const contractAddress = env.CONTRACT_ADDRESS


	// instantiate settings
	const settings: Partial<SessionSettings> = {
		networks: [{
			...networks[findSupportedNetwork(env.CHAIN_HANDLE)!.chainId],
			rpcUrl: findSupportedNetwork(env.CHAIN_HANDLE)!.rpcUrl,
			relayer: {
				url: relayerUrl,
				provider: {
					url: findSupportedNetwork(env.CHAIN_HANDLE)!.rpcUrl
				}
			}
		}],
	}

    // create a single signer sequence wallet session
	const session = await Session.singleSigner({
		settings: settings,
		signer: env.PKEY,
		projectAccessKey: env.PROJECT_ACCESS_KEY
	})

	// get signer
	const signer = session.account.getSigner(findSupportedNetwork(env.CHAIN_HANDLE)!.chainId)

	// create interface from partial abi
	const collectibleInterface = new ethers.Interface([
		'function mint(address to, uint256 tokenId, uint256 amount, bytes data)'
	])

	// create calldata
	const data = collectibleInterface.encodeFunctionData(
		'mint', [address, tokenId, 1, "0x00"]
	)

	// create transaction object
	const txn = { to: contractAddress, data }

	try {
		return await signer.sendTransaction(txn)
	} catch (err) {
		throw err
	}
}

これらの手順が完了したら、前の手順に従って再デプロイおよびテストできます。今回は、POSTリクエストでミント完了のトランザクションハッシュが返り、GETリクエストではブロック番号が返されるはずです。

全コードを確認したい場合は、こちらの実装例をご覧ください