Oracle:
- wrapping FIO tokens and FIO domains from the FIO chain to other chains
- unwrapping FIO tokens and FIO domains from other chains to FIO chain
- burn FIO domains from FIO chain to selected NFT chain
- Node.js 22.14.0
- Linux host (e.g., Ubuntu Server 20.04)
The app now uses the config package with environment-specific JSON files. Most tunables moved from .env into config/*.json.
- Base config:
config/default.json - Overrides by network:
config/mainnet.json,config/testnet.json - Scalar env injection (API keys, secrets):
config/custom-environment-variables.json - Runtime resolver for
ENV_VAR_*placeholders inside config:config/config.js
Environment selection is driven by NODE_ENV and matching dotenv file:
NODE_ENV=mainnet→ loadsconfig/mainnet.jsonand.env.mainnetNODE_ENV=testnet→ loadsconfig/testnet.jsonand.env.testnet
References:
config/config.js:1explains the load order and environment selectionconfig/custom-environment-variables.json:1maps env vars to config paths
Create both .env.mainnet and .env.testnet in repo root (use .env.example as a guide): .env.example:1
Required/env-injected values:
- FIO keys/account
FIO_ORACLE_PRIVATE_KEY,FIO_ORACLE_ACCOUNT
- Chain oracle keys (used via
ENV_VAR_*placeholders in config)ETH_ORACLE_PRIVATE,ETH_ORACLE_PUBLICPOLYGON_ORACLE_PRIVATE,POLYGON_ORACLE_PUBLICBASE_ORACLE_PRIVATE,BASE_ORACLE_PUBLIC
- RPC/API keys
INFURA_ETH_TOKENS_API_KEY,INFURA_BASE_TOKENS_API_KEY,INFURA_POLYGON_NFTS_API_KEYMORALIS_API_KEYMORALIS_RPC_NODE_API_KEY_ETHEREUM,MORALIS_RPC_NODE_API_KEY_POLYGON,MORALIS_RPC_NODE_API_KEY_BASETHIRDWEB_API_KEY
- AWS creds for S3 log sync
AWS_S3_KEY,AWS_S3_SECRET,AWS_S3_PERMITTED_FOLDER
Moved to config (no longer in .env):
- App port, restart/retry timing, gas settings, FIO servers, job timeouts, logging flags, AWS bucket/region, etc. See
config/default.json:1.
These are examples/skeletons showing the current shape and where values come from.
Main base config: config/default.json:1
{
"app": { "port": 3000, "restartTimeout": 5000, "maxRetries": 5, "stabilityThreshold": 30000 },
"autoRetryMissingActions": { "maxRetries": 5, "retryDelayMs": 5000, "timeRangeStart": 900000, "timeRangeEnd": 3600000 },
"aws": { "s3Key": "", "s3Secret": "", "s3Bucket": "fio-oracle-logs", "s3Region": "us-east-1", "s3PermittedFolder": "" },
"chainDefaults": { "useGasApi": 1, "gasPriceLevel": "average", "defaultHardfork": "london" },
"fio": { "serverUrlHistory": [], "serverUrlAction": [], "getTableRowsOffset": 1000, "historyOffset": 1000, "lowestOracleId": 0, "maxRetries": 5, "privateKey": "", "account": "", "permission": "active", "serverStaleThresholdMinutes": 5 },
"jobTimeouts": { "defaultJobTimeout": 60000, "burnDomainsJobTimeout": 10800000, "autoRetryMissingActionsTimeout": 600000 },
"logging": { "logToFile": false, "syncIntervalHours": 1, "enableS3Sync": false },
"moralis": { "apiKey": "", "rpcBaseUrl": "https://site1.moralis-nodes.com", "rpcBaseUrlFallback": "https://site2.moralis-nodes.com", "defaultTimeoutBetweenCalls": 1000 },
"thirdWeb": { "apiKey": "" },
"supportedChains": { "tokens": [], "nfts": [] }
}
Mainnet overrides: config/mainnet.json:1
{
"app": { "port": 3030 },
"fio": {
"serverUrlHistory": ["https://fio.server-url.io/", "https://fio.server-url-2.io/"],
"serverUrlAction": ["https://fio.server-url.io/", "https://fio.server-url-2.io/"],
"lowestOracleId": 900
},
"logging": { "enableS3Sync": false },
"supportedChains": {
"tokens": [
{
"chainParams": { "chainName": "ethereum", "chainCode": "ETH", "chainId": 1 },
"contractAddress": "0x...",
"contractTypeName": "fio.erc20",
"blocksRangeLimit": 3000,
"blocksOffset": 7,
"gasLimit": 200000,
"defaultGasPrice": 30,
"infura": { "rpcUrl": "https://mainnet.infura.io/v3", "apiKey": "ENV_VAR_INFURA_ETH_TOKENS_API_KEY" },
"moralis": { "rpcNodeApiKey": "ENV_VAR_MORALIS_RPC_NODE_API_KEY_ETHEREUM", "chainName": "eth" },
"thirdweb": { "chainName": "ethereum" },
"privateKey": "ENV_VAR_ETH_ORACLE_PRIVATE",
"publicKey": "ENV_VAR_ETH_ORACLE_PUBLIC"
}
// ...additional chains
],
"nfts": [
{
"chainParams": { "chainName": "polygon", "chainCode": "POL", "chainId": 137 },
"contractAddress": "0x...",
"contractTypeName": "fio.erc721",
"blocksRangeLimit": 3000,
"gasLimit": 200000,
"defaultGasPrice": 50,
"infura": { "rpcUrl": "https://polygon-mainnet.infura.io/v3", "apiKey": "ENV_VAR_INFURA_POLYGON_NFTS_API_KEY" },
"moralis": { "rpcNodeApiKey": "ENV_VAR_MORALIS_RPC_NODE_API_KEY_POLYGON", "chainName": "polygon" },
"thirdweb": { "chainName": "polygon" },
"privateKey": "ENV_VAR_POLYGON_ORACLE_PRIVATE",
"publicKey": "ENV_VAR_POLYGON_ORACLE_PUBLIC"
}
]
}
}
Testnet overrides: config/testnet.json:1
{
"app": { "port": 3020 },
"fio": {
"serverUrlHistory": ["https://testnet.fio.server-url.io/", "https://testnet.fio.server-url-2.io/"],
"serverUrlAction": ["https://testnet.fio.server-url.io/", "https://testnet.fio.server-url-2.io/"],
"lowestOracleId": 416
},
"supportedChains": {
"tokens": [
{
"chainParams": { "chainName": "sepolia", "chainCode": "ETH", "chainId": 11155111 },
"contractAddress": "0x...",
"contractTypeName": "fio.erc20",
"blocksRangeLimit": 3000,
"blocksOffset": 7,
"gasLimit": 200000,
"defaultGasPrice": 30,
"infura": { "rpcUrl": "https://sepolia.infura.io/v3", "apiKey": "ENV_VAR_INFURA_ETH_TOKENS_API_KEY" },
"moralis": { "rpcNodeApiKey": "ENV_VAR_MORALIS_RPC_NODE_API_KEY_ETHEREUM", "chainName": "sepolia" },
"thirdweb": { "chainName": "sepolia" },
"privateKey": "ENV_VAR_ETH_ORACLE_PRIVATE",
"publicKey": "ENV_VAR_ETH_ORACLE_PUBLIC"
}
// ...additional chains
],
"nfts": [
{
"chainParams": { "chainName": "polygon amoy", "chainCode": "POL", "chainId": 80002 },
"contractAddress": "0x...",
"contractTypeName": "fio.erc721",
"blocksRangeLimit": 3000,
"gasLimit": 200000,
"defaultGasPrice": 50,
"infura": { "rpcUrl": "https://polygon-amoy.infura.io/v3", "apiKey": "ENV_VAR_INFURA_POLYGON_NFTS_API_KEY" },
"moralis": { "rpcNodeApiKey": "ENV_VAR_MORALIS_RPC_NODE_API_KEY_POLYGON", "chainName": "amoy" },
"thirdweb": { "chainName": "polygonAmoy" },
"privateKey": "ENV_VAR_POLYGON_ORACLE_PRIVATE",
"publicKey": "ENV_VAR_POLYGON_ORACLE_PUBLIC"
}
]
}
}
Notes:
- Any string value starting with
ENV_VAR_is resolved at runtime to the respective environment variable byconfig/config.js:38. - Scalar values like
aws.s3Key,fio.privateKey,moralis.apiKey,thirdWeb.apiKeyare mapped to env vars byconfig/custom-environment-variables.json:1.
- Copy
.env.example→.env.mainnetand.env.testnetand fill required keys - Install deps:
npm install
Run server:
- Mainnet:
npm run start(package.json:7) - Testnet:
npm run start:testnet(package.json:8)
Server listens on app.port from config. Startup/retry behavior is controlled by values in config/default.json:1 and logged to console. See server.js:1 and controller/main.js:1.
There is a helper CLI for on-demand wrap/unwrap/burn actions with optional queueing.
- Mainnet:
npm run oracle - Testnet:
npm run oracle:testnet
Usage (from scripts/oracle.js:1):
npm run oracle <action> <type> [key:value params]
<action>: wrap | unwrap | burn
<type>: tokens | nfts
Params (key:value):
chainCode:<code> - required (ETH, POL, BASE, ...)
nftName:<name> - for nfts (wrap/unwrap/burn)
tokenId:<id> - for burn nfts (optional if nftName resolves to tokenId)
amount:<value> - for tokens (wrap/unwrap) amount in SUF
address:<addr> - EVM address for wrap; FIO handle for unwrap
obtId:<id> - wrap: oracle id from FIO table; unwrap/burn: FIO tx hash
clean:true|false - if true, enqueue into normal job log instead of immediate execution
manualSetGasPrice:<wei> - optional manual gas price override
Examples:
npm run oracle wrap tokens chainCode:ETH amount:12000000000 address:0x... obtId:944 clean:true manualSetGasPrice:1650000016
npm run oracle wrap nfts chainCode:POL nftName:fiohacker address:0x... obtId:945 clean:true
npm run oracle unwrap tokens chainCode:BASE amount:12000000000 address:alice@fiotestnet obtId:<fioTxHash>
npm run oracle burn nfts chainCode:POL nftName:fiodomainname obtId:<fioTxHash>
Multiple FIO servers can be configured and the app rotates on failures. Configure in config:
fio.serverUrlHistory[]andfio.serverUrlAction[]inconfig/mainnet.json:1orconfig/testnet.json:1.- Retries and backoff settings are in
app,autoRetryMissingActions, andfio.maxRetriesinconfig/default.json:1.
Endpoint: GET /api/v1/health (controller/routes/health.js:1)
Example: http://localhost:<port>/api/v1/health
Returns 200 { status: "ok", timestamp: "..." } when healthy.
- Local log roots per environment are prepared under
controller/api/. - S3 sync is controlled in config:
- Enable/disable:
logging.enableS3Sync(default false) - Interval hours:
logging.syncIntervalHours - File vs console:
logging.logToFile
- Enable/disable:
- AWS credentials come from env; bucket/region are in config:
- Env:
AWS_S3_KEY,AWS_S3_SECRET,AWS_S3_PERMITTED_FOLDER - Config:
aws.s3Bucket,aws.s3Region
- Env:
Tip: On startup the app prints chain balances and gas price suggestions when chainDefaults.useGasApi is enabled. See controller/main.js:1.