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
4,030 changes: 3,273 additions & 757 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions src/compatibility/matrix/stellar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export { SorobanCompatibilityMatrix } from './soroban-compatibility-matrix';
export type {
CompatibilityEntry,
CompatibilityQuery,
CompatibilityQueryResult,
ChainPair,
TransferDirection,
} from './soroban-compatibility-matrix.types';
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { SorobanCompatibilityMatrix } from './soroban-compatibility-matrix';

describe('SorobanCompatibilityMatrix', () => {
let matrix: SorobanCompatibilityMatrix;

beforeEach(() => {
matrix = new SorobanCompatibilityMatrix(() => 1_000);
});

// ─── add / getAll ─────────────────────────────────────────────────────────

it('adds an entry and stamps addedAt', () => {
matrix.add({ sourceChain: 'stellar', targetChain: 'ethereum', direction: 'bidirectional', supportedAssets: ['USDC'] });

const all = matrix.getAll();
expect(all).toHaveLength(1);
expect(all[0].addedAt).toBe(1_000);
});

it('overwrites existing entry on re-add', () => {
matrix.add({ sourceChain: 'stellar', targetChain: 'ethereum', direction: 'bidirectional', supportedAssets: ['USDC'] });
matrix.add({ sourceChain: 'stellar', targetChain: 'ethereum', direction: 'unidirectional', supportedAssets: ['XLM'] });

expect(matrix.getAll()).toHaveLength(1);
expect(matrix.getAll()[0].direction).toBe('unidirectional');
});

// ─── remove ───────────────────────────────────────────────────────────────

it('removes an entry', () => {
matrix.add({ sourceChain: 'stellar', targetChain: 'ethereum', direction: 'bidirectional', supportedAssets: ['USDC'] });
expect(matrix.remove({ sourceChain: 'stellar', targetChain: 'ethereum' })).toBe(true);
expect(matrix.getAll()).toHaveLength(0);
});

it('returns false when removing unknown pair', () => {
expect(matrix.remove({ sourceChain: 'x', targetChain: 'y' })).toBe(false);
});

// ─── query / isSupported ──────────────────────────────────────────────────

it('returns supported=true for a registered pair', () => {
matrix.add({ sourceChain: 'stellar', targetChain: 'ethereum', direction: 'bidirectional', supportedAssets: ['USDC'] });

expect(matrix.isSupported({ sourceChain: 'stellar', targetChain: 'ethereum' })).toBe(true);
});

it('matches bidirectional pair in reverse direction', () => {
matrix.add({ sourceChain: 'stellar', targetChain: 'ethereum', direction: 'bidirectional', supportedAssets: ['USDC'] });

expect(matrix.isSupported({ sourceChain: 'ethereum', targetChain: 'stellar' })).toBe(true);
});

it('returns supported=false for unknown pair', () => {
expect(matrix.isSupported({ sourceChain: 'stellar', targetChain: 'arbitrum' })).toBe(false);
});

it('returns supported=false when entry.supported is false', () => {
matrix.add({ sourceChain: 'stellar', targetChain: 'ethereum', direction: 'bidirectional', supportedAssets: ['USDC'], supported: false });

expect(matrix.isSupported({ sourceChain: 'stellar', targetChain: 'ethereum' })).toBe(false);
});

it('filters by asset when asset is provided', () => {
matrix.add({ sourceChain: 'stellar', targetChain: 'ethereum', direction: 'bidirectional', supportedAssets: ['USDC'] });

expect(matrix.isSupported({ sourceChain: 'stellar', targetChain: 'ethereum', asset: 'USDC' })).toBe(true);
expect(matrix.isSupported({ sourceChain: 'stellar', targetChain: 'ethereum', asset: 'DAI' })).toBe(false);
});

it('query returns the entry even when asset is unsupported', () => {
matrix.add({ sourceChain: 'stellar', targetChain: 'ethereum', direction: 'bidirectional', supportedAssets: ['USDC'] });

const result = matrix.query({ sourceChain: 'stellar', targetChain: 'ethereum', asset: 'DAI' });
expect(result.supported).toBe(false);
expect(result.entry).not.toBeNull();
});

// ─── getForChain ──────────────────────────────────────────────────────────

it('returns entries where chain is source or bidirectional target', () => {
matrix.add({ sourceChain: 'stellar', targetChain: 'ethereum', direction: 'bidirectional', supportedAssets: ['USDC'] });
matrix.add({ sourceChain: 'stellar', targetChain: 'polygon', direction: 'unidirectional', supportedAssets: ['XLM'] });
matrix.add({ sourceChain: 'arbitrum', targetChain: 'ethereum', direction: 'bidirectional', supportedAssets: ['USDC'] });

const stellarEntries = matrix.getForChain('stellar');
expect(stellarEntries.map((e) => e.targetChain).sort()).toEqual(['ethereum', 'polygon']);
});
});
134 changes: 134 additions & 0 deletions src/compatibility/matrix/stellar/soroban-compatibility-matrix.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import {
CompatibilityEntry,
CompatibilityQuery,
CompatibilityQueryResult,
ChainPair,
TransferDirection,
} from './soroban-compatibility-matrix.types';

/**
* Tracks which chain pairs are supported for Soroban ↔ EVM bridge transfers
* and exposes query APIs so callers can validate routes before attempting them.
*
* Entries are keyed by a canonical `<source>:<target>` string. Bidirectional
* pairs are stored once and matched in either direction.
*
* Usage:
* const matrix = new SorobanCompatibilityMatrix();
* matrix.add({ sourceChain: 'stellar', targetChain: 'ethereum', direction: 'bidirectional', supportedAssets: ['USDC'] });
* matrix.isSupported({ sourceChain: 'stellar', targetChain: 'ethereum' }); // true
*/
export class SorobanCompatibilityMatrix {
private readonly entries = new Map<string, CompatibilityEntry>();
private readonly now: () => number;

constructor(now: () => number = () => Date.now()) {
this.now = now;
}

// ─── Mutation ─────────────────────────────────────────────────────────────

/**
* Add or replace a chain-pair compatibility entry.
*
* When an entry for the same key already exists it is overwritten so the
* matrix stays up-to-date without manual removal.
*/
add(
pair: ChainPair & {
direction: TransferDirection;
supportedAssets: string[];
supported?: boolean;
},
): void {
const key = this.key(pair.sourceChain, pair.targetChain);
this.entries.set(key, {
sourceChain: pair.sourceChain,
targetChain: pair.targetChain,
supported: pair.supported ?? true,
direction: pair.direction,
supportedAssets: [...pair.supportedAssets],
addedAt: this.now(),
});
}

/** Remove a chain-pair entry. Returns `true` if it existed. */
remove(pair: ChainPair): boolean {
const key = this.key(pair.sourceChain, pair.targetChain);
if (this.entries.delete(key)) return true;

// Try reverse key for bidirectional entries stored in the opposite order
const reverseKey = this.key(pair.targetChain, pair.sourceChain);
return this.entries.delete(reverseKey);
}

// ─── Query ────────────────────────────────────────────────────────────────

/**
* Check whether a transfer combination is supported.
*
* For bidirectional entries the direction of the query is irrelevant —
* both `(A → B)` and `(B → A)` return the same entry.
*
* If an `asset` is provided the entry must also list it in `supportedAssets`.
*/
query(q: CompatibilityQuery): CompatibilityQueryResult {
const entry = this.findEntry(q.sourceChain, q.targetChain);

if (!entry || !entry.supported) {
return { supported: false, entry: null };
}

if (q.asset && !entry.supportedAssets.includes(q.asset)) {
return { supported: false, entry };
}

return { supported: true, entry };
}

/** Convenience boolean wrapper around `query`. */
isSupported(q: CompatibilityQuery): boolean {
return this.query(q).supported;
}

/** All currently registered entries. */
getAll(): CompatibilityEntry[] {
return [...this.entries.values()];
}

/**
* All entries where `chain` appears as either source or target,
* sorted alphabetically by the partner chain name.
*/
getForChain(chain: string): CompatibilityEntry[] {
return this.getAll()
.filter(
(e) =>
e.sourceChain === chain ||
(e.direction === 'bidirectional' && e.targetChain === chain),
)
.sort((a, b) => {
const partnerA =
a.sourceChain === chain ? a.targetChain : a.sourceChain;
const partnerB =
b.sourceChain === chain ? b.targetChain : b.sourceChain;
return partnerA.localeCompare(partnerB);
});
}

// ─── Private ──────────────────────────────────────────────────────────────

private key(source: string, target: string): string {
return `${source}:${target}`;
}

private findEntry(
source: string,
target: string,
): CompatibilityEntry | undefined {
return (
this.entries.get(this.key(source, target)) ??
this.entries.get(this.key(target, source))
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export type TransferDirection = 'unidirectional' | 'bidirectional';

export interface ChainPair {
sourceChain: string;
targetChain: string;
}

export interface CompatibilityEntry extends ChainPair {
supported: boolean;
direction: TransferDirection;
supportedAssets: string[];
addedAt: number;
}

export interface CompatibilityQuery extends ChainPair {
asset?: string;
}

export interface CompatibilityQueryResult {
supported: boolean;
entry: CompatibilityEntry | null;
}
7 changes: 7 additions & 0 deletions src/logging/audit/stellar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export { SorobanBridgeAuditLogger } from './soroban-bridge-audit-logger';
export type {
AuditEvent,
AuditEventType,
AuditSearchQuery,
AuditLoggerConfig,
} from './soroban-bridge-audit-logger.types';
Loading
Loading