Sei Giga Developer Guide
Introduction
Building on Sei Giga opens up possibilities for creating applications that were previously impossible on traditional blockchains. This guide provides you a sneek peek to start developing high-performance dApps that can handle millions of users.
Key advantages for developers:
- Familiar Tools: Full EVM compatibility means you can use existing Ethereum tooling
- Superior Performance: Build apps that feel like Web2 with Web3 guarantees
- Lower Costs: Efficient architecture means cheaper transactions for users
- Advanced Features: Access to unique precompiles and parallel execution
- Future-Proof: Designed for the next generation of blockchain applications
Prerequisites
- Familiarity with Solidity or Vyper
- Basic understanding of EVM and smart contracts
- Node.js 16+ and npm/yarn installed
- Git for version control
Development Environment Setup
Quick Start
# Install Sei CLI
npm install -g @sei/cli
# Create a new project
sei create my-dapp
cd my-dapp
# Install dependencies
npm install
# Start local development node
sei node start
# Deploy your first contract
sei deploy contracts/HelloSei.sol
Quick Setup
# Add Sei network to your Hardhat config
npx hardhat init
// hardhat.config.js - Essential configuration only
module.exports = {
networks: {
seiTestnet: {
url: 'https://evm-rpc-testnet.sei-apis.com/',
chainId: 1328,
accounts: [process.env.PRIVATE_KEY]
}
},
solidity: '0.8.20'
};
Smart Contract Development
Writing High-Performance Contracts
Sei Giga’s parallel execution engine rewards well-designed contracts. Follow these patterns for optimal performance:
Pattern 1: User-Isolated State
// GOOD: Each user has isolated state
contract OptimizedToken {
mapping(address => uint256) private balances;
mapping(address => mapping(address => uint256)) private allowances;
function transfer(address to, uint256 amount) public {
// Only touches sender and recipient state
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
balances[to] += amount;
}
}
// BAD: Global state creates conflicts
contract PoorToken {
uint256 public totalTransfers; // Global counter
function transfer(address to, uint256 amount) public {
totalTransfers++; // Every transfer conflicts!
// ... transfer logic
}
}
Pattern 2: Optimistic Updates
contract OptimisticDEX {
struct Pool {
uint256 reserve0;
uint256 reserve1;
uint256 k; // Constant product
uint256 lastUpdate;
}
mapping(bytes32 => Pool) public pools;
mapping(address => uint256) public userNonces;
event Swap(address indexed user, bytes32 indexed poolId, uint256 amountIn, uint256 amountOut);
event LiquidityAdded(address indexed user, bytes32 indexed poolId, uint256 amount0, uint256 amount1);
function swap(
bytes32 poolId,
uint256 amountIn,
uint256 minAmountOut,
bool zeroForOne
) external {
Pool storage pool = pools[poolId];
require(pool.k > 0, "Pool does not exist");
uint256 amountOut;
if (zeroForOne) {
// Swap token0 for token1
amountOut = (pool.reserve1 * amountIn) / (pool.reserve0 + amountIn);
require(amountOut >= minAmountOut, "Insufficient output");
pool.reserve0 += amountIn;
pool.reserve1 -= amountOut;
} else {
// Swap token1 for token0
amountOut = (pool.reserve0 * amountIn) / (pool.reserve1 + amountIn);
require(amountOut >= minAmountOut, "Insufficient output");
pool.reserve1 += amountIn;
pool.reserve0 -= amountOut;
}
// Update constant product
pool.k = pool.reserve0 * pool.reserve1;
pool.lastUpdate = block.timestamp;
// Increment user nonce for parallel execution
userNonces[msg.sender]++;
emit Swap(msg.sender, poolId, amountIn, amountOut);
}
function addLiquidity(
bytes32 poolId,
uint256 amount0,
uint256 amount1
) external {
Pool storage pool = pools[poolId];
if (pool.k == 0) {
// Initialize pool
pool.reserve0 = amount0;
pool.reserve1 = amount1;
} else {
// Add proportional liquidity
require(
amount0 * pool.reserve1 == amount1 * pool.reserve0,
"Liquidity ratio mismatch"
);
pool.reserve0 += amount0;
pool.reserve1 += amount1;
}
pool.k = pool.reserve0 * pool.reserve1;
pool.lastUpdate = block.timestamp;
emit LiquidityAdded(msg.sender, poolId, amount0, amount1);
}
function getAmountOut(
uint256 amountIn,
uint256 reserveIn,
uint256 reserveOut
) public pure returns (uint256 amountOut) {
require(amountIn > 0, "Insufficient input amount");
require(reserveIn > 0 && reserveOut > 0, "Insufficient liquidity");
uint256 numerator = reserveOut * amountIn;
uint256 denominator = reserveIn + amountIn;
amountOut = numerator / denominator;
}
function getPoolInfo(bytes32 poolId) external view returns (Pool memory) {
return pools[poolId];
}
function getUserNonce(address user) external view returns (uint256) {
return userNonces[user];
}
}
Pattern 3: Batch Operations
contract BatchProcessor {
mapping(address => uint256) public balances;
event BatchTransfer(address indexed sender, uint256 totalAmount, uint256 recipientCount);
event Transfer(address indexed from, address indexed to, uint256 amount);
// Process multiple operations in one transaction
function batchTransfer(
address[] calldata recipients,
uint256[] calldata amounts
) external {
require(recipients.length == amounts.length, "Length mismatch");
require(recipients.length > 0, "Empty batch");
require(recipients.length <= 100, "Batch too large");
uint256 totalAmount = 0;
// Calculate total amount needed
for (uint256 i = 0; i < amounts.length; i++) {
require(amounts[i] > 0, "Invalid amount");
require(recipients[i] != address(0), "Invalid recipient");
totalAmount += amounts[i];
}
// Single balance check
require(balances[msg.sender] >= totalAmount, "Insufficient balance");
balances[msg.sender] -= totalAmount;
// Parallel-friendly distribution
for (uint256 i = 0; i < recipients.length; i++) {
balances[recipients[i]] += amounts[i];
emit Transfer(msg.sender, recipients[i], amounts[i]);
}
emit BatchTransfer(msg.sender, totalAmount, recipients.length);
}
function batchCall(
address[] calldata targets,
bytes[] calldata data
) external returns (bytes[] memory results) {
require(targets.length == data.length, "Length mismatch");
require(targets.length > 0, "Empty batch");
require(targets.length <= 50, "Batch too large");
results = new bytes[](targets.length);
for (uint256 i = 0; i < targets.length; i++) {
require(targets[i] != address(0), "Invalid target");
(bool success, bytes memory result) = targets[i].call(data[i]);
require(success, "Call failed");
results[i] = result;
}
}
function deposit() external payable {
require(msg.value > 0, "Must deposit positive amount");
balances[msg.sender] += msg.value;
}
function withdraw(uint256 amount) external {
require(amount > 0, "Must withdraw positive amount");
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
}
Gas Optimization Techniques
Sei Giga Gas Tips:
-
Storage operations are expensive
-
minimize writes - Use
memory
for temporary data -
Pack struct variables efficiently
-
Use events for data not needed on-chain - Leverage view functions for reads
contract GasOptimized {
// Pack structs to use fewer storage slots
struct User {
uint128 balance; // Slot 1 (16 bytes)
uint64 lastUpdate; // Slot 1 (8 bytes) - same slot!
uint64 nonce; // Slot 1 (8 bytes) - still same slot!
address referrer; // Slot 2 (20 bytes)
bool isActive; // Slot 2 (1 byte) - same slot as referrer!
}
mapping(address => User) public users;
mapping(address => uint256) public balances;
// Use constants for values that don't change
uint256 private constant PRECISION = 1e18;
uint256 private constant MAX_BATCH_SIZE = 100;
event UserUpdated(address indexed user, uint128 balance, uint64 nonce);
// Cache array lengths in loops
function sumBalances(address[] memory userAddresses) public view returns (uint256 total) {
uint256 length = userAddresses.length; // Cache length
for (uint256 i = 0; i < length; ) {
total += balances[userAddresses[i]];
unchecked { i++; } // Save gas on overflow check
}
}
// Batch operations for gas efficiency
function batchUpdateUsers(
address[] calldata userAddresses,
uint128[] calldata newBalances
) external {
require(userAddresses.length == newBalances.length, "Length mismatch");
require(userAddresses.length <= MAX_BATCH_SIZE, "Batch too large");
uint256 length = userAddresses.length;
for (uint256 i = 0; i < length; ) {
User storage user = users[userAddresses[i]];
user.balance = newBalances[i];
user.lastUpdate = uint64(block.timestamp);
user.nonce++;
emit UserUpdated(userAddresses[i], newBalances[i], user.nonce);
unchecked { i++; }
}
}
// Use memory for temporary calculations
function calculateRewards(address userAddress) external view returns (uint256 reward) {
User memory user = users[userAddress]; // Load to memory once
if (!user.isActive) {
return 0;
}
uint256 timeDiff = block.timestamp - user.lastUpdate;
reward = (uint256(user.balance) * timeDiff * PRECISION) / (365 days * 100); // 1% APY
}
// Efficient storage packing demonstration
function updateUser(
address userAddress,
uint128 newBalance,
address newReferrer,
bool active
) external {
User storage user = users[userAddress];
// Single storage write updates multiple fields
user.balance = newBalance;
user.lastUpdate = uint64(block.timestamp);
user.nonce++;
user.referrer = newReferrer;
user.isActive = active;
emit UserUpdated(userAddress, newBalance, user.nonce);
}
}
Sei-Specific Features
Native Precompiles
Sei Giga includes powerful precompiles for enhanced functionality:
// Oracle Precompile interface
interface IOracle {
struct OracleExchangeRate {
string exchangeRate;
string lastUpdate;
int64 lastUpdateTimestamp;
}
struct DenomOracleExchangeRatePair {
string denom;
OracleExchangeRate oracleExchangeRateVal;
}
struct OracleTwap {
string denom;
IOracle constant oracle = IOracle(0x0000000000000000000000000000000000001008);
mapping(address => uint256) public balances;
uint256 constant PRICE_STALENESS_THRESHOLD = 60; // 60 seconds
event PriceUpdate(string denom, string price, int64 timestamp);
modifier withFreshPrice(string memory denom) {
IOracle.DenomOracleExchangeRatePair[] memory rates = oracle.getExchangeRates();
bool found = false;
for (uint256 i = 0; i < rates.length; i++) {
if (keccak256(bytes(rates[i].denom)) == keccak256(bytes(denom))) {
uint256 age = block.timestamp - uint256(uint64(rates[i].oracleExchangeRateVal.lastUpdateTimestamp));
require(age < PRICE_STALENESS_THRESHOLD, "Price too stale");
emit PriceUpdate(
rates[i].denom,
rates[i].oracleExchangeRateVal.exchangeRate,
rates[i].oracleExchangeRateVal.lastUpdateTimestamp
);
found = true;
break;
}
}
require(found, "Price not found");
_;
}
function priceAwareTransfer(
string memory denom,
address to,
uint256 amount
) external withFreshPrice(denom) {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
balances[to] += amount;
}
function isPriceStale(string memory denom, uint256 maxAge) public view returns (bool) {
(, int64 timestamp) = getCurrentPrice(denom);
return block.timestamp - uint256(uint64(timestamp)) > maxAge;
}
}
Parallel Execution Awareness
Design contracts that maximize parallel execution:
contract ParallelFriendlyNFT {
// Each token ID is independent
mapping(uint256 => address) private owners;
mapping(uint256 => string) private tokenURIs;
// Minting doesn't conflict between users
function mint(uint256 tokenId, string memory uri) external {
require(owners[tokenId] == address(0), "Already minted");
owners[tokenId] = msg.sender;
tokenURIs[tokenId] = uri;
emit Transfer(address(0), msg.sender, tokenId);
}
// Transfers only affect specific tokens
function transfer(address to, uint256 tokenId) external {
require(owners[tokenId] == msg.sender, "Not owner");
owners[tokenId] = to;
emit Transfer(msg.sender, to, tokenId);
}
}
Frontend Integration
Web3 Setup
// lib/web3.ts
import { createPublicClient, createWalletClient, custom, http } from 'viem';
import { seiGiga } from './chains';
export const publicClient = createPublicClient({
chain: seiGiga,
transport: http('https://evm-rpc.sei-apis.com')
});
export const walletClient = createWalletClient({
chain: seiGiga,
transport: custom(window.ethereum)
});
// Custom chain configuration
export const seiGiga = {
id: 1329,
name: 'Sei Giga',
network: 'sei-giga',
nativeCurrency: {
decimals: 18,
name: 'Sei',
symbol: 'SEI'
},
rpcUrls: {
public: { http: ['https://evm-rpc.sei-apis.com'] },
default: { http: ['https://evm-rpc.sei-apis.com'] }
},
blockExplorers: {
default: { name: 'SeiScan', url: 'https://seiscan.io' }
}
};
React Hook Examples
// hooks/useContract.ts
import { useContractRead, useContractWrite, usePrepareContractWrite } from 'wagmi';
import { parseEther } from 'viem';
import TokenABI from '../abi/Token.json';
export function useTokenBalance(address: `0x${string}`) {
return useContractRead({
address: TOKEN_ADDRESS,
abi: TokenABI,
functionName: 'balanceOf',
args: [address],
watch: true // Auto-refresh on new blocks
});
}
export function useTokenTransfer() {
const [recipient, setRecipient] = useState('');
const [amount, setAmount] = useState('');
const { config } = usePrepareContractWrite({
address: TOKEN_ADDRESS,
abi: TokenABI,
functionName: 'transfer',
args: [recipient as `0x${string}`, parseEther(amount || '0')],
enabled: Boolean(recipient && amount)
});
const { write, isLoading, isSuccess } = useContractWrite(config);
return {
transfer: write,
isLoading,
isSuccess,
setRecipient,
setAmount
};
}
Optimistic UI Updates
// Optimistically update UI before transaction confirms
function TransferButton({ recipient, amount }: TransferProps) {
const queryClient = useQueryClient();
const { transfer } = useTokenTransfer();
const handleTransfer = async () => {
// Optimistic update
queryClient.setQueryData(['balance', account], (old: bigint) =>
old - parseEther(amount)
);
try {
await transfer?.();
} catch (error) {
// Revert on error
queryClient.invalidateQueries(['balance', account]);
}
};
return (
<button onClick={handleTransfer}>
Transfer {amount} SEI
</button>
);
}
Testing & Debugging
Unit Testing
// test/Token.test.js
const { expect } = require('chai');
const { ethers } = require('hardhat');
describe('Token', function () {
let token;
let owner, addr1, addr2;
beforeEach(async function () {
[owner, addr1, addr2] = await ethers.getSigners();
const Token = await ethers.getContractFactory('Token');
token = await Token.deploy('SeiToken', 'SEI', ethers.parseEther('1000000'));
await token.waitForDeployment();
});
describe('Parallel Execution', function () {
it('Should handle concurrent transfers', async function () {
// Setup
await token.transfer(addr1.address, ethers.parseEther('1000'));
await token.transfer(addr2.address, ethers.parseEther('1000'));
// Execute transfers in parallel
const tx1 = token.connect(addr1).transfer(owner.address, ethers.parseEther('100'));
const tx2 = token.connect(addr2).transfer(owner.address, ethers.parseEther('200'));
// Both should succeed
await expect(tx1).to.not.be.reverted;
await expect(tx2).to.not.be.reverted;
// Verify final state
expect(await token.balanceOf(owner.address)).to.equal(ethers.parseEther('998300'));
});
});
});
Performance Testing
// scripts/performance-test.js
async function testThroughput() {
const Token = await ethers.getContractFactory('Token');
const token = await Token.deploy('TestToken', 'TEST', ethers.parseEther('1000000'));
const signers = await ethers.getSigners();
const batchSize = 100;
const iterations = 10;
console.log('Starting throughput test...');
const startTime = Date.now();
for (let i = 0; i < iterations; i++) {
const promises = [];
// Create batch of transactions
for (let j = 0; j < batchSize; j++) {
const signer = signers[j % signers.length];
const tx = token.connect(signer).transfer(signers[(j + 1) % signers.length].address, ethers.parseEther('1'));
promises.push(tx);
}
// Execute in parallel
await Promise.all(promises);
}
const endTime = Date.now();
const totalTx = batchSize * iterations;
const duration = (endTime - startTime) / 1000;
const tps = totalTx / duration;
console.log(`Processed ${totalTx} transactions in ${duration}s`);
console.log(`Throughput: ${tps.toFixed(2)} TPS`);
}
Debugging Tools
Common Issues & Solutions:
- Transaction Reverts: Use
hardhat console.log
in contracts - Gas Estimation Errors: Increase gas limit by 20%
- Nonce Issues: Use
eth_getTransactionCount
for accurate nonces - RPC Timeouts: Implement retry logic with exponential backoff
Deployment & DevOps
Production Deployment Checklist
# 1. Security audit
npm run audit
# 2. Gas optimization report
npm run gas-report
# 3. Test coverage
npm run coverage
# 4. Deploy to testnet
npm run deploy:testnet
# 5. Verify contracts
npm run verify:testnet
# 6. Integration tests
npm run test:integration
# 7. Deploy to mainnet
npm run deploy:mainnet
# 8. Verify on mainnet
npm run verify:mainnet
Contract Verification
// scripts/verify.js
async function main() {
await hre.run('verify:verify', {
address: CONTRACT_ADDRESS,
constructorArguments: ['SeiToken', 'SEI', ethers.parseEther('1000000')]
});
}
Monitoring & Analytics
// Monitor contract events
import { publicClient } from './web3';
publicClient.watchContractEvent({
address: TOKEN_ADDRESS,
abi: TokenABI,
eventName: 'Transfer',
onLogs: (logs) => {
logs.forEach((log) => {
console.log('Transfer:', {
from: log.args.from,
to: log.args.to,
value: formatEther(log.args.value)
});
// Send to analytics
analytics.track('token_transfer', {
from: log.args.from,
to: log.args.to,
value: log.args.value.toString(),
blockNumber: log.blockNumber
});
});
}
});
Best Practices Summary
Architecture Guidelines
- State Isolation: Design contracts where operations on different entities don’t conflict
- Batch Operations: Group multiple operations to amortize gas costs
- Event-Driven: Use events for off-chain data and indexing
- Upgrade Patterns: Implement proxy patterns for upgradability
- Access Control: Use role-based permissions for admin functions
Security Considerations
contract SecureContract {
// Use OpenZeppelin libraries
using SafeERC20 for IERC20;
// Implement reentrancy guards
uint256 private _status = 1;
modifier nonReentrant() {
require(_status == 1, "Reentrant call");
_status = 2;
_;
_status = 1;
}
// Validate inputs
modifier validAddress(address addr) {
require(addr != address(0), "Invalid address");
_;
}
// Use checks-effects-interactions pattern
function withdraw(uint256 amount) external nonReentrant {
// Checks
require(balances[msg.sender] >= amount, "Insufficient balance");
// Effects
balances[msg.sender] -= amount;
// Interactions
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
}
Advanced Development Patterns
Designing for Parallel Execution
Consider a typical DeFi scenario with multiple users:
// Traditional sequential processing:
// User A swaps USDC for ETH (100ms)
// User B stakes SEI (100ms)
// User C mints NFT (100ms)
// Total time: 300ms
// Sei's parallel processing:
// All three execute simultaneously
// Total time: 100ms
The key: transactions touching different state execute in parallel.
State Isolation Pattern
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract ParallelFriendlyDEX {
// GOOD: State isolated by user
mapping(address => uint256) private balances;
mapping(address => mapping(address => uint256)) private allowances;
// Each user's operations are independent
function deposit() external payable {
balances[msg.sender] += msg.value;
}
// BAD: Global state creates conflicts
uint256 public totalDeposits; // Every deposit must update this
function depositWithGlobalCounter() external payable {
balances[msg.sender] += msg.value;
totalDeposits += msg.value; // Forces sequential execution
}
}
Gas Optimization Techniques
Struct Packing
contract OptimizedStorage {
// BAD: Uses 3 storage slots
struct Inefficient {
uint256 amount; // 256 bits = 1 slot
uint64 timestamp; // 64 bits = 1 slot (wastes 192 bits)
uint64 nonce; // 64 bits = 1 slot (wastes 192 bits)
}
// GOOD: Uses 1 storage slot
struct OptimalPacked {
uint128 amount; // 128 bits
uint64 timestamp; // 64 bits
uint64 nonce; // 64 bits
// Total: 256 bits = 1 slot
}
}
Event-Driven Architecture
contract EventDrivenAuction {
struct Auction {
uint128 highestBid;
address highestBidder;
uint64 endTime;
}
mapping(uint256 => Auction) public auctions;
// Store bid history in events, not storage
event BidPlaced(uint256 indexed auctionId, address indexed bidder, uint256 amount);
event AuctionWon(uint256 indexed auctionId, address indexed winner, uint256 amount);
function bid(uint256 auctionId) external payable {
Auction storage auction = auctions[auctionId];
require(block.timestamp < auction.endTime, "Auction ended");
require(msg.value > auction.highestBid, "Bid too low");
// Refund previous bidder
if (auction.highestBidder != address(0)) {
payable(auction.highestBidder).transfer(auction.highestBid);
}
auction.highestBid = uint128(msg.value);
auction.highestBidder = msg.sender;
// Emit event instead of storing bid history
emit BidPlaced(auctionId, msg.sender, msg.value);
}
}
Common Pitfalls & Solutions
Avoiding Sequential Bottlenecks
contract AvoidBottlenecks {
// BAD: Global nonce forces sequential execution
uint256 public globalNonce;
mapping(uint256 => address) public nonceToUser;
function badMint() external {
uint256 tokenId = globalNonce++;
nonceToUser[tokenId] = msg.sender;
}
// GOOD: User-specific nonces allow parallelism
mapping(address => uint256) public userNonces;
mapping(address => mapping(uint256 => uint256)) public userTokens;
function goodMint() external {
uint256 userNonce = userNonces[msg.sender]++;
userTokens[msg.sender][userNonce] = block.timestamp;
}
}
Optimistic Execution
contract OptimisticVault {
mapping(address => uint256) public balances;
uint256 public totalSupply;
// Optimistic deposit - assumes success
function deposit(uint256 amount) external returns (uint256 shares) {
// Calculate shares optimistically
if (totalSupply == 0) {
shares = amount;
} else {
shares = (amount * totalSupply) / address(this).balance;
}
// Update state
balances[msg.sender] += shares;
totalSupply += shares;
// Transfer happens last - if it fails, entire tx reverts
(bool success,) = address(this).call{value: amount}("");
require(success, "Transfer failed");
}
}
Development Checklists
Gas Optimization Checklist
- ✓ Pack structs to minimize storage slots
- ✓ Use events for historical data
- ✓ Cache array lengths in loops
- ✓ Use
unchecked
blocks where safe - ✓ Batch operations when possible
- ✓ Minimize external calls
Parallel Execution Checklist
- ✓ Isolate state by user/entity
- ✓ Avoid global counters
- ✓ Use mapping over arrays for lookups
- ✓ Design for optimistic execution
- ✓ Minimize shared state dependencies
Next Steps
Now that you understand Sei Giga development patterns:
- Learn the Fundamentals - Review the Sei Giga Overview for architecture details
- Review Technical Specs - Study the Technical Specifications for detailed specifications