This page show show to register any amount of validators to the SSV network.
Prerequisite: This tutorial assumes you already have keystores generated, or will use the code illustrated here to generate them pragmatically.
1. Import
Copy import { SSVSDK , chains } from '@ssv-labs/ssv-sdk'
import { parseEther , createPublicClient , createWalletClient , http } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
To instantiate the SDK, provide a number of parameters:
Public client object created using viem
Wallet object created using viem
You can use these like so to instantiate the SDK and store it an object:
Copy // Setup viem clients
const chain = chains .mainnet // or chains.holesky
const transport = http ()
const publicClient = createPublicClient ({
chain ,
transport ,
})
const account = privateKeyToAccount ( '0x...' )
const walletClient = createWalletClient ({
account ,
chain ,
transport ,
})
// Initialize SDK with viem clients
const sdk = new SSVSDK ({
publicClient ,
walletClient ,
})
Next we can use that keystore to generate our keyshare transaction payload:
Copy const keysharesPayload = await sdk . utils .generateKeyShares ({
keystore : JSON .stringify (keystore) ,
keystore_password : 'test1234' ,
operator_keys : [ "LS0tLS1..." , "LS0tLS2..." , "LS0tLS3..." , "LS0tLS4..." ] ,
operator_ids : [ "5" , "8" , "9" , "10" ] ,
owner_address : "YOUR_OWNER_ADDRESS" ,
nonce : "NONCE_OF_OWNER_ADDRESS" ,
})
Before doing the registration transaction, the specified amount of SSV needs to be approved:
Copy await sdk . contract . token .write
.approve ({
args : {
spender : sdk . core . contractAddresses .setter ,
amount : parseEther ( '10' ) ,
} ,
})
.then ((tx) => tx .wait ())
Then finally the registerValidators
function can be called and return the transaction receipt:
Copy const txn_receipt = await sdk . clusters .registerValidators ({
args : {
keyshares : keysharesPayload ,
depositAmount : parseEther ( '30' )
} ,
}) .then (tx => tx .wait ())
console .log ( "txn_receipt: " , txn_receipt)
Full code example
This example assumes you already have a number of keystore files and they are stored, under whatever is set for KEYSTORE_FILE_DIRECTORY
in the .env file.
Install:
Copy npm install @ssv-labs/ssv-sdk fs path
.env example file for the below script:
Copy PRIVATE_KEY = 0xYOUR_PRIVATE_KEY
OWNER_ADDRESS = 0xA4831B989972605A62141a667578d742927Cbef9
KEYSTORE_PASSWORD = test1234
KEYSTORE_FILE_DIRECTORY = ./validator_keys
DEPOSIT_AMOUNT = 5
MINIMUM_RUNWAY_DAYS = 30
Copy import { SSVSDK , chains } from '@ssv-labs/ssv-sdk'
import { operators } from './mock'
import { parseEther , createPublicClient , createWalletClient , http } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import * as dotenv from 'dotenv' ;
import * as fs from 'fs' ;
import * as path from 'path' ;
dotenv .config ();
async function main () : Promise < void > {
if ( ! process . env . KEYSTORE_FILE_DIRECTORY ||
! process . env . OWNER_ADDRESS ||
! process . env . KEYSTORE_PASSWORD ) {
throw new Error ( 'Required environment variables are not set' );
}
const private_key : `0x ${ string } ` = process . env . PRIVATE_KEY as `0x ${ string } ` ;
// Setup viem clients
const chain = chains .holesky // or chains.mainnet
const transport = http ()
const publicClient = createPublicClient ({
chain ,
transport
})
const account = privateKeyToAccount (private_key as `0x ${ string } ` )
const walletClient = createWalletClient ({
account ,
chain ,
transport ,
})
// Initialize SDK with viem clients
const sdk = new SSVSDK ({
publicClient ,
walletClient ,
})
console .log ( operators .keys)
console .log ( operators . ids .map ((id) => Number (id)))
const directoryPath = process . env . KEYSTORE_FILE_DIRECTORY ;
let keystoresArray : { name : string ; keystore : any }[];
try {
keystoresArray = await loadKeystores (directoryPath);
console .log ( 'Loaded keystores: Keystore Amount: ' , keystoresArray . length );
} catch (error) {
console .error ( 'Failed to load keystores:' , error);
throw error; // Exit if we can't load keystores
}
// Now we know keystoresArray is defined
await sdk . contract . token .write
.approve ({
args : {
spender : sdk . config . contractAddresses .setter ,
amount : parseEther ( '10000' ) ,
} ,
})
.then ((tx) => tx .wait ())
let nonce = Number ( await sdk . api .getOwnerNonce ({ owner : process . env . OWNER_ADDRESS }))
console .log ( "Initial nonce: " , nonce)
const chunkSize = 40 ; // Number of validators per transaction
for ( let i = 0 ; i < keystoresArray . length ; i += chunkSize) {
const chunk = keystoresArray .slice (i , i + chunkSize);
const keystoreValues = chunk .map (item => item .keystore);
const keysharesPayload = await sdk . utils .generateKeyShares ({
keystore : keystoreValues ,
keystore_password : process . env . KEYSTORE_PASSWORD ,
operator_keys : operators .keys ,
operator_ids : operators . ids .map ((id) => Number (id)) ,
owner_address : process . env . OWNER_ADDRESS ,
nonce : nonce ,
})
nonce = nonce + Number ( chunk . length )
console .log ( "New nonce: " , nonce)
// TODO: validate keysharesPayload
let txn_receipt
try {
console .log ( `Processing chunk from index ${ i } to ${ i + chunk . length - 1 } ` );
txn_receipt = await sdk . clusters .registerValidators ({
args : {
keyshares : keysharesPayload ,
depositAmount : parseEther ( '30' )
} ,
}) .then (tx => tx .wait ())
console .log ( "txn_receipt: " , txn_receipt)
} catch (error) {
logErrorToFile (error);
console .log ( "Failed to do register: " , error)
}
}
}
async function loadKeystores (directory : string ) : Promise <{ name : string ; keystore : any }[]> {
const keystoresArray : { name : string ; keystore : any }[] = [];
try {
const files = await fs . promises .readdir (directory);
for ( const file of files) {
if ( file .startsWith ( 'keystore-m' ) && file .endsWith ( '.json' )) {
const filePath = path .join (directory , file);
const fileContent = await fs . promises .readFile (filePath , 'utf-8' );
const jsonContent = JSON .parse (fileContent);
keystoresArray .push ({ name : file , keystore : jsonContent });
}
}
return keystoresArray;
} catch (error) {
console .error ( 'Error loading keystores:' , error);
throw error;
}
}
function logErrorToFile (error : unknown ) : void {
const errorMessage = `Failed to do register: ${ error instanceof Error ? error .message : String (error) } \n` ;
// Log the error to the console
console .log (errorMessage);
// Save the error message to a local file
const filePath = './error-log.txt' ;
fs .appendFile (filePath , errorMessage , (err) => {
if (err) {
console .error ( "Failed to write to file: " , err);
} else {
console .log ( `Error saved to file: ${ filePath } ` );
}
});
}
main ();