The Starknet client provides methods to interact with Snapshot X governance spaces on the Starknet L2 network. It supports both signature-based (gasless) and transaction-based approaches.
Installation
npm install @snapshot-labs/sx
Client Types
The Starknet client comes in two implementations:
StarknetSig : Signature-based client for gasless voting via Mana relayer
StarknetTx : Transaction-based client for direct on-chain interactions
StarknetSig (Signature-based)
The signature-based client enables gasless governance actions through EIP-712 style signed messages on Starknet.
Initialization
import { clients } from '@snapshot-labs/sx' ;
import { RpcProvider } from 'starknet' ;
const client = new clients . StarknetSig ({
networkConfig: {
eip712ChainId: 'SN_MAIN' , // or 'SN_SEPOLIA'
// ... other network config
},
manaUrl: 'https://mana.snapshot.org' ,
ethUrl: 'https://eth.llamarpc.com' ,
starkProvider: new RpcProvider ({
nodeUrl: 'https://starknet-mainnet.public.blastapi.io'
}),
whitelistServerUrl: 'https://whitelist.snapshot.org'
});
Configuration
Network-specific configuration for Starknet Starknet chain identifier (e.g., ‘SN_MAIN’, ‘SN_SEPOLIA’)
URL of the Mana relayer service for submitting signed transactions
Ethereum RPC URL for cross-chain operations (L1 storage proofs)
Starknet.js RPC provider for reading blockchain state
URL of the whitelist server for strategy validation
Methods
propose()
Create a new governance proposal with signature-based authentication.
import { Account } from 'starknet' ;
const account = new Account (
provider ,
accountAddress ,
privateKey
);
const envelope = await client . propose ({
signer: account ,
data: {
space: '0x...' , // Space contract address
authenticator: '0x...' , // Authenticator contract
strategies: [
{
index: 0 ,
address: '0x...' ,
params: '0x' ,
metadata: {}
}
],
executionStrategy: {
addr: '0x...' ,
params: [ '0x' ] // Array of parameters on Starknet
},
metadataUri: 'ipfs://...' // Proposal metadata
}
});
// Send the signed envelope to Mana
const receipt = await client . send ( envelope );
Starknet.js Account instance with signing capabilities
Proposal data object Space contract address (felt format)
Authenticator contract address
Array of proposal validation strategies
Execution strategy with address and parameter array
IPFS URI for proposal metadata (automatically split for Cairo short strings)
Returns an envelope containing the signature data and proposal data
vote()
Cast a vote on a proposal using a signed message.
const envelope = await client . vote ({
signer: account ,
data: {
space: '0x...' ,
authenticator: '0x...' ,
strategies: [
{
index: 0 ,
address: '0x...' ,
params: '0x'
}
],
proposal: 1 , // Proposal ID
choice: 1 , // 1 = For, 2 = Against, 3 = Abstain
metadataUri: '' // Optional vote metadata
}
});
const receipt = await client . send ( envelope );
Vote choice: 1 (For), 2 (Against), or 3 (Abstain)
Proposal ID (converted to uint256 internally)
updateProposal()
Update an existing proposal’s execution strategy or metadata.
const envelope = await client . updateProposal ({
signer: account ,
data: {
space: '0x...' ,
proposal: 1 ,
authenticator: '0x...' ,
executionStrategy: {
addr: '0x...' ,
params: [ '0x' ]
},
metadataUri: 'ipfs://...'
}
});
const receipt = await client . send ( envelope );
send()
Send a signed envelope to the Mana relayer for on-chain execution.
const result = await client . send ( envelope );
console . log ( 'Transaction hash:' , result );
StarknetTx (Transaction-based)
The transaction-based client allows direct on-chain interactions on Starknet, requiring users to pay gas fees.
Initialization
import { clients } from '@snapshot-labs/sx' ;
import { RpcProvider } from 'starknet' ;
const client = new clients . StarknetTx ({
networkConfig: {
eip712ChainId: 'SN_MAIN' ,
spaceFactory: '0x...' ,
masterSpace: '0x...' ,
starknetCore: '0x...' , // L1 StarknetCore contract
l1AvatarExecutionStrategyFactory: '0x...' ,
l1AvatarExecutionStrategyImplementation: '0x...'
},
ethUrl: 'https://eth.llamarpc.com' ,
starkProvider: new RpcProvider ({
nodeUrl: 'https://starknet-mainnet.public.blastapi.io'
}),
whitelistServerUrl: 'https://whitelist.snapshot.org'
});
Space Deployment
deploySpace()
Deploy a new governance space on Starknet.
import { Account } from 'starknet' ;
const account = new Account (
provider ,
accountAddress ,
privateKey
);
const { txId , address } = await client . deploySpace ({
account ,
params: {
controller: account . address ,
votingDelay: 0 ,
minVotingDuration: 3600 , // 1 hour
maxVotingDuration: 604800 , // 1 week
proposalValidationStrategy: {
addr: '0x...' ,
params: [ '0x' ]
},
proposalValidationStrategyMetadataUri: '' ,
daoUri: '' ,
metadataUri: 'ipfs://...' ,
authenticators: [ '0x...' ],
votingStrategies: [
{ addr: '0x...' , params: [ '0x' ] }
],
votingStrategiesMetadata: [ '' ]
},
saltNonce: '0x...' // Optional, random if not provided
});
console . log ( 'Space deployed at:' , address );
console . log ( 'Transaction hash:' , txId );
deployL1AvatarExecution()
Deploy an L1 Avatar execution strategy for cross-chain governance.
Deploy L1 Avatar Execution
import { Wallet } from '@ethersproject/wallet' ;
const l1Signer = new Wallet ( privateKey , ethProvider );
const { txId , address } = await client . deployL1AvatarExecution ({
signer: l1Signer ,
params: {
controller: l1Signer . address ,
target: '0x...' , // L1 Safe address
executionRelayer: '0x...' , // L2 relayer address
spaces: [ '0x...' ], // L2 space addresses
quorum: BigInt ( '1000000000000000000' ) // 1 token
},
salt: '0x...' // Optional
});
console . log ( 'L1 execution strategy deployed at:' , address );
Governance Actions
propose()
Submit a proposal transaction directly to the blockchain.
Submit Proposal Transaction
const tx = await client . propose (
account ,
{
data: {
space: '0x...' ,
authenticator: '0x...' ,
strategies: [ ... ],
executionStrategy: { addr: '0x...' , params: [ '0x' ] },
metadataUri: 'ipfs://...'
}
}
);
await provider . waitForTransaction ( tx . transaction_hash );
vote()
Submit a vote transaction.
const tx = await client . vote (
account ,
{
data: {
space: '0x...' ,
authenticator: '0x...' ,
strategies: [ ... ],
proposal: 1 ,
choice: 1 ,
metadataUri: ''
}
}
);
await provider . waitForTransaction ( tx . transaction_hash );
execute()
Execute a passed proposal.
const tx = await client . execute ({
signer: account ,
space: '0x...' ,
proposalId: 1 ,
executionPayload: [ '0x...' ]
});
await provider . waitForTransaction ( tx . transaction_hash );
cancelProposal()
Cancel a proposal (only by proposal author).
const tx = await client . cancelProposal ({
signer: account ,
space: '0x...' ,
proposal: 1
});
await provider . waitForTransaction ( tx . transaction_hash );
Space Management
updateSettings()
Update space configuration.
const tx = await client . updateSettings ({
signer: account ,
space: '0x...' ,
settings: {
minVotingDuration: 7200 ,
maxVotingDuration: 1209600 ,
metadataUri: 'ipfs://...' ,
votingStrategiesToAdd: [
{ addr: '0x...' , params: [ '0x' ] }
],
authenticatorsToAdd: [ '0x...' ]
}
});
await provider . waitForTransaction ( tx . transaction_hash );
transferOwnership()
Transfer space ownership.
const tx = await client . transferOwnership ({
signer: account ,
space: '0x...' ,
owner: '0x...' // New owner address
});
await provider . waitForTransaction ( tx . transaction_hash );
Helper Methods
predictSpaceAddress()
Predict the address of a space before deployment.
const saltNonce = '0x...' ;
const predictedAddress = await client . predictSpaceAddress ({
account ,
saltNonce
});
console . log ( 'Space will be deployed at:' , predictedAddress );
Type Definitions
StrategyConfig
type StrategyConfig = {
index : number ; // Strategy index in the space
address : string ; // Strategy contract address (felt)
params : string ; // Encoded parameters
metadata ?: Record < string , any >; // Optional metadata
};
AddressConfig (Starknet)
type AddressConfig = {
addr : string ; // Contract address (felt)
params : string []; // Array of parameters (Cairo format)
};
Envelope
type Envelope < T > = {
signatureData ?: SignatureData ;
data : T ;
};
Key Differences from EVM
Starknet clients have some important differences from EVM clients:
Parameters Format : Execution strategy and strategy config params are arrays (string[]) instead of a single encoded string
String Handling : Long strings (like URIs) are automatically split into Cairo short strings
Proposal IDs : Automatically converted to uint256 format for Starknet
Account Model : Uses Starknet’s account abstraction with Account instead of ethers Signer
Cross-Chain Governance
Starknet spaces can execute transactions on Ethereum L1 through L1 Avatar execution strategies:
// 1. Deploy L1 execution strategy (see deployL1AvatarExecution above)
// 2. Create proposal on L2 with L1 execution
const envelope = await starknetSigClient . propose ({
signer: account ,
data: {
space: '0x...' , // L2 space
executionStrategy: {
addr: l1ExecutionAddress , // L1 execution strategy
params: [ /* L1 execution payload */ ]
},
// ... other params
}
});
// 3. After proposal passes, transactions execute on L1
L1Executor
The L1Executor client enables executing Starknet proposals on Ethereum L1 through the L1 Avatar execution strategy.
Installation
import { clients } from '@snapshot-labs/sx' ;
const l1Executor = new clients . L1Executor ();
Execute Proposal on L1
Execute a passed Starknet proposal on Ethereum L1:
import { Wallet } from '@ethersproject/wallet' ;
const signer = new Wallet ( 'YOUR_PRIVATE_KEY' , provider );
await l1Executor . execute ({
signer ,
executor: '0x...' , // L1 executor contract address
space: '0x...' , // Starknet space address
proposalId: 1 ,
proposal: {
startTimestamp: 1700000000 n ,
minEndTimestamp: 1700086400 n ,
maxEndTimestamp: 1700172800 n ,
finalizationStatus: 1 ,
executionPayloadHash: '0x...' ,
executionStrategy: '0x...' ,
authorAddressType: 0 ,
author: '0x...' ,
activeVotingStrategies: 1 n
},
votesFor: 1000000000000000000 n ,
votesAgainst: 0 n ,
votesAbstain: 0 n ,
executionHash: '0x...' ,
transactions: [
{
to: '0x...' ,
value: 0 n ,
data: '0x...' ,
operation: 0 ,
salt: 0 n
}
]
});
L1 executor contract address
Proposal data including timestamps, execution strategy, and finalization status
Hash of the execution payload
transactions
MetaTransaction[]
required
Array of transactions to execute on L1
HerodotusController
The HerodotusController client manages cross-chain state proofs using Herodotus for L1→L2 voting strategies.
Installation
import { clients } from '@snapshot-labs/sx' ;
import { starknetMainnet } from '@snapshot-labs/sx' ;
const herodotus = new clients . HerodotusController ( starknetMainnet );
Cache Timestamp
Cache an L1 timestamp on Starknet for cross-chain proof verification:
import { Account } from 'starknet' ;
const account = new Account ( provider , address , privateKey );
await herodotus . cacheTimestamp ({
signer: account ,
contractAddress: '0x...' , // Strategy contract address
timestamp: 1700000000 ,
binaryTree: {
remapper: {
onchain_remapper_id: 1
},
proofs: [
{
element_index: 0 ,
element_hash: '0x...' ,
siblings_hashes: [ '0x...' ],
peaks_hashes: [ '0x...' ]
}
]
}
}, {
nonce: '1' // Optional
});
Starknet strategy contract that uses L1 proofs
L1 timestamp to cache on Starknet
Merkle tree proof data from Herodotus API
Optional transaction nonce
The HerodotusController is used internally by cross-chain voting strategies that need to verify L1 state on Starknet. You typically don’t need to call it directly unless implementing custom cross-chain strategies.
Source Code
View the complete implementation: