How to sign transactions
After calling a crafting endpoint, the API returns an unsigned Pharos (EVM) 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 Pharos native staking actions supported by the API.
Below are common signing approaches supported by EVM-compatible tooling.
Signing with a private key
Signing with Ethers JS using a Private Key:
Pharos is EVM-compatible, so standard Ethereum tooling can be used for transaction signing.
The crafted transaction payload returned by the API is passed to the signer using library Ethers.js. The signer produces a valid EVM signature, and the resulting signed transaction can then be broadcast to the Pharos network.
This method is typically used in backend services, scripts, or environments where private keys are managed directly by the application.
It involves the use of the Ethers JavaScript library, which allows developers to interact with the Ethereum virtual machine directly from a JavaScript application. To sign a transaction, a developer typically imports the SDK, creates a transaction object, and then uses a private key to sign this object. This signing process is crucial as it verifies the identity of the transaction initiator and ensures the transaction cannot be altered without the initiator's consent.
Complete with your own wallet private key or seed at .env
ETHEREUM_WALLET_PRIVATE_KEY=
ETHEREUM_RPC=
ETHEREUM_WALLET_ADDRESS=
EVM_CHAIN_ID=
const { Common, Chain, Hardfork } = require('@ethereumjs/common')
const Transaction = require('@ethereumjs/tx');
const { RLP } = require('@ethereumjs/rlp');
const ethUtil = require('ethereumjs-util');
const { ethers } = require("ethers");
const path = require('path');
require('dotenv').config({ path: path.join(__dirname, './../.env') })
const accountFrom = {
address: process.env.ETHEREUM_WALLET_ADDRESS,
privateKey: process.env.ETHEREUM_WALLET_PRIVATE_KEY,
rpcEndpoint: process.env.ETHEREUM_RPC,
chainId: process.env.EVM_CHAIN_ID
};
const common = new Common({ chain: accountFrom.chainId })
const signUnsignedTxWithPk = async ({serialized_tx_hex, raw_tx_hex_hash}) => {
const serializedTxBytes = ethers.getBytes(serialized_tx_hex);
const rawTxHexHashWithoutPrefix = raw_tx_hex_hash.substring(2);
const txObject = Transaction.TransactionFactory.fromSerializedData(serializedTxBytes, { common });
const txJson = txObject.toJSON();
let num = BigInt(txJson.nonce);
const nonceNormalized = num === 0n ? num : txJson.nonce;
const params = [nonceNormalized, txJson.gasPrice, txJson.gasLimit, txJson.to, txJson.value, txJson.data];
const rawTransaction = RLP.encode(params);
const privateKeyBuffer = Buffer.from(accountFrom.privateKey, 'hex');
const txHashBytes = Buffer.from(rawTxHexHashWithoutPrefix, 'hex');
const signResult = ethUtil.ecsign(txHashBytes, privateKeyBuffer, Number(accountFrom.chainId));
const rBuff = signResult.r;
const sBuff = signResult.s;
const r = rBuff.toString('hex');
const s = sBuff.toString('hex');
const v = signResult.v;
return { r, s, v};
}
// Get serialized_tx_hex and raw_tx_hex_hash from stake action response (see Pharos action response model in API reference)
const signature = await signUnsignedTxWithPk({serialized_tx_hex, raw_tx_hex_hash})
// With the Signature you need to pass it to the /prepare endpoint
// Check api reference
- Get the
serialized_tx_hexandraw_tx_hex_hashfrom any of the Pharos crafting actions - Pass them to the signer code above
- Once you have the signature, call the prepare endpoint in the Pharos flow:
/api/v1/pharos/native/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 EVM 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 signature (r, s, v), which you then pass to the prepare step.
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 = "ETH";
const rawTxHexHashWithoutPrefix = unsigned_tx_hash_hex.startsWith("0x")
? unsigned_tx_hash_hex.substring(2)
: unsigned_tx_hash_hex;
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": rawTxHexHashWithoutPrefix
}
]
}
}
};
const fbTx = await fireblocks.createTransaction(tx);
const result = await waitForTxCompletion(fbTx);
const signature = result.signedMessages[0].signature;
const v = 27 + signature.v;
const r = '0x'+signature.r;
const s = '0x'+signature.s;
return { r, s, v};
}
// Get raw_tx_hex_hash from stake action response (Pharos action response model)
const signature_hex = await signWithFb(raw_tx_hex_hash);
// With the signature, call /api/v1/pharos/native/action/prepare
// Check api reference
Steps:
- Get
raw_tx_hex_hashfrom any Pharos crafting action response - Pass it to the signer code above
- Once you have the signature, call
/api/v1/pharos/native/action/prepare