Build a backend app on MultiBaas
Once you have created a MultiBaas deployment, you can use it to build a backend application. Typically, you will write a smart contract in Solidity, deploy it via MultiBaas' web UI or via our Hardhat or Forge plugins, and then use one of our SDKs, or the MultiBaas API directly, to interact with the blockchain.
Provision a backend API key
API keys for backend apps can be provisioned via the MultiBaas web UI. For API keys that need to make changes to MultiBaas, for example to link new contracts or add aliases to addresses, or to sign Cloud Wallet transactions, the Administrators permission should be selected. Since any API key with this level of permission has full control over the MultiBaas deployment, special care must be taken to protect and secure API keys. Administrator API keys should never be publicly exposed.
MultiBaas features for backend apps
MultiBaas combines a number of different features that are useful for building backend apps:
- Interact with smart contract functions
- Compose, sign (see next section), and submit transactions to the blockchain, monitor them , and be notified via a webhook
- Efficiently read and aggregate smart contract events
Sign transactions
Cloud Wallets
Cloud Wallets are the easiest way to sign and submit transactions to the blockchain with MultiBaas. Simply pass the "signAndSubmit": true
parameter along with a smart contract function call that writes to the blockchain, and MultiBaas will have the Cloud Wallet sign the transaction, and automatically submit it to the blockchain for you. This also works with transferring ETH and deploying contracts. MultiBaas will return a signed transaction hash, that can be used to look up the transaction or its receipt via the API, or via the transaction explorer in the MultiBaas web UI.
Cloud Wallet transactions can also be managed with the Transaction Manager (TXM) and can be configured to issue webhooks once included in a block. Cloud Wallets can also be used to sign and submit raw transactions as well as to sign typed or untyped data.
Seed phrases or private keys
Within a backend app, here are any number of ways to store and manage seed phrases and private keys. The following examples will assume the private keys are imported via environment variables and otherwise managed securely.
MultiBaas also offers an API endpoint to submit signed transactions to the blockchain, so that a separate JSON RPC provider is not needed.
TypeScript (JavaScript)
ethers.js is a JS toolkit for managing blockchain interactions. The unsigned transaction from MultiBaas needs to be reformatted slightly for ethers.js. The following sample code will call a smart contract write function, reformat the returned unsigned transaction, and submit it to a connected web3 browser wallet for signing and submission to the blockchain.
Getting an unsigned transaction from the MultiBaas API by calling a smart contract function, signing the transaction, and submitting it to the blockchain
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(
"ethereum",
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("ethereum");
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
};
You can run it as follows:
node index.js -address <0x-your-contract-address> -contract-label <your-contract-label> -method <your-contract-method-name>
Go
The following is a complete command line application where MultiBaas composes a transaction to transfer ERC20 tokens from one address to another, which is then signed by a private key within the Go application, and then submitted back to the blockchain via MultiBaas.
Some setup is required by setting the following environment variables:
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>"
In addition, an ERC20 smart contract must be deployed on the blockchain, and the signing (sending) address must have sufficient token balance.
The program can then be run by specifying a few command line arguments:
go run . -addressOrAlias sampletoken -to 0x1C568f7B1de5fdEe019fD1213160241F4e470e02 -amount 1
Where -addressOrAlias sampletoken
is the address alias where the ERC20 smart contract has been deployed, -to 0x1C568f7B1de5fdEe019fD1213160241F4e470e02
is the recipient address, and -amount 1
is the quantity of ERC20 tokens to send.
If successful, the program will output something like:
Signed transaction hash: 0x969978e258248b1648cdf510de29c787a6346f16b7376cd0bdaf41f796a77d7e
Transaction submitted successfully!
Full sample Go program source code
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 (
chainName = "ethereum" // always "ethereum", for any chain
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, chainName, 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, chainName).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, chainName).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!")
}