Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 7 additions & 7 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ POLL_INTERVAL_MS=6000
# ─── SAC Contract IDs ────────────────────────────────────────
# Comma-separated list of Soroban SAC (Stellar Asset Contract) IDs to watch.
# When unset, defaults to the native XLM SAC for the configured STELLAR_NETWORK:
# testnet → CDMLFMKMMD7MWZP3FKUBZPVHTUEDLSX4BYGYKH4GCESXYHS3IHQ4EIG4
# mainnet → CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC
# testnet → CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC
# mainnet → CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA
#
# How to derive a SAC address for any Stellar asset:
# import { Asset, Networks } from '@stellar/stellar-sdk';
Expand All @@ -57,15 +57,15 @@ POLL_INTERVAL_MS=6000
# .contractId(Networks.PUBLIC);
#
# Known mainnet SAC addresses:
# XLM (native): CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC
# USDC (Centre): CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA
# XLM (native): CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA
# USDC (Centre): CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75
#
# Example (watch XLM + USDC on mainnet):
# SAC_CONTRACT_IDS=CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC,CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA
# SAC_CONTRACT_IDS=CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA,CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75
#
# Per-network watch lists (for dual-network deployments when NETWORKS=testnet,mainnet):
# SAC_CONTRACT_IDS_TESTNET=CDMLFMKMMD7MWZP3FKUBZPVHTUEDLSX4BYGYKH4GCESXYHS3IHQ4EIG4
# SAC_CONTRACT_IDS_MAINNET=CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC
# SAC_CONTRACT_IDS_TESTNET=CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC
# SAC_CONTRACT_IDS_MAINNET=CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA
#
# Soroban RPC's getEvents accepts multiple contract IDs in a single request —
# there is no N+1 query penalty for watching multiple contracts.
Expand Down
2 changes: 1 addition & 1 deletion docs/DUAL_NETWORK.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ behave exactly as before; #161 and #163 pass it explicitly.
| `NETWORKS` | `testnet` | `testnet,mainnet` to index both in one process |
| `STELLAR_NETWORK` | `testnet` | `mainnet` |
| `SOROBAN_RPC_URL` | `https://soroban-testnet.stellar.org` | external provider endpoint (**secret — host env only**) |
| `SAC_CONTRACT_IDS` | testnet SAC `CDMLFMKM…` | mainnet XLM SAC `CDLZFC3SY…` |
| `SAC_CONTRACT_IDS` | testnet SAC `CDLZFC3SY…` | mainnet XLM SAC `CAS3J7GY…` |
| `DATABASE_URL` | testnet DB | **separate** mainnet DB |
| `HORIZON_URL` | optional testnet | optional `https://horizon.stellar.org` |

Expand Down
12 changes: 12 additions & 0 deletions src/__tests__/sacContractIds.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
* - SAC_CONTRACT_IDS takes precedence over CONTRACT_IDS when both set
*/

import { Asset, Networks } from '@stellar/stellar-sdk';
import {
resolveSacContractIds,
DEFAULT_XLM_SAC_MAINNET,
Expand All @@ -22,6 +23,8 @@ beforeEach(() => {
// Shallow-clone so mutations don't bleed between tests
process.env = { ...ORIGINAL_ENV };
delete process.env.SAC_CONTRACT_IDS;
delete process.env.SAC_CONTRACT_IDS_MAINNET;
delete process.env.SAC_CONTRACT_IDS_TESTNET;
delete process.env.CONTRACT_IDS;
delete process.env.STELLAR_NETWORK;
});
Expand Down Expand Up @@ -69,6 +72,15 @@ describe('resolveSacContractIds — legacy CONTRACT_IDS fallback', () => {

// ── Default XLM SAC fallback ─────────────────────────────────────────────────
describe('resolveSacContractIds — default XLM SAC when no env var is set', () => {
it.each([
['mainnet', Networks.PUBLIC, DEFAULT_XLM_SAC_MAINNET],
['testnet', Networks.TESTNET, DEFAULT_XLM_SAC_TESTNET],
] as const)('uses the SDK-derived native SAC for %s', (network, passphrase, defaultId) => {
const derivedId = Asset.native().contractId(passphrase);
expect(defaultId).toBe(derivedId);
expect(resolveSacContractIds(network)).toEqual([derivedId]);
});

it('defaults to the testnet XLM SAC when STELLAR_NETWORK is unset', () => {
const result = resolveSacContractIds();
expect(result).toEqual([DEFAULT_XLM_SAC_TESTNET]);
Expand Down
21 changes: 15 additions & 6 deletions src/__tests__/sacDetect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* fake instance fetcher is injected throughout.
*/

import { xdr } from "@stellar/stellar-sdk";
import { Asset, Networks, xdr } from "@stellar/stellar-sdk";
import {
executableIsSac,
instanceValIsSac,
Expand All @@ -17,9 +17,6 @@ import {
type InstanceFetcher,
} from "../indexer/sac-detect";

// The native XLM SACs are hard-coded as known SACs in the module.
const TESTNET_XLM_SAC = "CDMLFMKMMD7MWZP3FKUBZPVHTUEDLSX4BYGYKH4GCESXYHS3IHQ4EIG4";

function sacInstanceVal(): xdr.ScVal {
return xdr.ScVal.scvContractInstance(
new xdr.ScContractInstance({
Expand Down Expand Up @@ -67,12 +64,24 @@ describe("instanceValIsSac", () => {
});

describe("detectSac", () => {
it("returns true for a known native XLM SAC without fetching", async () => {
it.each([
["mainnet", Networks.PUBLIC],
["testnet", Networks.TESTNET],
] as const)("recognizes the SDK-derived %s native XLM SAC without fetching", async (network, passphrase) => {
const fetcher = jest.fn<Promise<xdr.ScVal | null>, [string]>();
await expect(detectSac(TESTNET_XLM_SAC, fetcher)).resolves.toBe(true);
const contractId = Asset.native().contractId(passphrase);
await expect(detectSac(contractId, fetcher, network)).resolves.toBe(true);
expect(fetcher).not.toHaveBeenCalled();
});

it("does not treat the standalone native SAC as a known mainnet or testnet contract", async () => {
const contractId = Asset.native().contractId(Networks.STANDALONE);
const fetcher = jest.fn(async () => null);
await expect(detectSac(contractId, fetcher, "mainnet")).resolves.toBe(false);
await expect(detectSac(contractId, fetcher, "testnet")).resolves.toBe(false);
expect(fetcher).toHaveBeenCalledTimes(2);
});

it("classifies a SAC via the injected fetcher", async () => {
const fetcher: InstanceFetcher = async () => sacInstanceVal();
await expect(detectSac("CSACTOKEN", fetcher)).resolves.toBe(true);
Expand Down
4 changes: 2 additions & 2 deletions src/indexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ export function resolveNftContractIds(network?: Network): string[] {
// These are derived from Asset.native().contractId(Networks.PUBLIC / Networks.TESTNET)
// and serve as the backwards-compatible default when SAC_CONTRACT_IDS is unset.
export const DEFAULT_XLM_SAC_MAINNET =
"CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC";
"CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA";
export const DEFAULT_XLM_SAC_TESTNET =
"CDMLFMKMMD7MWZP3FKUBZPVHTUEDLSX4BYGYKH4GCESXYHS3IHQ4EIG4";
"CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC";

/**
* Resolve the list of SAC contract IDs to watch.
Expand Down
4 changes: 2 additions & 2 deletions src/indexer/sac-detect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ import { resolveNetwork, type Network } from "../network";
// The native XLM SAC on mainnet and testnet. These are fixed by the network and
// never change, so we short-circuit detection (and any RPC call) for them.
const KNOWN_SAC_IDS = new Set<string>([
"CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC", // mainnet native XLM
"CDMLFMKMMD7MWZP3FKUBZPVHTUEDLSX4BYGYKH4GCESXYHS3IHQ4EIG4", // testnet native XLM
"CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA", // mainnet native XLM
"CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC", // testnet native XLM
]);

// ─── Pure helpers ─────────────────────────────────────────────────────────────
Expand Down