Files
zeekayandHanzo Dev 64519916bf the corporate name is Lux Industries Inc
Lux Partners Limited is not the entity behind this work. Every place the
old name stood — copyright lines, site footers, brand config, legal and
compliance text — now names the company that actually holds it.

lux.fund is the exception and is not renamed to match: that site belongs
to Lux Assets Management LLC, a different company, and it says so.

Two kinds of line were left for a person rather than rewritten. A dated
news item recording that Lux Partners Limited was established in the Isle
of Man is a historical fact, and renaming it would falsify the record. And
a sentence asserting that the company is domiciled in the Isle of Man is a
claim about a jurisdiction, not a name — an Inc or an LLC is not an Isle
of Man company, so swapping the name there would assert something untrue.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-28 13:01:04 -07:00

624 lines
18 KiB
Go

// Copyright (c) 2025 Lux Industries Inc
// SPDX-License-Identifier: MIT
package defi
import (
"encoding/hex"
"fmt"
"math/big"
"strings"
"time"
)
// SynthsIndexer indexes synthetic asset events (Alchemix-style)
type SynthsIndexer struct {
vaults map[string]*SynthVault
deposits []*SynthDeposit
withdrawals []*SynthWithdrawal
mints []*SynthMint
burns []*SynthBurn
transmutes []*SynthTransmute
positions map[string]*SynthPosition // account:vault -> position
synthTokens map[string]*SynthToken
onDeposit func(*SynthDeposit)
onWithdrawal func(*SynthWithdrawal)
onMint func(*SynthMint)
onBurn func(*SynthBurn)
onTransmute func(*SynthTransmute)
}
// SynthVault represents an Alchemist vault
type SynthVault struct {
Address string `json:"address"`
Name string `json:"name"`
UnderlyingToken string `json:"underlyingToken"`
UnderlyingSymbol string `json:"underlyingSymbol,omitempty"`
YieldToken string `json:"yieldToken"`
SynthToken string `json:"synthToken"` // e.g., sUSD, sETH, sLUX
SynthSymbol string `json:"synthSymbol"`
TotalDeposited *big.Int `json:"totalDeposited"`
TotalBorrowed *big.Int `json:"totalBorrowed"`
CollateralRatio float64 `json:"collateralRatio"` // e.g., 2.0 = 200%
MaxLTV float64 `json:"maxLtv"` // Max loan-to-value
HarvestFee uint64 `json:"harvestFee"` // Fee in basis points
LiquidationFee uint64 `json:"liquidationFee"`
ActivePositions int `json:"activePositions"`
TVL *big.Float `json:"tvl"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
// SynthToken represents a synthetic token (sUSD, sETH, sBTC, sLUX, etc.)
type SynthToken struct {
Address string `json:"address"`
Symbol string `json:"symbol"`
Name string `json:"name"`
Decimals uint8 `json:"decimals"`
Underlying string `json:"underlying"`
TotalSupply *big.Int `json:"totalSupply"`
Price *big.Int `json:"price,omitempty"` // Price in underlying terms
IsCrossChain bool `json:"isCrossChain"`
}
// SynthPosition represents a user's position in a vault
type SynthPosition struct {
ID string `json:"id"`
Account string `json:"account"`
VaultAddress string `json:"vaultAddress"`
Deposited *big.Int `json:"deposited"` // Collateral deposited
Borrowed *big.Int `json:"borrowed"` // Synths borrowed
YieldEarned *big.Int `json:"yieldEarned"` // Accumulated yield
DebtRepaid *big.Int `json:"debtRepaid"` // Debt auto-repaid by yield
LTV float64 `json:"ltv"` // Current loan-to-value
HealthFactor float64 `json:"healthFactor"`
IsActive bool `json:"isActive"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
// SynthDeposit represents a collateral deposit
type SynthDeposit struct {
ID string `json:"id"`
TxHash string `json:"txHash"`
BlockNumber uint64 `json:"blockNumber"`
LogIndex uint64 `json:"logIndex"`
VaultAddress string `json:"vaultAddress"`
Account string `json:"account"`
Token string `json:"token"`
Amount *big.Int `json:"amount"`
Shares *big.Int `json:"shares"` // Vault shares received
Timestamp time.Time `json:"timestamp"`
}
// SynthWithdrawal represents a collateral withdrawal
type SynthWithdrawal struct {
ID string `json:"id"`
TxHash string `json:"txHash"`
BlockNumber uint64 `json:"blockNumber"`
LogIndex uint64 `json:"logIndex"`
VaultAddress string `json:"vaultAddress"`
Account string `json:"account"`
Token string `json:"token"`
Amount *big.Int `json:"amount"`
Shares *big.Int `json:"shares"` // Vault shares burned
Timestamp time.Time `json:"timestamp"`
}
// SynthMint represents minting synthetic tokens (borrowing)
type SynthMint struct {
ID string `json:"id"`
TxHash string `json:"txHash"`
BlockNumber uint64 `json:"blockNumber"`
LogIndex uint64 `json:"logIndex"`
VaultAddress string `json:"vaultAddress"`
Account string `json:"account"`
SynthToken string `json:"synthToken"`
Amount *big.Int `json:"amount"`
Recipient string `json:"recipient"`
Timestamp time.Time `json:"timestamp"`
}
// SynthBurn represents burning synthetic tokens (repaying)
type SynthBurn struct {
ID string `json:"id"`
TxHash string `json:"txHash"`
BlockNumber uint64 `json:"blockNumber"`
LogIndex uint64 `json:"logIndex"`
VaultAddress string `json:"vaultAddress"`
Account string `json:"account"`
SynthToken string `json:"synthToken"`
Amount *big.Int `json:"amount"`
Timestamp time.Time `json:"timestamp"`
}
// SynthTransmute represents transmuting synths to underlying
type SynthTransmute struct {
ID string `json:"id"`
TxHash string `json:"txHash"`
BlockNumber uint64 `json:"blockNumber"`
LogIndex uint64 `json:"logIndex"`
TransmuterAddr string `json:"transmuterAddress"`
Account string `json:"account"`
SynthToken string `json:"synthToken"`
SynthAmount *big.Int `json:"synthAmount"`
UnderlyingToken string `json:"underlyingToken"`
UnderlyingAmount *big.Int `json:"underlyingAmount"`
Timestamp time.Time `json:"timestamp"`
}
// NewSynthsIndexer creates a new synthetics indexer
func NewSynthsIndexer() *SynthsIndexer {
return &SynthsIndexer{
vaults: make(map[string]*SynthVault),
deposits: make([]*SynthDeposit, 0),
withdrawals: make([]*SynthWithdrawal, 0),
mints: make([]*SynthMint, 0),
burns: make([]*SynthBurn, 0),
transmutes: make([]*SynthTransmute, 0),
positions: make(map[string]*SynthPosition),
synthTokens: make(map[string]*SynthToken),
}
}
// RegisterSynthToken registers a synthetic token
func (s *SynthsIndexer) RegisterSynthToken(token *SynthToken) {
s.synthTokens[strings.ToLower(token.Address)] = token
}
// SetCallbacks sets event callbacks
func (s *SynthsIndexer) SetCallbacks(
onDeposit func(*SynthDeposit),
onWithdrawal func(*SynthWithdrawal),
onMint func(*SynthMint),
onBurn func(*SynthBurn),
onTransmute func(*SynthTransmute),
) {
s.onDeposit = onDeposit
s.onWithdrawal = onWithdrawal
s.onMint = onMint
s.onBurn = onBurn
s.onTransmute = onTransmute
}
// IndexLog processes a log entry for synths events
func (s *SynthsIndexer) IndexLog(log *LogEntry) error {
if len(log.Topics) == 0 {
return nil
}
topic0 := log.Topics[0]
switch topic0 {
case SynthsDepositSig:
return s.indexDeposit(log)
case SynthsWithdrawSig:
return s.indexWithdrawal(log)
case SynthsMintSig:
return s.indexMint(log)
case SynthsBurnSig:
return s.indexBurn(log)
case SynthsTransmuteSig:
return s.indexTransmute(log)
}
return nil
}
// indexDeposit processes a deposit event
func (s *SynthsIndexer) indexDeposit(log *LogEntry) error {
if len(log.Topics) < 2 {
return fmt.Errorf("invalid deposit event")
}
data, err := hex.DecodeString(strings.TrimPrefix(log.Data, "0x"))
if err != nil {
return err
}
if len(data) < 96 {
return fmt.Errorf("invalid deposit data")
}
account := topicToAddress(log.Topics[1])
token := "0x" + hex.EncodeToString(data[12:32])
amount := new(big.Int).SetBytes(data[32:64])
shares := new(big.Int).SetBytes(data[64:96])
deposit := &SynthDeposit{
ID: fmt.Sprintf("%s-%d", log.TxHash, log.LogIndex),
TxHash: log.TxHash,
BlockNumber: log.BlockNumber,
LogIndex: log.LogIndex,
VaultAddress: log.Address,
Account: account,
Token: token,
Amount: amount,
Shares: shares,
Timestamp: log.Timestamp,
}
s.deposits = append(s.deposits, deposit)
// Update position
s.updatePositionDeposit(log.Address, account, amount)
// Update vault
vault := s.getOrCreateVault(log.Address)
vault.TotalDeposited = new(big.Int).Add(vault.TotalDeposited, amount)
vault.UpdatedAt = log.Timestamp
if s.onDeposit != nil {
s.onDeposit(deposit)
}
return nil
}
// indexWithdrawal processes a withdrawal event
func (s *SynthsIndexer) indexWithdrawal(log *LogEntry) error {
if len(log.Topics) < 2 {
return fmt.Errorf("invalid withdrawal event")
}
data, err := hex.DecodeString(strings.TrimPrefix(log.Data, "0x"))
if err != nil {
return err
}
if len(data) < 96 {
return fmt.Errorf("invalid withdrawal data")
}
account := topicToAddress(log.Topics[1])
token := "0x" + hex.EncodeToString(data[12:32])
amount := new(big.Int).SetBytes(data[32:64])
shares := new(big.Int).SetBytes(data[64:96])
withdrawal := &SynthWithdrawal{
ID: fmt.Sprintf("%s-%d", log.TxHash, log.LogIndex),
TxHash: log.TxHash,
BlockNumber: log.BlockNumber,
LogIndex: log.LogIndex,
VaultAddress: log.Address,
Account: account,
Token: token,
Amount: amount,
Shares: shares,
Timestamp: log.Timestamp,
}
s.withdrawals = append(s.withdrawals, withdrawal)
// Update position
s.updatePositionWithdrawal(log.Address, account, amount)
// Update vault
vault := s.getOrCreateVault(log.Address)
vault.TotalDeposited = new(big.Int).Sub(vault.TotalDeposited, amount)
vault.UpdatedAt = log.Timestamp
if s.onWithdrawal != nil {
s.onWithdrawal(withdrawal)
}
return nil
}
// indexMint processes a mint (borrow) event
func (s *SynthsIndexer) indexMint(log *LogEntry) error {
if len(log.Topics) < 2 {
return fmt.Errorf("invalid mint event")
}
data, err := hex.DecodeString(strings.TrimPrefix(log.Data, "0x"))
if err != nil {
return err
}
if len(data) < 64 {
return fmt.Errorf("invalid mint data")
}
account := topicToAddress(log.Topics[1])
amount := new(big.Int).SetBytes(data[0:32])
recipient := "0x" + hex.EncodeToString(data[44:64])
vault := s.getOrCreateVault(log.Address)
mint := &SynthMint{
ID: fmt.Sprintf("%s-%d", log.TxHash, log.LogIndex),
TxHash: log.TxHash,
BlockNumber: log.BlockNumber,
LogIndex: log.LogIndex,
VaultAddress: log.Address,
Account: account,
SynthToken: vault.SynthToken,
Amount: amount,
Recipient: recipient,
Timestamp: log.Timestamp,
}
s.mints = append(s.mints, mint)
// Update position
s.updatePositionMint(log.Address, account, amount)
// Update vault
vault.TotalBorrowed = new(big.Int).Add(vault.TotalBorrowed, amount)
vault.UpdatedAt = log.Timestamp
if s.onMint != nil {
s.onMint(mint)
}
return nil
}
// indexBurn processes a burn (repay) event
func (s *SynthsIndexer) indexBurn(log *LogEntry) error {
if len(log.Topics) < 2 {
return fmt.Errorf("invalid burn event")
}
data, err := hex.DecodeString(strings.TrimPrefix(log.Data, "0x"))
if err != nil {
return err
}
if len(data) < 32 {
return fmt.Errorf("invalid burn data")
}
account := topicToAddress(log.Topics[1])
amount := new(big.Int).SetBytes(data[0:32])
vault := s.getOrCreateVault(log.Address)
burn := &SynthBurn{
ID: fmt.Sprintf("%s-%d", log.TxHash, log.LogIndex),
TxHash: log.TxHash,
BlockNumber: log.BlockNumber,
LogIndex: log.LogIndex,
VaultAddress: log.Address,
Account: account,
SynthToken: vault.SynthToken,
Amount: amount,
Timestamp: log.Timestamp,
}
s.burns = append(s.burns, burn)
// Update position
s.updatePositionBurn(log.Address, account, amount)
// Update vault
vault.TotalBorrowed = new(big.Int).Sub(vault.TotalBorrowed, amount)
vault.UpdatedAt = log.Timestamp
if s.onBurn != nil {
s.onBurn(burn)
}
return nil
}
// indexTransmute processes a transmute event
func (s *SynthsIndexer) indexTransmute(log *LogEntry) error {
if len(log.Topics) < 2 {
return fmt.Errorf("invalid transmute event")
}
data, err := hex.DecodeString(strings.TrimPrefix(log.Data, "0x"))
if err != nil {
return err
}
if len(data) < 128 {
return fmt.Errorf("invalid transmute data")
}
account := topicToAddress(log.Topics[1])
synthToken := "0x" + hex.EncodeToString(data[12:32])
synthAmount := new(big.Int).SetBytes(data[32:64])
underlyingToken := "0x" + hex.EncodeToString(data[76:96])
underlyingAmount := new(big.Int).SetBytes(data[96:128])
transmute := &SynthTransmute{
ID: fmt.Sprintf("%s-%d", log.TxHash, log.LogIndex),
TxHash: log.TxHash,
BlockNumber: log.BlockNumber,
LogIndex: log.LogIndex,
TransmuterAddr: log.Address,
Account: account,
SynthToken: synthToken,
SynthAmount: synthAmount,
UnderlyingToken: underlyingToken,
UnderlyingAmount: underlyingAmount,
Timestamp: log.Timestamp,
}
s.transmutes = append(s.transmutes, transmute)
if s.onTransmute != nil {
s.onTransmute(transmute)
}
return nil
}
// Position update helpers
func (s *SynthsIndexer) getPositionID(vault, account string) string {
return fmt.Sprintf("%s:%s", strings.ToLower(account), strings.ToLower(vault))
}
func (s *SynthsIndexer) getOrCreatePosition(vault, account string) *SynthPosition {
id := s.getPositionID(vault, account)
if pos, exists := s.positions[id]; exists {
return pos
}
pos := &SynthPosition{
ID: id,
Account: account,
VaultAddress: vault,
Deposited: big.NewInt(0),
Borrowed: big.NewInt(0),
YieldEarned: big.NewInt(0),
DebtRepaid: big.NewInt(0),
IsActive: true,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
s.positions[id] = pos
return pos
}
func (s *SynthsIndexer) updatePositionDeposit(vault, account string, amount *big.Int) {
pos := s.getOrCreatePosition(vault, account)
pos.Deposited = new(big.Int).Add(pos.Deposited, amount)
pos.UpdatedAt = time.Now()
s.updatePositionLTV(pos)
}
func (s *SynthsIndexer) updatePositionWithdrawal(vault, account string, amount *big.Int) {
pos := s.getOrCreatePosition(vault, account)
pos.Deposited = new(big.Int).Sub(pos.Deposited, amount)
pos.UpdatedAt = time.Now()
s.updatePositionLTV(pos)
}
func (s *SynthsIndexer) updatePositionMint(vault, account string, amount *big.Int) {
pos := s.getOrCreatePosition(vault, account)
pos.Borrowed = new(big.Int).Add(pos.Borrowed, amount)
pos.UpdatedAt = time.Now()
s.updatePositionLTV(pos)
}
func (s *SynthsIndexer) updatePositionBurn(vault, account string, amount *big.Int) {
pos := s.getOrCreatePosition(vault, account)
pos.Borrowed = new(big.Int).Sub(pos.Borrowed, amount)
pos.UpdatedAt = time.Now()
s.updatePositionLTV(pos)
if pos.Borrowed.Sign() <= 0 && pos.Deposited.Sign() <= 0 {
pos.IsActive = false
}
}
func (s *SynthsIndexer) updatePositionLTV(pos *SynthPosition) {
if pos.Deposited.Sign() == 0 {
pos.LTV = 0
pos.HealthFactor = 0
return
}
borrowedFloat := new(big.Float).SetInt(pos.Borrowed)
depositedFloat := new(big.Float).SetInt(pos.Deposited)
ltv, _ := new(big.Float).Quo(borrowedFloat, depositedFloat).Float64()
pos.LTV = ltv
// Health factor = 1/LTV (higher is healthier)
if ltv > 0 {
pos.HealthFactor = 1.0 / ltv
} else {
pos.HealthFactor = 100 // Fully collateralized
}
}
// getOrCreateVault gets or creates a vault entry
func (s *SynthsIndexer) getOrCreateVault(address string) *SynthVault {
addr := strings.ToLower(address)
if vault, exists := s.vaults[addr]; exists {
return vault
}
vault := &SynthVault{
Address: addr,
TotalDeposited: big.NewInt(0),
TotalBorrowed: big.NewInt(0),
TVL: big.NewFloat(0),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
s.vaults[addr] = vault
return vault
}
// GetVault returns a vault by address
func (s *SynthsIndexer) GetVault(address string) *SynthVault {
return s.vaults[strings.ToLower(address)]
}
// GetAllVaults returns all indexed vaults
func (s *SynthsIndexer) GetAllVaults() []*SynthVault {
vaults := make([]*SynthVault, 0, len(s.vaults))
for _, v := range s.vaults {
vaults = append(vaults, v)
}
return vaults
}
// GetPositionsByAccount returns all positions for an account
func (s *SynthsIndexer) GetPositionsByAccount(account string) []*SynthPosition {
var result []*SynthPosition
acc := strings.ToLower(account)
for _, pos := range s.positions {
if strings.ToLower(pos.Account) == acc {
result = append(result, pos)
}
}
return result
}
// GetActivePositions returns all active positions
func (s *SynthsIndexer) GetActivePositions() []*SynthPosition {
var result []*SynthPosition
for _, pos := range s.positions {
if pos.IsActive {
result = append(result, pos)
}
}
return result
}
// SynthsStats represents synthetics indexer statistics
type SynthsStats struct {
VaultCount uint64 `json:"vaultCount"`
PositionCount uint64 `json:"positionCount"`
ActivePositions uint64 `json:"activePositions"`
TotalDeposited *big.Int `json:"totalDeposited"`
TotalBorrowed *big.Int `json:"totalBorrowed"`
TotalDeposits uint64 `json:"totalDeposits"`
TotalWithdrawals uint64 `json:"totalWithdrawals"`
TotalMints uint64 `json:"totalMints"`
TotalBurns uint64 `json:"totalBurns"`
TotalTransmutes uint64 `json:"totalTransmutes"`
LastUpdated time.Time `json:"lastUpdated"`
}
// GetStats returns aggregate statistics
func (s *SynthsIndexer) GetStats() *SynthsStats {
stats := &SynthsStats{
VaultCount: uint64(len(s.vaults)),
PositionCount: uint64(len(s.positions)),
ActivePositions: uint64(len(s.GetActivePositions())),
TotalDeposited: big.NewInt(0),
TotalBorrowed: big.NewInt(0),
TotalDeposits: uint64(len(s.deposits)),
TotalWithdrawals: uint64(len(s.withdrawals)),
TotalMints: uint64(len(s.mints)),
TotalBurns: uint64(len(s.burns)),
TotalTransmutes: uint64(len(s.transmutes)),
LastUpdated: time.Now(),
}
for _, vault := range s.vaults {
stats.TotalDeposited.Add(stats.TotalDeposited, vault.TotalDeposited)
stats.TotalBorrowed.Add(stats.TotalBorrowed, vault.TotalBorrowed)
}
return stats
}