Skip to main content

How to sign transactions

After calling a crafting endpoint, the API returns an unsigned Cosmos transaction payload. This payload must be signed before it can be broadcast to the network.

Signing is fully handled by your application. The Stakely Staking API never has access to private keys.

The signing concepts and examples described in this section apply to all Cosmos Hub staking flows supported by the API, regardless of the specific staking method used.

Below are common signing approaches supported by Cosmos-based tooling.

Signing with a private key

Signing with Cosmos SDK JS

This approach uses the Cosmos SDK JavaScript libraries to sign transactions locally with a private key.

The crafted transaction payload returned by the API is passed to the SDK signer, which produces a valid Cosmos signature. The resulting signed transaction can then be broadcast to the network.

This method is typically used in backend services, scripts, or environments where private keys are managed directly by the application.

Complete with your own wallet private key or seed at .env

COSMOS_WALLET_SEED=
COSMOS_WALLET_PRIVATE_KEY=
COSMOS_RPC=
COSMOS_WALLET_ADDRESS=
const { fromHex,toHex, bech32 } = require("@cosmjs/encoding");
const { TxRaw, SignDoc } = require("cosmjs-types/cosmos/tx/v1beta1/tx");
const { DirectSecp256k1HdWallet, DirectSecp256k1Wallet, makeSignDoc, Registry, decodeTxRaw } = require('@cosmjs/proto-signing')
const { decodeSignature,pubkeyToAddress, pubkeyToRawAddress } = require("@cosmjs/amino");

const accountFrom = {
seed: process.env.COSMOS_WALLET_SEED,
privateKey: process.env.COSMOS_WALLET_PRIVATE_KEY,
rpcEndpoint: process.env.COSMOS_RPC,
address: process.env.COSMOS_WALLET_ADDRESS};

const signUnsignedTxWithPk = async ({unsigned_tx_hex, tx_auth_info_hex}) => {
const unsignedTxBytes = fromHex(unsigned_tx_hex);

const authInfoBytes = fromHex(tx_auth_info_hex);

// Deserialize the transaction bytes to a TxRaw object
const unsignedTx = TxRaw.decode(unsignedTxBytes);
const toJson = TxRaw
let myWallet;
const mmemonic = accountFrom.seed ? true : false;

if (mmemonic) {
myWallet = await DirectSecp256k1HdWallet.fromMnemonic(accountFrom.seed, { prefix })
} else {
myWallet = await DirectSecp256k1Wallet.fromKey(accountFrom.privateKey, prefix);
}
// Create a SigningStargateClient
const client = await SigningStargateClient.connectWithSigner(accountFrom.rpcEndpoint, myWallet);
// ###############
const [firstAccount] = await myWallet.getAccounts();
firstAccount


const { accountNumber, sequence } = await client.getSequence(firstAccount.address);

const chainId = await client.getChainId();

const signDoc = makeSignDoc(unsignedTx.bodyBytes, authInfoBytes, chainId, accountNumber);

const { signed, signature } = await myWallet.signDirect(firstAccount.address, signDoc);

const decodedSignature = decodeSignature(signature)

const signature_hex= toHex(decodedSignature.signature)

const txBodyHex = toHex(signed.bodyBytes)

return { signature_hex, tx_body_hex };
}
// Get unsigned_tx_hex and tx_auth_info_hex from stake action response (reference at doc models StakeActionResponseDTO)

const { signature_hex, tx_body_hex } = await signUnsignedTxWithPk({unsigned_tx_hex, tx_auth_info_hex })
// You need to pass both values (signature_hex, tx_body_hex) + tx_auth_info_hex (obtained from any of the actions endpoints) to the prepare endpoint
// Check api reference

  1. Get the unsigned_tx_hex and tx_auth_info_hex from any of the Stake actions action/stake, action/unstake, action/claim-rewards
  2. Pass it to the signer code explaind below
  3. Once you have the signature proceed calling action/prepare

Signing using external custodians or MPC wallets

Transactions can also be signed using external custodians or MPC-based wallets. In this setup, private keys are never exposed to the application and signing is delegated to the external signing service.

Fireblocks

Fireblocks is an example of a third-party custodian that can be used to sign Cosmos transactions generated by the Stakely Staking API.

The crafted transaction payload is submitted to Fireblocks through its API. Fireblocks performs the signing operation within its secure environment and returns a signed transaction, which can then be broadcast to the blockchain network.

The following code example demonstrates this flow using the Fireblocks JavaScript/TypeScript SDK.

Complete with your own Fireblocks credentials at .env

FIREBLOCKS_API_SECRET=
FIREBLOCKS_API_KEY=
FIREBLOCKS_BASE_URL=
FIREBLOCKS_VAULT_ID=

const { FireblocksSDK, TransactionOperation, PeerType, TransactionStatus } = require("fireblocks-sdk")

const apiSecret = process.env.FIREBLOCKS_API_SECRET;
const apiKey = process.env.FIREBLOCKS_API_KEY;
const baseUrl = process.env.FIREBLOCKS_BASE_URL;
const vaultId = process.env.FIREBLOCKS_VAULT_ID;

const fireblocks = new FireblocksSDK(apiSecret, apiKey, baseUrl);

const waitForTxCompletion = async (fbTx) => {
try {
let tx = fbTx;
while (tx.status != TransactionStatus.COMPLETED) {
if (
tx.status == TransactionStatus.BLOCKED ||
tx.status == TransactionStatus.FAILED ||
tx.status == TransactionStatus.CANCELLED
) {
throw Error(`Fireblocks signer: the transaction has been ${tx.status}`);
} else if (tx.status == TransactionStatus.REJECTED) {
throw Error(
`Fireblocks signer: the transaction has been rejected, make sure that the TAP security policy is not blocking the transaction`,
);
}
tx = await fireblocks.getTransactionById(fbTx.id);
}

return await fireblocks.getTransactionById(fbTx.id);
} catch (err) {
throw new Error("Fireblocks signer (waitForTxCompletion): " + err);
}
}

const signWithFb = async (unsigned_tx_hash_hex) => {
const assetId = "ATOM";

const tx = {
assetId: assetId,
operation: TransactionOperation.RAW,
source: {
type: PeerType.VAULT_ACCOUNT,
id: vaultId
},
note: "Sign transaction from stakely staking api",
"extraParameters": {
"rawMessageData": {
"messages": [
{
"content": unsigned_tx_hash_hex
}
]
}
}
};
const fbTx = await fireblocks.createTransaction(tx);
const result = await waitForTxCompletion(fbTx);
const signature = result.signedMessages[0].signature;
return signature.fullSig;
}

// Get unsigned_tx_hash_hex from stake action response (reference at doc models StakeActionResponseDTO)

const signature_hex = await signWithFb(unsigned_tx_hash_hex);

// With the Signature you need to pass it to the /prepare endpoint
// Check api reference

Steps:

  1. Get the unsigned_tx_hash_hex from any of the Stake actions action/stake, action/unstake, action/claim-rewards
  2. Pass it to the signer code explaind below
  3. Once you have the signature proceed calling action/prepare