How to sign transactions
After calling a crafting endpoint, the API returns an unsigned Sui 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 Sui staking flows supported by the API.
Signing with a private key
Signing with Mysten Sui SDK
This approach uses the official Mysten Sui SDK to sign transactions locally with an Ed25519 private key.
The crafted transaction returned by the API can be decoded and signed with Ed25519Keypair. The resulting base64 signature can then be sent to the prepare endpoint.
Complete with your own private key at .env:
SUI_PRIVATE_KEY=
const { fromBase64 } = require('@mysten/sui/utils');
const { Ed25519Keypair } = require('@mysten/sui/keypairs/ed25519');
const signWithPrivateKey = async (unsigned_tx_b64, privateKeyHex) => {
if (!privateKeyHex) {
throw new Error('SUI_PRIVATE_KEY is required for non-Fireblocks signing');
}
const txBytes = fromBase64(unsigned_tx_b64);
const cleanHex = privateKeyHex.startsWith('0x') ? privateKeyHex.slice(2) : privateKeyHex;
const secretKeyBytes = Uint8Array.from(Buffer.from(cleanHex, 'hex'));
const keypair = Ed25519Keypair.fromSecretKey(secretKeyBytes);
const { signature } = await keypair.signTransaction(txBytes);
return { signature };
};
// Get unsigned_tx_b64 from stake/unstake action response
// const { signature } = await signWithPrivateKey(unsigned_tx_b64, process.env.SUI_PRIVATE_KEY);
// Then call action/prepare with { unsigned_tx_b64, signatures: [signature] }
Steps:
- Get
unsigned_tx_b64from a stake or unstake action response. - Pass it to the signer code with your
SUI_PRIVATE_KEY. - Call
action/preparewith the signature.
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 can be used to sign Sui transactions generated by the Stakely Staking API.
Complete with your Fireblocks credentials at .env:
FIREBLOCKS_API_SECRET=
FIREBLOCKS_API_KEY=
FIREBLOCKS_BASE_URL=
FIREBLOCKS_VAULT_ID=
SUI_FIREBLOCKS_ASSET_ID=
const { fromBase64, toBase64 } = require('@mysten/sui/utils');
const { FireblocksSDK, TransactionOperation, PeerType, TransactionStatus } = require('fireblocks-sdk');
const fireblocks = new FireblocksSDK(
process.env.FIREBLOCKS_API_SECRET,
process.env.FIREBLOCKS_API_KEY,
process.env.FIREBLOCKS_BASE_URL,
);
const waitForTxCompletion = async (fbTx) => {
let tx = fbTx;
while (tx.status !== TransactionStatus.COMPLETED) {
if (
tx.status === TransactionStatus.BLOCKED ||
tx.status === TransactionStatus.FAILED ||
tx.status === TransactionStatus.CANCELLED
) {
throw new Error(`Fireblocks signer: transaction ${tx.status}`);
}
tx = await fireblocks.getTransactionById(fbTx.id);
}
return fireblocks.getTransactionById(fbTx.id);
};
const signWithFb = async (txBytesB64, assetId, vaultAccountId = '1') => {
if (!fireblocks) {
throw new Error('Fireblocks SDK is not configured');
}
const txBytes = fromBase64(txBytesB64);
const contentHex = Buffer.from(txBytes).toString('hex');
const tx = {
assetId,
operation: TransactionOperation.RAW,
source: {
type: PeerType.VAULT_ACCOUNT,
id: vaultAccountId,
},
note: 'Sign Sui transaction from stakely staking api',
extraParameters: {
rawMessageData: {
messages: [
{
content: contentHex,
},
],
},
},
};
const fbTx = await fireblocks.createTransaction(tx);
const result = await waitForTxCompletion(fbTx);
const signedMessage = result.signedMessages && result.signedMessages[0];
if (!signedMessage || !signedMessage.signature || !signedMessage.signature.fullSig) {
throw new Error('Fireblocks did not return a usable signature for Sui');
}
// Fireblocks returns raw Ed25519 signature and public key data.
// Sui expects base64(flag || sig || pubkey). For now, assume fullSig is already
// the concatenation of signature || pubkey as hex and wrap with the Ed25519 flag (0x00).
const fullSigHex = signedMessage.signature.fullSig;
const fullSigBytes = Buffer.from(fullSigHex, 'hex');
const suiSigWithFlag = Buffer.concat([Buffer.from([0]), fullSigBytes]);
const suiSignatureB64 = toBase64(suiSigWithFlag);
return suiSignatureB64;
};
Steps:
- Get
unsigned_tx_b64from a stake or unstake action response. - Call
signWithFb(unsigned_tx_b64, SUI_FIREBLOCKS_ASSET_ID, FIREBLOCKS_VAULT_ID)to retrieve the signature. - Call
action/preparewith the returned signature string.