How to sign transactions
After calling a crafting endpoint, the API returns an unsigned Solana transaction payload. This payload must be signed before it can be sent 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 Solana staking flows supported by the API, regardless of the specific staking method used.
Below are common signing approaches supported by Solana tooling.
Signing with a private key
Signing with @solana/web3.js
This approach uses the official Solana JavaScript library, @solana/web3.js, to sign transactions locally with a private key.
The crafted transaction returned by the API is deserialized into a Solana Transaction object and signed using the appropriate signer. The resulting signed transaction can then be sent to the Solana 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
SOL_WALLET_PRIVATE_KEY=
const web3 = require('@solana/web3.js')
const bs58 = require('bs58')
const accountFrom = {
privateKey: process.env.COSMOS_WALLET_PRIVATE_KEY,
};
const getRawTransaction = async(
encodedTransaction
) =>{
let recoveredTransaction;
try {
recoveredTransaction = web3.Transaction.from(
Buffer.from(encodedTransaction, 'hex')
);
} catch (error) {
recoveredTransaction = web3.VersionedTransaction.deserialize(
Buffer.from(encodedTransaction, 'hex')
);
}
return recoveredTransaction;
}
const signUnsignedTxWithPk = async ({unsigned_tx_hex}) => {
const recoveredTransaction = await getRawTransaction(unsigned_tx_hex);
let signatures;
let transactionBuffer = recoveredTransaction.serializeMessage();
const decodedPk = bs58.decode(accountFrom.privateKey);
const keypair = web3.Keypair.fromSecretKey(decodedPk);
if (recoveredTransaction instanceof web3.VersionedTransaction) {
recoveredTransaction.sign([keypair]);
} else {
recoveredTransaction.partialSign(keypair);
}
// recoveredTransaction.partialSign(nonceAccount);
signatures = [Buffer.from(recoveredTransaction.signature).toString("hex")];
return signatures;
}
// Get unsigned_tx_hex from stake action response (reference at doc models SolanaStakeActionResponseDTO)
const signature_hex = await signUnsignedTxWithPk({unsigned_tx_hex })
// With the Signature you need to pass it to the /prepare endpoint
// Check api reference
- Get the
unsigned_tx_hexfrom any of the Stake actionsaction/create-nonce-account,action/stake,action/unstake,action/withdraw - Pass it to the signer code explaind below
- 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 Solana 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 web3 = require('@solana/web3.js')
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 getRawTransaction = async(
encodedTransaction
) =>{
let recoveredTransaction;
try {
recoveredTransaction = web3.Transaction.from(
Buffer.from(encodedTransaction, 'hex')
);
} catch (error) {
recoveredTransaction = web3.VersionedTransaction.deserialize(
Buffer.from(encodedTransaction, 'hex')
);
}
return recoveredTransaction;
}
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 (transactionBuffer) => {
const assetId = "SOL";
// const assetId = "SOL_TEST"; // For Fireblocks testnet
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": transactionBuffer.toString("hex")
}
]
}
}
};
const fbTx = await fireblocks.createTransaction(tx);
const result = await waitForTxCompletion(fbTx);
let signatures = [];
result.signedMessages?.forEach((signedMessage) => {
if (signedMessage.derivationPath[3] == 0) {
signatures.push(signedMessage.signature.fullSig);
}
});
return signatures;
}
// Get unsigned_tx_hash_hex from stake action response (reference at doc models StakeActionResponseDTO)
const transactionBuffer = await getRawTransaction(unsigned_tx_hash_hex);
const signature_hex = await signWithFb(transactionBuffer);
// With the Signature you need to pass it to the /prepare endpoint
// Check api reference
Steps:
- Get the
unsigned_tx_hash_hexfrom any of the Stake actionsaction/stake,action/unstake,action/claim-rewards - Pass it to the signer code explaind below
- Once you have the signature proceed calling
action/prepare