Skip to content
Open
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
24 changes: 22 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -784,7 +784,27 @@ The SDK can be configured via environment variables or explicit constructor opti

---

---

## ConduitConfig Reference

| Field | Type | Default | Effect |
|---|---|---|---|
| network | `'mainnet' \| 'testnet' \| 'local'` | (required) | Which Stellar network to connect to. |
| keypair | `Keypair` | undefined | Signing keypair used for mutating operations. |
| wallet | `WalletAdapter` | undefined | Browser/mobile wallet adapter (e.g. WalletConnect). |
| signer | `Signer` | undefined | Custom signer plugin (KMS/HSM). Takes precedence over keypair. |
| rpcUrl | `string` | Network default | Override the default Soroban RPC endpoint. |
| factoryAddress | `string` | Network default | Override the deployed DripFactory contract ID. |
| governorAddress | `string` | Network default | Override the deployed DripGovernor contract ID. |
| confirmationPollIntervalMs | `number` | 1000 | Poll interval for transaction confirmation. |
| confirmationMaxAttempts | `number` | 30 | Maximum confirmation polling attempts. |
| fee | `string` | undefined | Explicit inclusion fee in stroops. Takes precedence over feeMultiplier. |
| feeMultiplier | `number` | 1 | Multiplier applied to BASE_FEE when fee is not set. |
| negativeAddressCacheTtlMs | `number` | 30000 | Negative cache TTL in ms for null streamAddress queries in FactoryModule. |

---

## License

MIT — see [`LICENSE`](./LICENSE).
\n## ConduitConfig reference\n\n| Field | Type | Default | Effect |\n|---|---|---|---|\n| network | \`mainnet | testnet | local\` | (required) | Which Stellar network to connect to. |\n| keypair | \`Keypair\` | undefined | Signing keypair used for mutating operations. |\n| wallet | \`WalletAdapter\` | undefined | Browser/mobile wallet adapter (e.g. WalletConnect). |\n| signer | \`Signer\` | undefined | Custom signer plugin (KMS/HSM). Takes precedence over keypair. |\n| rpcUrl | \`string\` | Network default | Override the default Soroban RPC endpoint. |\n| factoryAddress | \`string\` | Network default | Override the deployed DripFactory contract ID. |\n| governorAddress | \`string\` | Network default | Override the deployed DripGovernor contract ID. |\n| confirmationPollIntervalMs | \`number\` | 1000 | Poll interval for transaction confirmation. |\n| confirmationMaxAttempts | \`number\` | 30 | Maximum confirmation polling attempts. |\n| fee | \`string\` | undefined | Explicit inclusion fee in stroops. Takes precedence over feeMultiplier. |\n| feeMultiplier | \`number\` | 1 | Multiplier applied to BASE_FEE when fee is not set. |\n\n
MIT — see [`LICENSE`](./LICENSE).
13 changes: 9 additions & 4 deletions src/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,13 @@ import { SUPPORTED_NETWORKS, UnsupportedChainError } from './errors.js';
* `clearAddressCache()` (#568). A *found* address is immutable and cached
* for the module's lifetime.
*/
const NEGATIVE_ADDRESS_CACHE_TTL_MS = 30_000;
export const DEFAULT_NEGATIVE_ADDRESS_CACHE_TTL_MS = 30_000;

export class FactoryModule {
private readonly rpcUrl: string;
private readonly passphrase: string;
private readonly factoryId: string;
private readonly negativeAddressCacheTtlMs: number;

/**
* Active wallet adapter, if the client was configured with `wallet` (or a
Expand All @@ -47,9 +48,9 @@ export class FactoryModule {

// streamId -> contract address. A resolved (non-null) address is immutable
// and cached for the module's lifetime; a `null` result is cached with a
// short TTL (see NEGATIVE_ADDRESS_CACHE_TTL_MS) so a dashboard polling
// configurable TTL (see DEFAULT_NEGATIVE_ADDRESS_CACHE_TTL_MS) so a dashboard polling
// list() over a page with a few archived/pending ids does not re-issue a
// stream_address simulation for each of them on every refresh (#568).
// stream_address simulation for each of them on every refresh (#568, #602).
private readonly addressCache = new Map<string, string | null>();
private readonly negativeCacheExpiry = new Map<string, number>();

Expand All @@ -60,6 +61,10 @@ export class FactoryModule {
if (!(SUPPORTED_NETWORKS as readonly string[]).includes(config.network)) {
throw new UnsupportedChainError(config.network);
}
this.negativeAddressCacheTtlMs =
config.negativeAddressCacheTtlMs !== undefined
? Math.max(0, config.negativeAddressCacheTtlMs)
: DEFAULT_NEGATIVE_ADDRESS_CACHE_TTL_MS;
this.rpcUrl = config.rpcUrl ?? DEFAULT_RPC[config.network];
this.passphrase = NETWORK_PASSPHRASE[config.network];
// There is no known default DripFactory deployment for any network —
Expand Down Expand Up @@ -181,7 +186,7 @@ export class FactoryModule {

private _cacheNegative(key: string): void {
this.addressCache.set(key, null);
this.negativeCacheExpiry.set(key, Date.now() + NEGATIVE_ADDRESS_CACHE_TTL_MS);
this.negativeCacheExpiry.set(key, Date.now() + this.negativeAddressCacheTtlMs);
}

/**
Expand Down
5 changes: 3 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,10 @@ export {
timeoutSignal,
} from './utils.js';

// RPC server lifecycle
export { getServer, clearServerCache } from './soroban.js';
// RPC server lifecycle & fee resolution
export { getServer, clearServerCache, resolveFee } from './soroban.js';
export { getTokenDecimals, clearTokenDecimalsCache } from './soroban.js';
export { FactoryModule, DEFAULT_NEGATIVE_ADDRESS_CACHE_TTL_MS } from './factory.js';

export {
formatAddress,
Expand Down
51 changes: 51 additions & 0 deletions src/tests/factory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,57 @@ describe('FactoryModule — streamAddress()', () => {
await factory.streamAddress(1n);
expect(mockSimulate).toHaveBeenCalledTimes(2);
});

it('honours custom negativeAddressCacheTtlMs and expires after the configured duration (#602)', async () => {
const { FactoryModule } = await import('../factory.js');
mockSimulate
.mockResolvedValueOnce(makeVoidScVal())
.mockResolvedValueOnce(makeU32ScVal(1));

// Configure a short 30ms negative cache TTL
const factory = new FactoryModule({
...cfg(),
negativeAddressCacheTtlMs: 30,
});

const first = await factory.streamAddress(99n);
expect(first).toBeNull();
expect(mockSimulate).toHaveBeenCalledTimes(1);

// Immediate repeat query is served from negative cache
const second = await factory.streamAddress(99n);
expect(second).toBeNull();
expect(mockSimulate).toHaveBeenCalledTimes(1);

// Wait for TTL to expire
await new Promise((resolve) => setTimeout(resolve, 45));

// Third query after TTL expiration triggers a fresh resolution
const third = await factory.streamAddress(99n);
expect(third).not.toBeNull();
expect(mockSimulate).toHaveBeenCalledTimes(2);
});

it('immediately re-queries when negativeAddressCacheTtlMs is 0 (#602)', async () => {
const { FactoryModule } = await import('../factory.js');
mockSimulate
.mockResolvedValueOnce(makeVoidScVal())
.mockResolvedValueOnce(makeVoidScVal());

const factory = new FactoryModule({
...cfg(),
negativeAddressCacheTtlMs: 0,
});

const first = await factory.streamAddress(101n);
expect(first).toBeNull();
expect(mockSimulate).toHaveBeenCalledTimes(1);

// With 0ms TTL, next query expires immediately and queries RPC
const second = await factory.streamAddress(101n);
expect(second).toBeNull();
expect(mockSimulate).toHaveBeenCalledTimes(2);
});
});

describe('FactoryModule — cache consolidation with StreamsModule', () => {
Expand Down
5 changes: 5 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ export interface ConduitConfig {
* Ignored when `fee` is set. Defaults to `1` (BASE_FEE, unchanged).
*/
feeMultiplier?: number;
/**
* Negative cache TTL in milliseconds for null (not-found) streamAddress queries in FactoryModule.
* Defaults to 30,000 ms (30 seconds) (see #602).
*/
negativeAddressCacheTtlMs?: number;
}

export interface StreamInfo {
Expand Down