Skip to content

Add only swaps token router api - endpoints, sync logic, and documentation#245

Open
najienka wants to merge 2 commits intomainfrom
onlyswaps-token-routes-api
Open

Add only swaps token router api - endpoints, sync logic, and documentation#245
najienka wants to merge 2 commits intomainfrom
onlyswaps-token-routes-api

Conversation

@najienka
Copy link
Contributor

@najienka najienka commented Nov 28, 2025

PR adds Only Swaps token routes API with endpoints, events syncing logic, and documentation.

Features:

  • Multi-chain event fetching (TokenMappingAdded/Removed) across all configured chains
  • Per-chain configurable start blocks (or 0 = to skip syncing and start listening from the latest block)
  • Sync logging: shows historical blocks to fetch and resumes from last saved block in data/db.json
  • Uses lowdb for the database but we can switch over to any other preferred database
  • Token metadata caching in database with deduplication
  • Soft delete for mappings (inactive but preserved)
  • REST API for mappings, tokens, and networks
  • Rate-limited, chunked syncing to handle RPC limits
  • Testnet support

Known Issues and Limitations:

  • Filecoin mainnet and testnet node might occasionally stall using their recommended gilf RPC URL - https://api.node.glif.io/rpc/v1
  • No parallel sync (sequential only to avoid exceeding rate limits with free RPC URLs)
  • Token mapping data for non-configured networks are stored in the database but a warning in the logs, e.g., if dst token found in a smart contract token mapping event is for dst chain id 1000 but no such chain in the configuration onlyswaps-token-routes-api/src/config/networks.ts file.

How to Use:

  • Copy .env.example to .env and populate the parameters: RPC URLs, router addresses, and start blocks similar to .env.example
  • Run npm run dev in development mode to launch indexer + API. See README for production mode.
  • Watch logs for sync status and rate-limit warnings
  • Test API endpoints using README examples, e.g., List all token mappings (optionally filtered by srcChainId and dstChainId) - curl -s "http://localhost:3000/api/mappings/?srcChainId=1&dstChainId=10" | jq

@najienka najienka marked this pull request as draft November 28, 2025 10:47
@najienka najienka requested a review from CluEleSsUK November 28, 2025 13:40
@najienka najienka marked this pull request as ready for review December 3, 2025 09:43
{
"name": "onlyswaps-token-routes-api",
"version": "0.0.1",
"description": "Token mapping indexer for Only Swaps protocol",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"description": "Token mapping indexer for Only Swaps protocol",
"description": "Token mapping indexer for the only swaps protocol",


router.get("/", async (req, res) => {
try {
const { srcChainId, dstChainId } = req.query;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm always a little nervous using capital letters in query params as the spec doesn't require case sensitivity

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in practice it's probably fine though


res.json({
...token,
networkName: network?.name
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if there's no network here, it's probably a 500

@@ -0,0 +1,95 @@
import { NetworkConfig } from "../types";

export const networks: NetworkConfig[] = [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initially I was going to suggest that we make this configurable via a config file, but on second thoughts it might be nicer to have it concretely here in typescript so adding new networks requires adding them here.
It's possible we could even feed the contract deployment job from here.

Then again, we could instead centralise the list of networks somewhere else and feed this app from it. Can't decide which is nicer hmm

private db!: Low<Database>;

async initialize() {
const file = path.join(__dirname, "../../data/db.json");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this filepath seems odd - would suggest feeding from an env var or using something like ~/onlyswaps/token-api/db.json as a default

const totalBlocks = currentBlock - fromBlock;
logger.info(`[${networkName}] Syncing from block ${fromBlock} to ${currentBlock} (${totalBlocks} blocks)`);

const chunkSize = Number(process.env.BLOCK_CHUNK_SIZE) || 10000;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would suggest centralising the reading of env vars into a config object to make it obvious what can/can't be configured, and pass the config object in the constructor

// Get TokenMappingRemoved events
const removedEvents = await provider.getLogs({
address: contract.target,
topics: [ethers.id("TokenMappingRemoved(uint256,address,address)")],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this repeats the ABI in our const at the top - could maybe separate it out into its own const so we only have to change it in one place

await this.db.updateNetworkBlockNumber(chainId, end);

// Add delay between chunks to avoid rate limiting
const delayMs = Number(process.env.REQUEST_DELAY_MS) || 200;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same comment re: config

logger.info(`[${networkName}] Sync complete. Last block: ${currentBlock}`);
}

private sleep(ms: number): Promise<void> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can extract to a util function somewhere rather than method

}
}

async syncHistoricalEvents(chainId: number, fromBlock: number = 0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the logic in this func is nice and clear, but it's very long - if it's possible to split it down into smaller chunks without making artificial functions it might be even clearer

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants