From 73d591e6e2579c839c503edebfbfa54487458649 Mon Sep 17 00:00:00 2001 From: OthmanImam Date: Mon, 23 Feb 2026 11:21:32 +0100 Subject: [PATCH] feat: Token metadata worker for Stellar tokens (detect, fetch, persist) --- .../migrations/003_create_tokens_table.sql | 11 ++++ backend/src/models/index.js | 6 ++ backend/src/models/token.js | 44 +++++++++++++ backend/src/services/tokenMetadataWorker.js | 62 +++++++++++++++++++ backend/src/workers/tokenMetaWorker.js | 10 +++ 5 files changed, 133 insertions(+) create mode 100644 backend/migrations/003_create_tokens_table.sql create mode 100644 backend/src/models/token.js create mode 100644 backend/src/services/tokenMetadataWorker.js create mode 100644 backend/src/workers/tokenMetaWorker.js diff --git a/backend/migrations/003_create_tokens_table.sql b/backend/migrations/003_create_tokens_table.sql new file mode 100644 index 0000000..503feb9 --- /dev/null +++ b/backend/migrations/003_create_tokens_table.sql @@ -0,0 +1,11 @@ +-- Migration: Create tokens table +CREATE TABLE IF NOT EXISTS tokens ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + address VARCHAR(255) UNIQUE NOT NULL, + symbol VARCHAR(32), + name VARCHAR(128), + decimals INTEGER, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS idx_tokens_address ON tokens(address); diff --git a/backend/src/models/index.js b/backend/src/models/index.js index e63f03e..abba23b 100644 --- a/backend/src/models/index.js +++ b/backend/src/models/index.js @@ -1,10 +1,15 @@ const { sequelize } = require('../database/connection'); + const ClaimsHistory = require('./claimsHistory'); const Vault = require('./vault'); const SubSchedule = require('./subSchedule'); const TVL = require('./tvl'); const Beneficiary = require('./beneficiary'); const Organization = require('./organization'); +const { Token, initTokenModel } = require('./token'); + + +initTokenModel(sequelize); const models = { ClaimsHistory, @@ -13,6 +18,7 @@ const models = { TVL, Beneficiary, Organization, + Token, sequelize, }; diff --git a/backend/src/models/token.js b/backend/src/models/token.js new file mode 100644 index 0000000..cfd413d --- /dev/null +++ b/backend/src/models/token.js @@ -0,0 +1,44 @@ +const { DataTypes, Model } = require('sequelize'); + +class Token extends Model {} + +function initTokenModel(sequelize) { + Token.init( + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + address: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + }, + symbol: { + type: DataTypes.STRING(32), + }, + name: { + type: DataTypes.STRING(128), + }, + decimals: { + type: DataTypes.INTEGER, + }, + createdAt: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW, + }, + updatedAt: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW, + }, + }, + { + sequelize, + tableName: 'tokens', + indexes: [{ fields: ['address'] }], + } + ); +} + +module.exports = { Token, initTokenModel }; diff --git a/backend/src/services/tokenMetadataWorker.js b/backend/src/services/tokenMetadataWorker.js new file mode 100644 index 0000000..8131ae2 --- /dev/null +++ b/backend/src/services/tokenMetadataWorker.js @@ -0,0 +1,62 @@ +const { Token } = require('../models/token'); +const Vault = require('../models/vault'); +const axios = require('axios'); + +const SOROBAN_RPC_URL = process.env.SOROBAN_RPC_URL || 'https://soroban-rpc.testnet.stellar.org'; + +/** + * Worker to detect new token addresses and fetch/store their metadata. + */ +class TokenMetadataWorker { + constructor(sequelize) { + this.sequelize = sequelize; + } + + async detectAndFetchNewTokens() { + // 1. Get all unique token addresses from Vaults + const vaults = await Vault.findAll({ attributes: ['token_address'] }); + const addresses = vaults.map(function(v) { return v.token_address; }); + const uniqueAddresses = Array.from(new Set(addresses)); + + // 2. For each address, check if it exists in tokens table + for (let i = 0; i < uniqueAddresses.length; i++) { + const address = uniqueAddresses[i]; + const exists = await Token.findOne({ where: { address } }); + if (!exists) { + // 3. Fetch metadata from Stellar + try { + const meta = await this.fetchTokenMetadata(address); + if (meta) { + await Token.create({ + address: address, + symbol: meta.symbol, + name: meta.name, + decimals: meta.decimals, + }); + console.log(`Token metadata stored for ${address}`); + } + } catch (err) { + console.error(`Failed to fetch/store metadata for ${address}:`, err); + } + } + } + } + + async fetchTokenMetadata(address) { + // Example: Replace with actual Soroban RPC call + try { + const response = await axios.post(`${SOROBAN_RPC_URL}/getTokenMetadata`, { address: address }); + const symbol = response.data.symbol; + const name = response.data.name; + const decimals = response.data.decimals; + if (symbol && name && typeof decimals === 'number') { + return { symbol: symbol, name: name, decimals: decimals }; + } + return null; + } catch (err) { + return null; + } + } +} + +module.exports = { TokenMetadataWorker }; diff --git a/backend/src/workers/tokenMetaWorker.js b/backend/src/workers/tokenMetaWorker.js new file mode 100644 index 0000000..84ac170 --- /dev/null +++ b/backend/src/workers/tokenMetaWorker.js @@ -0,0 +1,10 @@ +const { sequelize } = require('../models'); +const { TokenMetadataWorker } = require('../services/tokenMetadataWorker'); + +async function runWorker() { + const worker = new TokenMetadataWorker(sequelize); + await worker.detectAndFetchNewTokens(); + process.exit(0); +} + +runWorker();