Skip to main content
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

networkConfig
NetworkConfig
required
Network-specific configuration for Starknet
manaUrl
string
required
URL of the Mana relayer service for submitting signed transactions
ethUrl
string
required
Ethereum RPC URL for cross-chain operations (L1 storage proofs)
starkProvider
RpcProvider
required
Starknet.js RPC provider for reading blockchain state
whitelistServerUrl
string
required
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);
signer
Account
required
Starknet.js Account instance with signing capabilities
data
Propose
required
Proposal data object
envelope
Envelope<Propose>
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);
data.choice
Choice
required
Vote choice: 1 (For), 2 (Against), or 3 (Abstain)
data.proposal
number
required
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.
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.
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:
  1. Parameters Format: Execution strategy and strategy config params are arrays (string[]) instead of a single encoded string
  2. String Handling: Long strings (like URIs) are automatically split into Cairo short strings
  3. Proposal IDs: Automatically converted to uint256 format for Starknet
  4. 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: 1700000000n,
    minEndTimestamp: 1700086400n,
    maxEndTimestamp: 1700172800n,
    finalizationStatus: 1,
    executionPayloadHash: '0x...',
    executionStrategy: '0x...',
    authorAddressType: 0,
    author: '0x...',
    activeVotingStrategies: 1n
  },
  votesFor: 1000000000000000000n,
  votesAgainst: 0n,
  votesAbstain: 0n,
  executionHash: '0x...',
  transactions: [
    {
      to: '0x...',
      value: 0n,
      data: '0x...',
      operation: 0,
      salt: 0n
    }
  ]
});
executor
string
required
L1 executor contract address
space
string
required
Starknet space address
proposalId
number
required
Proposal ID on Starknet
proposal
object
required
Proposal data including timestamps, execution strategy, and finalization status
votesFor
bigint
required
Total votes in favor
votesAgainst
bigint
required
Total votes against
votesAbstain
bigint
required
Total abstain votes
executionHash
string
required
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
});
contractAddress
string
required
Starknet strategy contract that uses L1 proofs
timestamp
number
required
L1 timestamp to cache on Starknet
binaryTree
object
required
Merkle tree proof data from Herodotus API
nonce
string
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:

Build docs developers (and LLMs) love