Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Remora Agent Configuration
# Copy this file to .env and fill in your values
# NEVER commit .env to git!

# =============================================================================
# Blockchain Configuration
# =============================================================================

# RPC endpoint
# Free options (no API key needed):
# Ethereum: https://eth.llamarpc.com
# Sepolia: https://rpc.sepolia.org
# Base: https://mainnet.base.org
# Base Sepolia: https://sepolia.base.org
RPC_URL=https://sepolia.base.org

# Chain ID (1=mainnet, 11155111=sepolia, 8453=base, 84532=base-sepolia)
CHAIN_ID=84532

# =============================================================================
# Agent Wallet
# =============================================================================

# Private key (64 hex characters, without 0x prefix)
# Export from MetaMask: Account Details > Export Private Key
# WARNING: Keep this secret! Anyone with this key can control your funds.
AGENT_PRIVATE_KEY=your_private_key_here_without_0x_prefix

# =============================================================================
# Rebalance Agent
# =============================================================================

# Rebalance cron schedule (standard cron format)
# Examples:
# */5 * * * * - every 5 minutes
# 0 * * * * - every hour
# */30 * * * * - every 30 minutes
REBALANCE_SCHEDULE=*/5 * * * *

8 changes: 8 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ jobs:
with:
go-version: ${{ env.GO_VERSION }}
cache: true
- name: Install abigen
run: go install github.com/ethereum/go-ethereum/cmd/abigen@latest
- name: Generate contracts
run: export PATH=$PATH:$(go env GOPATH)/bin && make abigen
- name: Run go test
run: |
make test
Expand Down Expand Up @@ -66,6 +70,10 @@ jobs:
with:
go-version: ${{ env.GO_VERSION }}
cache: false
- name: Install abigen
run: go install github.com/ethereum/go-ethereum/cmd/abigen@latest
- name: Generate contracts
run: export PATH=$PATH:$(go env GOPATH)/bin && make abigen
- name: golangci-lint
uses: golangci/golangci-lint-action@v8
with:
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,6 @@ internal/liquidity/repository/contracts/*.go

# Local tools (large binary)
toolbox
.env
/rebalance
/bin/
125 changes: 125 additions & 0 deletions cmd/rebalance/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package main

import (
"context"
"log/slog"
"os"
"os/signal"
"syscall"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/joho/godotenv"
"github.com/robfig/cron/v3"

"remora/internal/agent"
"remora/internal/signer"
)

func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
AddSource: true,
}))
slog.SetDefault(logger)

// Load .env file
if err := godotenv.Load(); err != nil {
logger.Warn("no .env file found, using environment variables")
}

// Initialize signer
sgn, err := signer.NewFromEnv()
if err != nil {
logger.Error("failed to create signer", slog.Any("error", err))
os.Exit(1)
}
logger.Info("signer initialized", slog.String("address", sgn.Address().Hex()))

// Initialize eth client
rpcURL := os.Getenv("RPC_URL")
if rpcURL == "" {
logger.Error("RPC_URL not set")
os.Exit(1)
}

ethClient, err := ethclient.Dial(rpcURL)
if err != nil {
logger.Error("failed to connect to RPC", slog.Any("error", err))
os.Exit(1)
}
defer ethClient.Close()
logger.Info("connected to RPC", slog.String("url", rpcURL))

// Initialize vault source (mock for now)
// TODO: Replace with real VaultFactory implementation
vaultSource := agent.NewMockVaultSource([]common.Address{
// Add test vault addresses here
})

// Initialize agent service
agentSvc := agent.New(
vaultSource,
nil, // TODO: strategySvc
sgn,
ethClient,
logger,
)

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

// Setup cron scheduler
schedule := parseRebalanceSchedule()
c := cron.New()

_, err = c.AddFunc(schedule, func() {
runAgent(ctx, agentSvc, logger)
})
if err != nil {
logger.Error("invalid cron schedule", slog.Any("error", err))
os.Exit(1)

Check failure on line 80 in cmd/rebalance/main.go

View workflow job for this annotation

GitHub Actions / Lint

exitAfterDefer: os.Exit will exit, and `defer cancel()` will not run (gocritic)
}

c.Start()
logger.Info("agent started", slog.String("schedule", schedule))

// Run immediately on startup
runAgent(ctx, agentSvc, logger)

// Handle shutdown
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM)

<-interrupt
logger.Info("shutting down...")
c.Stop()
cancel()
}

func runAgent(ctx context.Context, agentSvc *agent.Service, logger *slog.Logger) {
logger.InfoContext(ctx, "running rebalance check")

results, err := agentSvc.Run(ctx)
if err != nil {
logger.ErrorContext(ctx, "rebalance run failed", slog.Any("error", err))
return
}

for _, r := range results {
logger.InfoContext(ctx, "vault processed",
slog.String("address", r.VaultAddress.Hex()),
slog.Bool("rebalanced", r.Rebalanced),
slog.String("reason", r.Reason),
)
}

logger.InfoContext(ctx, "rebalance check completed", slog.Int("vaults", len(results)))
}

func parseRebalanceSchedule() string {
schedule := os.Getenv("REBALANCE_SCHEDULE")
if schedule == "" {
return "*/5 * * * *" // default: every 5 minutes
}
return schedule
}
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ require (
github.com/google/go-cmp v0.7.0
github.com/google/uuid v1.6.0
github.com/jackc/pgx/v5 v5.5.4
github.com/joho/godotenv v1.5.1
github.com/mitchellh/mapstructure v1.5.0
github.com/riandyrn/otelchi v0.10.0
github.com/shopspring/decimal v1.4.0
github.com/spf13/viper v1.12.0
go.opentelemetry.io/otel v1.37.0
go.opentelemetry.io/otel/trace v1.37.0
Expand Down Expand Up @@ -219,6 +221,7 @@ require (
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect
github.com/raeperd/recvcheck v0.2.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/ryancurrah/gomodguard v1.4.1 // indirect
github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect
Expand All @@ -229,7 +232,6 @@ require (
github.com/schollz/progressbar/v3 v3.18.0 // indirect
github.com/securego/gosec/v2 v2.22.11 // indirect
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/sivchari/containedctx v1.0.3 // indirect
github.com/sonatard/noctx v0.4.0 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,8 @@ github.com/jingyugao/rowserrcheck v1.1.1 h1:zibz55j/MJtLsjP1OF4bSdgXxwL1b+Vn7Tjz
github.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c=
github.com/jjti/go-spancheck v0.6.5 h1:lmi7pKxa37oKYIMScialXUK6hP3iY5F1gu+mLBPgYB8=
github.com/jjti/go-spancheck v0.6.5/go.mod h1:aEogkeatBrbYsyW6y5TgDfihCulDYciL1B7rG2vSsrU=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/julz/importas v0.2.0 h1:y+MJN/UdL63QbFJHws9BVC5RpA2iq0kpjrFajTGivjQ=
github.com/julz/importas v0.2.0/go.mod h1:pThlt589EnCYtMnmhmRYY/qn9lCf/frPOK+WMx3xiJY=
github.com/kaptinlin/go-i18n v0.1.3 h1:Zmc2sp3N3eNxAPEiyfdbZgF+QF8LZdOdZNR1gHefUe4=
Expand Down Expand Up @@ -574,6 +576,8 @@ github.com/riandyrn/otelchi v0.10.0/go.mod h1:zBaX2FavWMlsvq4GqHit+QXxF1c5wIMZZF
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
Expand Down
126 changes: 126 additions & 0 deletions internal/agent/agent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package agent

import (
"context"
"log/slog"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"

"remora/internal/signer"
"remora/internal/strategy"
"remora/internal/vault"
)

// VaultSource provides access to vault addresses.
type VaultSource interface {
GetVaultAddresses(ctx context.Context) ([]common.Address, error)
}

// RebalanceResult represents the result of a rebalance operation.
type RebalanceResult struct {
VaultAddress common.Address
Rebalanced bool
Reason string // "deviation_exceeded", "skipped", "error"
}

// Service is the main agent orchestrator.
type Service struct {
vaultSource VaultSource
strategySvc strategy.Service
signer *signer.Signer
ethClient *ethclient.Client
logger *slog.Logger

deviationThreshold float64
}

// New creates a new agent service.
func New(
vaultSource VaultSource,
strategySvc strategy.Service,
signer *signer.Signer,
ethClient *ethclient.Client,
logger *slog.Logger,
) *Service {
return &Service{
vaultSource: vaultSource,
strategySvc: strategySvc,
signer: signer,
ethClient: ethClient,
logger: logger,
deviationThreshold: 0.1,

Check failure on line 52 in internal/agent/agent.go

View workflow job for this annotation

GitHub Actions / Lint

Magic number: 0.1, in <assign> detected (mnd)
}
}

// Run executes one round of rebalance check for all vaults.
func (s *Service) Run(ctx context.Context) ([]RebalanceResult, error) {
addresses, err := s.vaultSource.GetVaultAddresses(ctx)
if err != nil {
return nil, err
}

s.logger.Info("starting rebalance run", slog.Int("vault_count", len(addresses)))

var results []RebalanceResult
for _, addr := range addresses {
result := s.processVault(ctx, addr)
results = append(results, result)
}

return results, nil
}

// processVault handles rebalance logic for a single vault.
func (s *Service) processVault(_ context.Context, vaultAddr common.Address) RebalanceResult {
s.logger.Info("processing vault", slog.String("address", vaultAddr.Hex()))

// Step 1: Create vault client
auth, err := s.signer.TransactOpts()
if err != nil {
return RebalanceResult{VaultAddress: vaultAddr, Reason: "signer_error"}
}

vaultClient, err := vault.NewClient(vaultAddr, s.ethClient, auth)
if err != nil {
return RebalanceResult{VaultAddress: vaultAddr, Reason: "vault_client_error"}
}

// Step 2: Get vault state and current positions
// TODO: state, err := vaultClient.GetState(ctx)
// TODO: currentPositions, err := vaultClient.GetPositions(ctx)
_ = vaultClient

// Step 3: Compute target positions using strategy service
// TODO: targetResult, err := s.computeTargetPositions(ctx, state.PoolKey)

// Step 4: Calculate deviation between current and target
// TODO: deviation := s.calculateDeviation(currentPositions, targetResult)

// Step 5: Check if rebalance is needed
// TODO: if deviation < s.deviationThreshold { return skipped }

// Step 6: Execute rebalance
// TODO: err := s.executeRebalance(ctx, vaultClient, targetResult)

return RebalanceResult{
VaultAddress: vaultAddr,
Rebalanced: false,
Reason: "not_implemented",
}
}

// =============================================================================
// Private methods to implement
// =============================================================================

// computeTargetPositions computes target positions for a vault.
// Flow: PoolKey -> liquidity.GetDistribution -> strategy.ComputeTargetPositions
// func (s *Service) computeTargetPositions(ctx context.Context, poolKey vault.PoolKey) (*strategy.ComputeResult, error)

// calculateDeviation calculates deviation between current and target positions.
// func (s *Service) calculateDeviation(current []vault.Position, target *strategy.ComputeResult) float64

// executeRebalance executes rebalance transactions.
// Flow: 1. Burn all existing positions 2. Mint new positions
// func (s *Service) executeRebalance(ctx context.Context, client vault.Vault, target *strategy.ComputeResult) error
25 changes: 25 additions & 0 deletions internal/agent/vault_source_mock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package agent

import (
"context"

"github.com/ethereum/go-ethereum/common"
)

// MockVaultSource is a mock implementation of VaultSource for testing.
type MockVaultSource struct {
addresses []common.Address
}

// NewMockVaultSource creates a mock vault source with predefined addresses.
func NewMockVaultSource(addresses []common.Address) *MockVaultSource {
return &MockVaultSource{addresses: addresses}
}

// GetVaultAddresses returns the list of vault addresses.
func (m *MockVaultSource) GetVaultAddresses(_ context.Context) ([]common.Address, error) {
return m.addresses, nil
}

// Ensure MockVaultSource implements VaultSource.
var _ VaultSource = (*MockVaultSource)(nil)
Loading
Loading