MultiBaasでバックエンドアプリを構築する
MultiBaasデプロイメントを作成したら、それを使用してバックエンドアプリケーションを構築できます。通常、Solidityでスマートコントラクトを記述し、MultiBaasのWeb UIまたはHardhatやForgeプラグインを介してデプロイし、その後SDKのいずれか、またはMultiBaas APIを直接使用してブロックチェーンと対話します。
バックエンドAPIキーのプロビジョニング
バックエンドアプリ用のAPIキーは、MultiBaas Web UIを介してプロビジョニングできます。新しいコントラクトをリンクしたり、アドレスにエイリアスを追加したり、クラウドウォレットトランザクションに署名したりするなど、MultiBaasに変更を加える必要があるAPIキーには、Administrators権限を選択する必要があります。このレベルの権限を持つAPIキーはMultiBaasデプロイメントを完全に制御できるため、APIキーの保護とセキュリティには特別な注意を払う必要があります。Administrator APIキーは決して公開しないでください。
バックエンドアプリ用のMultiBaas機能
MultiBaasは、バックエンドアプリの構築に役立つ多数の機能を組み合わせています:
- スマートコントラクト関数との対話
- トランザクションを構成、署名(次のセクションを参照)、ブロックチェーンに送信し、監視し、webhookを介して通知を受け取る
- スマートコントラクトイベントを効率的に読み取りおよび集約
トランザクションの署名
クラウドウォレット
クラウドウォレットは、MultiBaasでトランザクションに署名してブロックチェーンに送信する最も簡単な方法です。ブロックチェーンに書き込むスマートコントラクト関数呼び出しと一緒に"signAndSubmit": trueパラメータを渡すだけで、MultiBaasはクラウドウォレットにトランザクションに署名させ、自動的にブロックチェーンに送信します。これはETHの転送やコントラクトのデプロイでも機能します。MultiBaasは署名されたトランザクションハッシュを返し、これを使用してAPI経由でトランザクションまたはそのレシートを検索したり、MultiBaas Web UIのトランザクションエクスプローラーを使用したりできます。
クラウドウォレットトランザクションは、トランザクションマネージャー(TXM)で管理することもでき、ブロックに含まれたらwebhookを発行するように構成できます。クラウドウォレットは、生のトランザクションの署名と送信や型付きまたは型なしデータの署名にも使用できます。
シードフレーズまたは秘密鍵
バックエンドアプリ内では、シードフレーズと秘密鍵を保存および管理する方法は多数あります。次の例では、秘密鍵が環境変数を介してインポートされ、それ以外は安全に管理されていることを前提としています。
MultiBaasは、署名されたトランザクションをブロックチェーンに送信するAPIエンドポイントも提供しているため、別のJSON RPCプロバイダーは必要ありません。
TypeScript (JavaScript)
ethers.jsは、ブロックチェーンの対話を管理するためのJSツールキットです。MultiBaasからの未署名トランザクションは、ethers.js用に若干再フォーマットする必要があります。次のサンプルコードは、スマートコントラクトの書き込み関数を呼び出し、返された未署名トランザクションを再フォーマットし、接続されたweb3ブラウザウォレットに送信して署名とブロックチェーンへの送信を行います。
スマートコントラクト関数を呼び出してMultiBaas APIから未署名トランザクションを取得し、トランザクションに署名してブロックチェーンに送信
require('dotenv').config();
const { ethers } = require('ethers');
const MultiBaas = require('@curvegrid/multibaas-sdk');
const { isAxiosError } = require('axios');
const { PRIVATE_KEY, SEED_PHRASE, RPC_URL, MULTIBAAS_API_KEY, MULTIBAAS_BASE_URL } = process.env;
if (!PRIVATE_KEY && !SEED_PHRASE) {
throw new Error('Missing PRIVATE_KEY or SEED_PHRASE in environment.');
}
if (!MULTIBAAS_API_KEY) {
throw new Error('Missing MULTIBAAS_API_KEY in environment.');
}
if (!RPC_URL) {
throw new Error('Missing RPC_URL in environment.');
}
// Setup MultiBaas
const config = new MultiBaas.Configuration({
basePath: `${MULTIBAAS_BASE_URL || 'http://<YOUR MULTIBAAS DEPLOYMENT ID>.multibaas.com'}/api/v0`,
accessToken: MULTIBAAS_API_KEY,
});
const contractsApi = new MultiBaas.ContractsApi(config);
const chainsClient = new MultiBaas.ChainsApi(config);
const provider = new ethers.JsonRpcProvider(RPC_URL);
// Set up wallet
let wallet;
if (PRIVATE_KEY) {
wallet = new ethers.Wallet(PRIVATE_KEY, provider);
} else {
const hdNode = ethers.HDNodeWallet.fromPhrase(SEED_PHRASE);
wallet = new ethers.Wallet(hdNode.privateKey, provider);
}
console.log('Wallet address:', wallet.address);
async function getUnsignedTransaction(
AddressAliasOrRawAddress,
contractLabel,
contractMethod,
payload,
) {
try {
// Fetch unsigned transaction from MultiBaas
const resp = await contractsApi.callContractFunction(
AddressAliasOrRawAddress,
contractLabel,
contractMethod,
payload,
);
const unsignedTx = resp.data.result.tx;
console.log('Unsigned Tx:', unsignedTx);
return unsignedTx;
} catch (error) {
console.error('Error fetching unsigned transaction:', error);
}
}
async function getChainId() {
const resp = await chainsClient.getChainStatus();
return resp.data.result.chainID;
}
async function signAndSendTransaction(txData) {
try {
if (!txData) {
throw new Error('Transaction data is undefined');
}
// Create a properly formatted transaction object with correct field names
const formattedTx = {
to: txData.to,
from: txData.from,
nonce: txData.nonce,
data: txData.data,
value: txData.value || '0x0',
gasLimit: txData.gas, // Map gas to gasLimit
maxFeePerGas: txData.gasFeeCap, // Map gasFeeCap to maxFeePerGas
maxPriorityFeePerGas: txData.gasTipCap, // Map gasTipCap to maxPriorityFeePerGas
type: txData.type,
chainId: await getChainId(),
};
// Sign transaction
const signedTx = await wallet.signTransaction(formattedTx);
console.log('Signed Tx:', signedTx);
// Submit via ethers provider
const txResponse = await provider.broadcastTransaction(signedTx);
console.log('Transaction submitted:', txResponse.hash);
return {
txHash: txResponse.hash,
signedTx,
rawResponse: txResponse,
};
} catch (error) {
if (isAxiosError(error)) {
console.error(
`MultiBaas error [${error.response?.status}]: ${error.response?.data?.message}`,
);
} else {
console.error('Transaction error:', error);
}
throw error;
}
}
// Parse command-line arguments
function parseArgs() {
const args = process.argv.slice(2);
const params = {};
for (let i = 0; i < args.length; i += 2) {
if (args[i].startsWith('-')) {
const key = args[i].substring(1);
const value = args[i + 1];
params[key] = value;
}
}
return params;
}
async function main() {
try {
const params = parseArgs();
// Validate required parameters
if (!params.address && !params.alias) {
throw new Error('Missing required parameter: address or alias');
}
if (!params.method) {
throw new Error('Missing required parameter: -method');
}
if (!params['contract-label']) {
throw new Error('Missing required parameter: -contract-label');
}
// Parse payload if provided
let args = [];
if (params.args) {
try {
args = JSON.parse(params.args);
} catch (e) {
console.warn('Could not parse payload as JSON, using as string argument');
args = [params.args];
}
}
const { alias, address, 'contract-label': contractLabel, method: contractMethod } = params;
// Prepare payload for MultiBaas callContractFunction
payload = {
args: args,
from: wallet.address,
};
const txData = await getUnsignedTransaction(
alias || address,
contractLabel,
contractMethod,
payload,
);
const result = await signAndSendTransaction(txData);
console.log('Transaction complete!');
console.log('Transaction hash:', result.txHash);
} catch (error) {
console.error('Error in main:', error.message);
process.exit(1);
}
}
// Execute main function if this script is run directly
if (require.main === module) {
main().catch(console.error);
}
// Export the function for use in other scripts
module.exports = {
signAndSendTransaction,
};
次のように実行できます:
node index.js -address <0x-your-contract-address> -contract-label <your-contract-label> -method <your-contract-method-name>
Go
次は、MultiBaasがERC20トークンをあるアドレスから別のアドレスに転送するトランザクションを構成し、それがGoアプリケーション内の秘密鍵によって署名され、MultiBaasを介してブロックチェーンに送信される、完全なコマンドラインアプリケーションです。
次の環境変数を設定することで、いくつかのセットアップが必要です:
export MULTIBAAS_DEPLOYMENT_URL="https://<YOUR MULTIBAAS DEPLOYMENT ID>.multibaas.com"
export MULTIBAAS_API_KEY="<YOUR MULTIBAAS DAPP USER API KEY>"
export PRIVATE_KEY_IN_HEX="<PRIVATE KEY FOR SIGNING THE TRANSACTION>"
さらに、ERC20スマートコントラクトがブロックチェーンにデプロイされている必要があり、署名(送信)アドレスには十分なトークン残高が必要です。
プログラムは、いくつかのコマンドライン引数を指定することで実行できます:
go run . -addressOrAlias sampletoken -to 0x1C568f7B1de5fdEe019fD1213160241F4e470e02 -amount 1
ここで、-addressOrAlias sampletokenはERC20スマートコントラクトがデプロイされているアドレスエイリアス、-to 0x1C568f7B1de5fdEe019fD1213160241F4e470e02は受信者アドレス、-amount 1は送信するERC20トークンの数量です。
成功すると、プログラムは次のような出力を行います:
Signed transaction hash: 0x969978e258248b1648cdf510de29c787a6346f16b7376cd0bdaf41f796a77d7e
Transaction submitted successfully!
完全なGoプログラムのサンプルソースコード
package main
import (
"context"
"crypto/ecdsa"
"flag"
"fmt"
"math/big"
"os"
mb "github.com/curvegrid/multibaas-sdk-go"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
)
const (
contractMethod = "transfer" // the smart contract function to call
)
// getUnsignedTransaction retrieves the unsigned transaction from the MultiBaas API
// and calls a smart contract function on the Ethereum blockchain
// using the MultiBaas API client.
func getUnsignedTransaction(ctx context.Context, client *mb.APIClient, addressOrAlias, contractLabel, from, to, value string) (*mb.TransactionToSignTx, error) {
payload := mb.PostMethodArgs{
Args: []interface{}{
to,
value,
},
From: mb.PtrString(from),
ContractOverride: mb.PtrBool(true), // don't require the contract to be linked to the address
}
// call the smart contract function and receive
resp, _, err := client.ContractsAPI.CallContractFunction(ctx, addressOrAlias, contractLabel, contractMethod).PostMethodArgs(payload).Execute()
if err != nil {
if mbErr, ok := mb.IsMultiBaasErr(err); ok {
return nil, fmt.Errorf("MultiBaas returned an error with status '%d' and message: '%s'\n", mbErr.Status, mbErr.Message)
}
return nil, fmt.Errorf("An unexpected error occurred: %v\n", err)
}
return &resp.Result.TransactionToSignResponse.Tx, nil
}
// multibaasTxToGoEthereumTxData converts a MultiBaas transaction to a Go Ethereum transaction.
func multibaasTxToGoEthereumTxData(chainID int64, mbTx *mb.TransactionToSignTx) (types.TxData, error) {
// setup the transaction data structure
var to *common.Address // default to a nil address, in case this is for deploying a contract
if mbTx.To.Get() != nil {
tempTo := common.HexToAddress(*mbTx.To.Get())
to = &tempTo
}
value, ok := big.NewInt(0).SetString(mbTx.Value, 10)
if !ok {
return nil, fmt.Errorf("failed to parse value: %s", mbTx.Value)
}
gasTipCap, ok := big.NewInt(0).SetString(*mbTx.GasTipCap, 10)
if !ok {
return nil, fmt.Errorf("failed to parse gasTipCap: %s", *mbTx.GasTipCap)
}
gasFeeCap, ok := big.NewInt(0).SetString(*mbTx.GasFeeCap, 10)
if !ok {
return nil, fmt.Errorf("failed to parse gasFeeCap: %s", *mbTx.GasFeeCap)
}
txData := &types.DynamicFeeTx{
ChainID: big.NewInt(chainID),
Nonce: uint64(mbTx.Nonce),
To: to,
Value: value,
Gas: uint64(mbTx.Gas),
GasTipCap: gasTipCap,
GasFeeCap: gasFeeCap,
Data: common.FromHex(mbTx.Data),
}
return txData, nil
}
// signTransaction signs the transaction using the private key.
func signTransaction(chainID int64, privateKey *ecdsa.PrivateKey, txData types.TxData) (*types.Transaction, error) {
// setup the signer
signer := types.NewLondonSigner(big.NewInt(chainID))
// sign the transaction
signedTx, err := types.SignNewTx(privateKey, signer, txData)
if err != nil {
return nil, err
}
return signedTx, nil
}
// submitTransaction submits the signed transaction to the blockchain via MultiBaas.
func submitTransaction(ctx context.Context, client *mb.APIClient, signedTx string) error {
signedTransactionSubmission := mb.SignedTransactionSubmission{
SignedTx: signedTx,
}
_, _, err := client.ChainsAPI.SubmitSignedTransaction(ctx).SignedTransactionSubmission(signedTransactionSubmission).Execute()
if err != nil {
if mbErr, ok := mb.IsMultiBaasErr(err); ok {
return fmt.Errorf("MultiBaas returned an error with status '%d' and message: '%s'\n", mbErr.Status, mbErr.Message)
}
return fmt.Errorf("An unexpected error occurred: %v\n", err)
}
return nil
}
// setupPrivateKey sets up the private key from the hex string.
func setupPrivateKey(hexPrivateKey string) (*ecdsa.PrivateKey, common.Address, error) {
// convert the hex string to a byte array
privateKeyBytes := common.FromHex(hexPrivateKey)
if len(privateKeyBytes) != 32 {
return nil, common.Address{}, fmt.Errorf("invalid private key length: %d", len(privateKeyBytes))
}
// parse the private key
privateKey, err := crypto.ToECDSA(privateKeyBytes)
if err != nil {
return nil, common.Address{}, fmt.Errorf("failed to parse private key: %v", err)
}
// compute the sender address from the private key
privateKeyAddress := crypto.PubkeyToAddress(privateKey.PublicKey)
return privateKey, privateKeyAddress, nil
}
// getChainID retrieves the chain ID from the MultiBaas API.
func getChainID(ctx context.Context, client *mb.APIClient) (int64, error) {
resp, _, err := client.ChainsAPI.GetChainStatus(ctx).Execute()
if err != nil {
if mbErr, ok := mb.IsMultiBaasErr(err); ok {
return 0, fmt.Errorf("MultiBaas returned an error with status '%d' and message: '%s'\n", mbErr.Status, mbErr.Message)
}
return 0, fmt.Errorf("An unexpected error occurred: %v\n", err)
}
return resp.Result.ChainID, nil
}
func main() {
// retrieve private configuration values from environment variables
mbDeploymentURL := os.Getenv("MULTIBAAS_DEPLOYMENT_URL")
if mbDeploymentURL == "" {
fmt.Println("Please set the environment variable 'MULTIBAAS_DEPLOYMENT_URL' to your MultiBaas deployment URL, 'i.e. https://sdjflkj34nsdfkj4k.multibaas.com'")
return
}
mbAPIKey := os.Getenv("MULTIBAAS_API_KEY")
if mbAPIKey == "" {
fmt.Println("Please set the environment variable 'MULTIBAAS_API_KEY' to your MultiBaas API key.")
return
}
hexPrivateKey := os.Getenv("PRIVATE_KEY_IN_HEX")
if hexPrivateKey == "" {
fmt.Println("Please set the environment variable 'PRIVATE_KEY_IN_HEX' to your private key in hex format.")
return
}
// retrieve additional configuration valus from the command line
addressOrAlias := flag.String("addressOrAlias", "", "The address or address alias of the ERC20 smart contract.")
contractLabel := flag.String("contractLabel", "erc20interface", "The label of the ERC20 smart contract.")
to := flag.String("to", "", "The address of the recipient.")
amount := flag.String("amount", "", "The amount of tokens to send.")
flag.Parse()
if *addressOrAlias == "" || *contractLabel == "" || *to == "" || *amount == "" {
fmt.Println("Please provide the address or alias, contract label, recipient address, and amount.")
return
}
// setup the MultiBaas SDK
conf := mb.NewConfiguration()
client := mb.NewAPIClient(conf)
ctx := context.Background()
ctx = context.WithValue(ctx, mb.ContextServerVariables, map[string]string{
"base_url": mbDeploymentURL,
})
ctx = context.WithValue(ctx, mb.ContextAccessToken, mbAPIKey)
// retrieve the chain ID from MultiBaas
chainID, err := getChainID(ctx, client)
if err != nil {
fmt.Printf("Error getting chain ID: %v\n", err)
return
}
// setup the private key
privateKey, privateKeyAddress, err := setupPrivateKey(hexPrivateKey)
if err != nil {
fmt.Printf("Error setting up private key: %v\n", err)
return
}
// get the unsigned transaction from MultiBaas
mbTx, err := getUnsignedTransaction(ctx, client, *addressOrAlias, *contractLabel, privateKeyAddress.Hex(), *to, *amount)
if err != nil {
fmt.Printf("Error getting unsigned transaction: %v\n", err)
return
}
// convert the MultiBaas transaction to a Go Ethereum transaction
txData, err := multibaasTxToGoEthereumTxData(chainID, mbTx) // 1 is the chain ID for Ethereum mainnet
if err != nil {
fmt.Printf("Error converting MultiBaas transaction to Go Ethereum transaction: %v\n", err)
return
}
// sign the unsigned transaction
signedTx, err := signTransaction(chainID, privateKey, txData)
if err != nil {
fmt.Printf("Error signing transaction: %v\n", err)
return
}
fmt.Printf("Signed transaction hash: %s\n", signedTx.Hash().Hex())
// convert the signed transaction to a hex string
signedTxBytes, err := signedTx.MarshalBinary()
if err != nil {
fmt.Printf("Error marshaling transaction: %v\n", err)
return
}
signedTxHex := common.Bytes2Hex(signedTxBytes)
// submit the signed transaction to the blockchain via MultiBaas
err = submitTransaction(ctx, client, signedTxHex)
if err != nil {
fmt.Printf("Error submitting transaction: %v\n", err)
return
}
fmt.Println("Transaction submitted successfully!")
}