所要時間:20〜30分
Sequence Transactions API はサーバーレスの Cloudflare Worker 上で実装でき、ゲームやアプリのユーザーは確認サインやガス代の支払いなしでシームレスに利用できます。また、リレイヤーによるトランザクション速度やスループット、再編成を心配する必要がなくなり、Cloudflare による自動スケーリングも利用できます。
以下のステップで、ホスト型ミンター API を 4 ステップで構築する方法をご案内します。
- Wrangler Cli で Cloudflare 環境をセットアップしテストをデプロイする
- Sequence Builder で ERC1155 コントラクトをデプロイ、スポンサー登録、およびメタデータの更新
- EthAuthProof を使って EOA DDoS を防ぐ
- コレクティブルをウォレットにミントする
結果として、以下の仕様を持つセキュアな API が完成します。
- HTTPS GET:blockNumber を返す
- HTTPS POST(proof, address):コレクティブルをミントし、トランザクションハッシュを返す
この実装を完了するには、wrangler cli、npm、Sequence Builder の基本知識が必要です。
1. Wrangler Cli で Cloudflare 環境をセットアップしテストをデプロイする
まず、mkdir
でディレクトリを作成し、cd
でそのディレクトリに移動し、pnpm init
でpackage.json
を作成します。
次に、プロジェクトに wrangler cli がインストールされていることを確認し、wrangler
キーワードをローカル bash セッションのエイリアスとして設定します。
pnpm install wrangler --save-dev
alias wrangler='./node_modules/.bin/wrangler'
Cloudflareサイトでアカウントを作成し、ログインしてCloudflareダッシュボードにアクセスし、Cloudflareプラットフォームをローカル開発環境と接続します。
ログイン後、ディレクトリ内でプロジェクトを初期化し、気に入ったランダム生成のプロジェクトフォルダ名を選択して、git で管理されている TypeScript の "Hello World" Worker
アプリケーションを初期化するプロンプトに従ってください。
このステップを完了するには、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 permission
を Sequence Wallet Transactions API Address
に付与してください。
利用する transactions API アドレスを知るには、まず以下のいずれかの方法で取得します。
- このアプリ を使い、ネットワークを選択して
generate local wallet
ボタンでウォレットキーを生成することで取得できます(デモ目的でのみご利用ください)。
推奨
: 以下のコードスニペットを使い、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 のディレクトリとプロジェクトに戻り、ethers
と 0xsequence
をインストールして Sequence API にアクセスし、リクエストが信頼できる Sequence ウォレットから来ているかどうかの証明を検証します。
pnpm install 0xsequence @0xsequence/network
次に、POST か GET かを判定後、ミドルウェアを追加してください。POST リクエストの場合は、渡された proofString
と address
、および環境変数が有効かどうかを検証します。
src/index.ts
に配置するコードの雛形はこのようになります。callContract
と getBlockNumber
はモック化されており、コントラクト呼び出し前に 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
そして、以前モックしていた callContract
と getBlockNumber
メソッドを次のように実装します:
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リクエストではブロック番号が返されるはずです。
全コードを確認したい場合は、こちらの実装例をご覧ください