Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
5e5c55e
cardano test
Woft257 Dec 12, 2025
c14eb72
del Docs
Woft257 Dec 12, 2025
f397904
Feat: Add muti-token in chain Cardano
Woft257 Dec 12, 2025
5724234
finish: Integration Cardano with BLOCKFROST
Woft257 Dec 12, 2025
3830cbf
Merge pull request #1 from Woft257/cardona
Woft257 Dec 12, 2025
3b92e3e
Restore readme and config.example (Remove concurrency and update blo…
Woft257 Dec 12, 2025
ac6553e
Feat: implementation Logic to handle transaction UTXO for Cardano
Woft257 Dec 12, 2025
03a62a5
Merge pull request #2 from Woft257/main
Woft257 Dec 12, 2025
c80c00f
Add Base Test
Woft257 Dec 12, 2025
e35d225
Feat: Add mock implementations and tests for Cardano and EVM block em…
Woft257 Dec 12, 2025
179c7e0
Refactor: update mockPubkeyStore methods and logger initialization in…
Woft257 Dec 12, 2025
b6a3ae8
Add decimal package import to BaseWorker
Woft257 Dec 12, 2025
5cf3310
Delete internal/worker/base_test.go
Woft257 Dec 12, 2025
dee68e7
Feat: add support for multi-asset transactions in Cardano, including …
Woft257 Dec 13, 2025
5dbfa48
Refactor: remove unused decimal import from base.go
Woft257 Dec 13, 2025
b3be1a1
Delete internal/worker/base_test.go
Woft257 Dec 13, 2025
4760b21
Merge pull request #3 from Woft257/cardona
Woft257 Dec 13, 2025
2836b3c
Feat: enhance reorg check to support Cardano network type
Woft257 Dec 13, 2025
2a16bc0
Merge pull request #4 from Woft257/cardona
Woft257 Dec 13, 2025
0e011a0
Added batch_size and concurrency settings in cardano for transaction …
Woft257 Dec 14, 2025
de3e3a3
Feat: refactor Cardano transaction handling and remove rich transacti…
Woft257 Dec 14, 2025
a127947
Feat: enhance Cardano transaction model with collateral and reference…
Woft257 Dec 14, 2025
671bba0
Refactor: standardize Cardano transaction fetch concurrency constant
Woft257 Dec 14, 2025
7f72270
Feat: optimize block fetching with concurrency control and error hand…
Woft257 Dec 14, 2025
eb70680
Feat: enhance parallel transaction fetching with rate-limit error han…
Woft257 Dec 14, 2025
43a6ef3
Merge pull request #5 from Woft257/cardona
Woft257 Dec 14, 2025
dda8121
Feat: improve Cardano API interactions with enhanced rate limiting an…
Woft257 Dec 21, 2025
4268059
Merge pull request #6 from Woft257/Fix-RPS
Woft257 Dec 21, 2025
1f1fec4
Feat: add transaction validation for finalization, TTL, and fees in C…
Woft257 Dec 21, 2025
035279d
Fix: correct variable assignment for transaction fees in GetTransacti…
Woft257 Dec 21, 2025
5dbdbf3
Merge pull request #7 from Woft257/Fix-RPS
Woft257 Dec 21, 2025
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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
data/
logs/
configs/config.yaml

# Binaries
/indexer
/indexer.exe
*.exe
21 changes: 21 additions & 0 deletions configs/config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,27 @@ chains:
- url: "https://bsc.blockrazor.xyz"
- url: "https://bnb.rpc.subquery.network/public"

cardano_mainnet:
internal_code: "CARDANO_MAINNET"
network_id: "cardano"
type: "cardano"
start_block: 12768402 # Cardano mainnet block height
poll_interval: "15s" # Cardano block time is ~20 seconds
nodes:
- url: "https://cardano-mainnet.blockfrost.io/api/v0"
auth:
type: "header"
key: "project_id"
value: "BLOCKFROST_API_KEY" # Get from https://blockfrost.io/
client:
timeout: "30s"
max_retries: 3
retry_delay: "10s"
throttle:
rps: 10 # Blockfrost free tier allows 10 req/s
burst: 20
concurrency: 4 # With a free plan from providers like Blockfrost, it's recommended to keep this value low (e.g., 2-4)

# Infrastructure services
services:
port: 8080 # Health check and monitoring server port
Expand Down
301 changes: 301 additions & 0 deletions internal/indexer/cardano.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
package indexer

import (
"context"
"fmt"
"strings"
"time"
"sync"


"github.com/fystack/multichain-indexer/internal/rpc"
"github.com/fystack/multichain-indexer/internal/rpc/cardano"
"github.com/fystack/multichain-indexer/pkg/common/config"
"github.com/fystack/multichain-indexer/pkg/common/constant"
"github.com/fystack/multichain-indexer/pkg/common/enum"
"github.com/fystack/multichain-indexer/pkg/common/logger"
"github.com/fystack/multichain-indexer/pkg/common/types"
"github.com/shopspring/decimal"
)


type CardanoIndexer struct {
chainName string
config config.ChainConfig
failover *rpc.Failover[cardano.CardanoAPI]
}

func NewCardanoIndexer(
chainName string,
cfg config.ChainConfig,
failover *rpc.Failover[cardano.CardanoAPI],
) *CardanoIndexer {
return &CardanoIndexer{
chainName: chainName,
config: cfg,
failover: failover,
}
}

func (c *CardanoIndexer) GetName() string { return strings.ToUpper(c.chainName) }
func (c *CardanoIndexer) GetNetworkType() enum.NetworkType { return enum.NetworkTypeCardano }
func (c *CardanoIndexer) GetNetworkInternalCode() string {
return c.config.InternalCode
}
func (c *CardanoIndexer) GetNetworkId() string {
return c.config.NetworkId
}

// GetLatestBlockNumber fetches the latest block number
func (c *CardanoIndexer) GetLatestBlockNumber(ctx context.Context) (uint64, error) {
var latest uint64
err := c.failover.ExecuteWithRetry(ctx, func(api cardano.CardanoAPI) error {
n, err := api.GetLatestBlockNumber(ctx)
latest = n
return err
})
return latest, err
}

// GetBlock fetches a single block (header + txs fetched in parallel with quota)
func (c *CardanoIndexer) GetBlock(ctx context.Context, blockNumber uint64) (*types.Block, error) {
var (
header *cardano.BlockResponse
txHashes []string
txs []cardano.Transaction
)

err := c.failover.ExecuteWithRetry(ctx, func(api cardano.CardanoAPI) error {
var err error
// Fetch block header first
header, err = api.GetBlockHeaderByNumber(ctx, blockNumber)
if err != nil {
return err
}
// Use block hash to fetch transactions (avoids duplicate GetBlockHeaderByNumber call)
txHashes, err = api.GetTransactionsByBlockHash(ctx, header.Hash)
if err != nil {
return err
}
concurrency := c.config.Throttle.Concurrency
if concurrency <= 0 {
concurrency = cardano.DefaultTxFetchConcurrency
}
// Clamp concurrency to the number of transactions to avoid creating useless goroutines
if numTxs := len(txHashes); numTxs > 0 && numTxs < concurrency {
concurrency = numTxs
}
txs, err = api.FetchTransactionsParallel(ctx, txHashes, concurrency)
return err
})
if err != nil {
return nil, err
}

block := &cardano.Block{
Hash: header.Hash,
Height: header.Height,
Slot: header.Slot,
Time: header.Time,
ParentHash: header.ParentHash,
}
// attach txs
for i := range txs {
block.Txs = append(block.Txs, txs[i])
}

return c.convertBlock(block), nil
}

// GetBlocks fetches a range of blocks
func (c *CardanoIndexer) GetBlocks(
ctx context.Context,
from, to uint64,
isParallel bool,
) ([]BlockResult, error) {
if to < from {
return nil, fmt.Errorf("invalid range: from=%d, to=%d", from, to)
}

blockNums := make([]uint64, 0, to-from+1)
for n := from; n <= to; n++ {
blockNums = append(blockNums, n)
}

return c.fetchBlocks(ctx, blockNums, isParallel)
}

// GetBlocksByNumbers fetches blocks by their numbers
func (c *CardanoIndexer) GetBlocksByNumbers(
ctx context.Context,
blockNumbers []uint64,
) ([]BlockResult, error) {
return c.fetchBlocks(ctx, blockNumbers, false)
}

// fetchBlocks is the internal method to fetch blocks
func (c *CardanoIndexer) fetchBlocks(
ctx context.Context,
blockNums []uint64,
isParallel bool,
) ([]BlockResult, error) {
if len(blockNums) == 0 {
return nil, nil
}

// For Cardano, we should fetch blocks sequentially to avoid rate limiting
// because each block fetch involves multiple API calls (header + txs + utxos for each tx)
// With Blockfrost free tier (10 RPS), parallel block fetching can easily exceed limits
workers := 1 // Always use 1 worker for block fetching to be safe

// Only use configured concurrency if explicitly parallel and concurrency > 1
if isParallel && c.config.Throttle.Concurrency > 1 {
workers = c.config.Throttle.Concurrency
if workers > len(blockNums) {
workers = len(blockNums)
}
}

type job struct {
num uint64
index int
}

jobs := make(chan job, len(blockNums))
results := make([]BlockResult, len(blockNums))

var wg sync.WaitGroup
for i := 0; i < workers; i++ {
wg.Add(1)
go func(workerID int) {
defer wg.Done()
blockCount := 0
for j := range jobs {
// Early exit if context is canceled
select {
case <-ctx.Done():
return
default:
}

// Add delay every 5 blocks to avoid rate limiting
// This is critical for Cardano/Blockfrost to prevent burst traffic
if blockCount > 0 && blockCount%5 == 0 {
logger.Debug("Rate limit protection: pausing between blocks",
"worker", workerID, "blocks_processed", blockCount)
select {
case <-ctx.Done():
return
case <-time.After(2 * time.Second):
}
}

block, err := c.GetBlock(ctx, j.num)
if err != nil {
logger.Warn("failed to fetch block", "block", j.num, "error", err)
results[j.index] = BlockResult{
Number: j.num,
Error: &Error{ErrorType: ErrorTypeBlockNotFound, Message: err.Error()},
}
} else {
results[j.index] = BlockResult{Number: j.num, Block: block}
}
blockCount++
}
}(i)
}

// Feed jobs to workers and close channel when done
go func() {
defer close(jobs)
for i, num := range blockNums {
select {
case jobs <- job{num: num, index: i}:
case <-ctx.Done():
return
}
}
}()

wg.Wait()

// Check if the context was canceled during the operation
if ctx.Err() != nil {
return nil, ctx.Err()
}

return results, nil
}

// convertBlock converts a Cardano block to the common Block type
func (c *CardanoIndexer) convertBlock(block *cardano.Block) *types.Block {
// Pre-allocate slice with a reasonable capacity to reduce re-allocations
estimatedSize := len(block.Txs) * 2
transactions := make([]types.Transaction, 0, estimatedSize)

for _, tx := range block.Txs {
// Skip failed transactions (e.g., script validation failed)
// valid when: no script (nil) OR smart contract executed successfully (true)
if tx.ValidContract != nil && !*tx.ValidContract {
continue
}
// Find a representative from address from non-reference, non-collateral inputs
fromAddr := ""
for _, inp := range tx.Inputs {
if !inp.Reference && !inp.Collateral && inp.Address != "" {
fromAddr = inp.Address
break
}
}

// Convert fee (lovelace -> ADA) and assign to the first transfer produced by this tx
feeAda := decimal.NewFromInt(int64(tx.Fee)).Div(decimal.NewFromInt(1_000_000))
feeAssigned := false

for _, out := range tx.Outputs {
// Skip collateral outputs as they are not considered transfers to the recipient
if out.Collateral {
continue
}
for _, amt := range out.Amounts {
if amt.Quantity == "" || amt.Quantity == "0" {
continue
}
tr := types.Transaction{
TxHash: tx.Hash,
NetworkId: c.GetNetworkId(),
BlockNumber: block.Height,
FromAddress: fromAddr,
ToAddress: out.Address,
Amount: amt.Quantity,
Type: constant.TxnTypeTransfer,
Timestamp: block.Time,
}
if amt.Unit != "lovelace" {
tr.AssetAddress = amt.Unit
}
if !feeAssigned {
tr.TxFee = feeAda
feeAssigned = true
}
transactions = append(transactions, tr)
}
}
}

return &types.Block{
Number: block.Height,
Hash: block.Hash,
ParentHash: block.ParentHash,
Timestamp: block.Time,
Transactions: transactions,
}
}

// IsHealthy checks if the indexer is healthy
func (c *CardanoIndexer) IsHealthy() bool {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := c.GetLatestBlockNumber(ctx)
return err == nil
}
22 changes: 22 additions & 0 deletions internal/rpc/cardano/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package cardano

import (
"context"

"github.com/fystack/multichain-indexer/internal/rpc"
)

// CardanoAPI defines the interface for Cardano RPC operations
type CardanoAPI interface {
rpc.NetworkClient
GetLatestBlockNumber(ctx context.Context) (uint64, error)
GetBlockHeaderByNumber(ctx context.Context, blockNumber uint64) (*BlockResponse, error)
GetBlockByNumber(ctx context.Context, blockNumber uint64) (*Block, error)
GetBlockHash(ctx context.Context, blockNumber uint64) (string, error)
GetTransactionsByBlock(ctx context.Context, blockNumber uint64) ([]string, error)
GetTransactionsByBlockHash(ctx context.Context, blockHash string) ([]string, error)
GetTransaction(ctx context.Context, txHash string) (*Transaction, error)
FetchTransactionsParallel(ctx context.Context, txHashes []string, concurrency int) ([]Transaction, error)
GetBlockByHash(ctx context.Context, blockHash string) (*Block, error)
}

Loading