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
108 changes: 108 additions & 0 deletions apps/backend/src/services/stellar-asset-dex-compatibility.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/**
* Stellar Asset Code DEX Compatibility Tests (#620)
*
* Tests valid alphanum4/alphanum12 asset codes and rejection of
* DEX-incompatible assets with clear error messages.
*/

import { describe, it, expect } from 'vitest';
import {
validateAssetCodeDexCompatibility,
resolveAssetVariant,
} from './stellar-asset-validator.service';

// ---------------------------------------------------------------------------
// resolveAssetVariant
// ---------------------------------------------------------------------------

describe('resolveAssetVariant', () => {
it('returns "native" for XLM', () => {
expect(resolveAssetVariant('XLM')).toBe('native');
});

it('returns "alphanum4" for 1-4 char codes', () => {
expect(resolveAssetVariant('A')).toBe('alphanum4');
expect(resolveAssetVariant('USDC')).toBe('alphanum4');
});

it('returns "alphanum12" for 5-12 char codes', () => {
expect(resolveAssetVariant('MYTKN')).toBe('alphanum12');
expect(resolveAssetVariant('STELLARCOIN')).toBe('alphanum12');
});

it('returns null for codes longer than 12 chars', () => {
expect(resolveAssetVariant('TOOLONGASSET1')).toBeNull();
});
});

// ---------------------------------------------------------------------------
// validateAssetCodeDexCompatibility
// ---------------------------------------------------------------------------

describe('validateAssetCodeDexCompatibility – valid codes', () => {
it('accepts XLM as native and DEX-compatible', () => {
const result = validateAssetCodeDexCompatibility('XLM');
expect(result.compatible).toBe(true);
expect(result.variant).toBe('native');
});

it('accepts valid alphanum4 codes', () => {
for (const code of ['USD', 'USDC', 'BTC', 'A']) {
const result = validateAssetCodeDexCompatibility(code);
expect(result.compatible).toBe(true);
expect(result.variant).toBe('alphanum4');
}
});

it('accepts valid alphanum12 codes', () => {
for (const code of ['MYTOKEN', 'STELLARCOIN', 'TOKEN12']) {
const result = validateAssetCodeDexCompatibility(code);
expect(result.compatible).toBe(true);
expect(result.variant).toBe('alphanum12');
}
});
});

describe('validateAssetCodeDexCompatibility – invalid / incompatible codes', () => {
it('rejects empty string', () => {
const result = validateAssetCodeDexCompatibility('');
expect(result.compatible).toBe(false);
expect(result.error?.code).toBe('ASSET_CODE_EMPTY');
});

it('rejects non-string input', () => {
const result = validateAssetCodeDexCompatibility(null);
expect(result.compatible).toBe(false);
});

it('rejects codes with special characters', () => {
const result = validateAssetCodeDexCompatibility('USD-C');
expect(result.compatible).toBe(false);
expect(result.error?.code).toBe('ASSET_CODE_INVALID_CHARSET');
});

it('rejects lowercase codes as DEX-incompatible', () => {
const result = validateAssetCodeDexCompatibility('usdc');
expect(result.compatible).toBe(false);
expect(result.error?.code).toBe('DEX_INCOMPATIBLE_CHARSET');
expect(result.error?.message).toContain('lowercase');
});

it('rejects mixed-case codes as DEX-incompatible', () => {
const result = validateAssetCodeDexCompatibility('Usdc');
expect(result.compatible).toBe(false);
expect(result.error?.code).toBe('DEX_INCOMPATIBLE_CHARSET');
});

it('rejects codes longer than 12 characters', () => {
const result = validateAssetCodeDexCompatibility('TOOLONGASSET1');
expect(result.compatible).toBe(false);
expect(result.error?.code).toBe('ASSET_CODE_INVALID_LENGTH');
});

it('provides a clear, actionable error message for lowercase codes', () => {
const result = validateAssetCodeDexCompatibility('mytoken');
expect(result.compatible).toBe(false);
expect(result.error?.message).toContain('DEX liquidity pools');
});
});
97 changes: 97 additions & 0 deletions apps/backend/src/services/stellar-asset-validator.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,100 @@ export class StellarAssetValidator {
}

export const stellarAssetValidator = new StellarAssetValidator();

// ---------------------------------------------------------------------------
// DEX Liquidity Pool Compatibility (#620)
// ---------------------------------------------------------------------------

/**
* Stellar asset type variants for DEX compatibility checks.
* - `alphanum4` : 1–4 character asset codes
* - `alphanum12` : 5–12 character asset codes
* - `native` : XLM (always DEX-compatible)
*/
export type AssetVariant = 'native' | 'alphanum4' | 'alphanum12';

export interface DexCompatibilityResult {
compatible: boolean;
variant?: AssetVariant;
error?: {
field: string;
message: string;
code: DexCompatibilityErrorCode;
};
}

export type DexCompatibilityErrorCode =
| AssetValidationErrorCode
| 'DEX_INCOMPATIBLE_CODE_LENGTH'
| 'DEX_INCOMPATIBLE_CHARSET';

/**
* Determines the asset variant (native / alphanum4 / alphanum12) from a code.
* Returns null if the code is invalid.
*/
export function resolveAssetVariant(code: string): AssetVariant | null {
if (code === 'XLM') return 'native';
if (code.length >= 1 && code.length <= 4) return 'alphanum4';
if (code.length >= 5 && code.length <= 12) return 'alphanum12';
return null;
}

/**
* Validates an asset code for DEX liquidity pool compatibility.
*
* DEX liquidity pools on Stellar require:
* - Alphanumeric-4 codes: 1–4 uppercase alphanumeric characters.
* - Alphanumeric-12 codes: 5–12 uppercase alphanumeric characters.
* - Native (XLM): always compatible.
* - Codes with lowercase letters are rejected (Stellar asset codes are case-sensitive
* on-chain and lowercase codes cannot participate in DEX pools).
*
* @param code - The asset code to validate.
* @returns A result indicating DEX compatibility and the resolved variant.
*/
export function validateAssetCodeDexCompatibility(code: unknown): DexCompatibilityResult {
// Reuse existing format validation first.
const formatResult = validateAssetCode(code);
if (!formatResult.valid) {
return {
compatible: false,
error: {
field: formatResult.error!.field,
message: formatResult.error!.message,
code: formatResult.error!.code,
},
};
}

const assetCode = code as string;

// DEX pools require uppercase-only codes.
if (assetCode !== assetCode.toUpperCase()) {
return {
compatible: false,
error: {
field: 'stellar.asset.code',
message: `Asset code "${assetCode}" contains lowercase characters. ` +
'DEX liquidity pools require uppercase alphanumeric codes only.',
code: 'DEX_INCOMPATIBLE_CHARSET',
},
};
}

// Codes longer than 12 characters cannot be represented in either alphanum type.
if (assetCode.length > 12) {
return {
compatible: false,
error: {
field: 'stellar.asset.code',
message: `Asset code "${assetCode}" exceeds 12 characters and cannot participate in DEX liquidity pools.`,
code: 'DEX_INCOMPATIBLE_CODE_LENGTH',
},
};
}

const variant = resolveAssetVariant(assetCode);

return { compatible: true, variant: variant ?? undefined };
}
57 changes: 56 additions & 1 deletion docs/migration-procedures.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Template Version Migration Procedures

This document outlines the standard procedures for migrating existing deployments to new template versions within the CRAFT platform.
This document outlines the standard procedures for migrating existing deployments to new template versions within the CRAFT platform, and for promoting Soroban contracts from testnet to mainnet.

## Overview

Expand Down Expand Up @@ -56,3 +56,58 @@ If any step in the migration workflow fails:
- **Test with Real Data**: Always test migrations using a copy of real deployment data.
- **Backward Compatibility**: New template versions should aim to be backward compatible with previous configurations.
- **Minimal Downtime**: Aim for zero-downtime migrations by leveraging Vercel's deployment previews.

---

## Soroban Contract Migration: Testnet → Mainnet (#617)

Promoting a Soroban contract from testnet to mainnet is a **high-risk, irreversible operation**. The procedure below enforces safety checks at every step.

### Overview

The `migrateSorobanContract` function in `packages/stellar/src/soroban-migration.ts` implements this flow:

1. **Validate config** – reject any testnet-only parameters before touching mainnet.
2. **Require explicit confirmation** – the caller must pass `{ confirm: true }` to proceed.
3. **Verify network passphrase** – the transaction must be signed for the mainnet passphrase.
4. **Deploy to mainnet** – only after all checks pass.

### Testnet-Only Parameters (Rejected on Mainnet)

The following configuration values are rejected when the target network is `mainnet`:

| Parameter | Testnet value | Reason |
|---|---|---|
| `networkPassphrase` | `Test SDF Network ; September 2015` | Wrong network |
| `horizonUrl` | `https://horizon-testnet.stellar.org` | Wrong endpoint |
| `sorobanRpcUrl` | `https://soroban-testnet.stellar.org` | Wrong endpoint |

### Usage

```typescript
import { migrateSorobanContract } from '@craft/stellar';

const result = await migrateSorobanContract({
wasmBinary,
sourcePublicKey,
config: {
network: 'mainnet',
horizonUrl: 'https://horizon.stellar.org',
networkPassphrase: Networks.PUBLIC,
sorobanRpcUrl: 'https://soroban-mainnet.stellar.org',
},
confirm: true, // explicit opt-in required
});

if (!result.ok) {
console.error('Migration rejected:', result.error);
}
```

### Safety Rules

- **Never** reuse a testnet keypair on mainnet without rotating secrets.
- **Always** run a dry-run simulation on testnet before promoting.
- **Verify** the contract WASM hash matches the audited binary before mainnet deployment.
- Mainnet promotion requires `confirm: true`; omitting it returns an error without touching the network.

2 changes: 2 additions & 0 deletions packages/stellar/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ export * from './config';
export * from './mock';
export * from './errors';
export * from './soroban';
export * from './soroban-migration';
export * from './soroban-event-relay';
export * from './trustline-validation';
Loading
Loading