Skip to content
Open
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
85 changes: 77 additions & 8 deletions scripts/smoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- XDR submit-only: signs a small self-payment and submits via fee bump
- func+auth (no auth): calls smoke-contract `no_auth_bump(42)` using a channel account
- func+auth (address auth): calls `write_with_address_auth(addr, 777)` and signs auth entries
- mgmt-api: tests management API (getFeeUsage, getFeeLimit, setFeeLimit, deleteFeeLimit)

Prerequisites
- Node.js 18+ (global fetch available)
Expand Down Expand Up @@ -51,7 +52,7 @@
--network (NETWORK) default: testnet | also supports: mainnet
--rpc-url (RPC_URL) default: https://soroban-testnet.stellar.org
--test-id (TEST_ID) optional: run only one test
options: xdr-payment, func-auth-no-auth, func-auth-address-auth
options: xdr-payment, func-auth-no-auth, func-auth-address-auth, mgmt-api
--concurrency (CONCURRENCY) optional: parallel requests per test (default: 1)
--debug optional: print full plugin response with logs/traces
--api-key-header (API_KEY_HEADER) optional: header name for API key in relayer mode (default: x-api-key)
Expand Down Expand Up @@ -113,7 +114,7 @@
const url = `${baseUrl}/api/v1/health`;
const res = await fetch(url, {
headers: { Authorization: `Bearer ${apiKey}` },
} as any).catch((e: any) => ({ ok: false, statusText: e?.message || String(e) }) as any);

Check warning on line 117 in scripts/smoke.ts

View workflow job for this annotation

GitHub Actions / Run build & test

Unexpected any. Specify a different type
if (!res.ok) throw new Error(`Relayer health check failed: ${res.status} ${res.statusText}`);
}

Expand Down Expand Up @@ -154,14 +155,21 @@
const baseUrl = String(args['base-url'] || process.env.BASE_URL || (pluginId ? 'http://localhost:8080' : ''));
const network = String(args.network || process.env.NETWORK || 'testnet').toLowerCase() as 'testnet' | 'mainnet';
const passphrase = np(network);
const rpcUrl = String(args['rpc-url'] || process.env.RPC_URL || 'https://soroban-testnet.stellar.org');
const defaultRpcUrl =
network === 'mainnet'
? 'https://soroban-rpc.mainnet.stellar.gateway.fm'
: 'https://soroban-testnet.stellar.org';
const rpcUrl = String(args['rpc-url'] || process.env.RPC_URL || defaultRpcUrl);
const accountName = String(args['account-name'] || process.env.ACCOUNT_NAME || 'test-account');
const testId = (args['test-id'] || process.env.TEST_ID) as string | undefined;
const debug = Boolean(args['debug'] || process.env.DEBUG);
const concurrency = parseInt(String(args['concurrency'] || process.env.CONCURRENCY || '1'), 10);
const contractId = String(
args['contract-id'] || process.env.CONTRACT_ID || 'CD3P6XI7YI6ATY5RM2CNXHRRT3LBGPC3WGR2D2OE6EQNVLVEA5HGUELG'
);
// Default contract IDs per network
const defaultContractId =
network === 'mainnet'
? 'CCP6S7LOAIWYZKFZQHVT44GAB7LDCXZEEZRAXVKCN4NWAWB4LEOXHWG4' // mainnet smoke contract
: 'CD3P6XI7YI6ATY5RM2CNXHRRT3LBGPC3WGR2D2OE6EQNVLVEA5HGUELG'; // testnet smoke contract
const contractId = String(args['contract-id'] || process.env.CONTRACT_ID || defaultContractId);

// Fee tracking flags
const apiKeyHeader = (args['api-key-header'] || process.env.API_KEY_HEADER || 'x-api-key') as string;
Expand Down Expand Up @@ -200,8 +208,10 @@
address: string;
contractId: string;
debug: boolean;
apiKey: string;
adminSecret?: string;
};
const ctx: Ctx = { client, rpc: rpcServer, passphrase, keypair, address, contractId, debug };
const ctx: Ctx = { client, rpc: rpcServer, passphrase, keypair, address, contractId, debug, apiKey, adminSecret };

const TESTS: { id: string; label: string; run: (ctx: Ctx) => Promise<void> }[] = [
{
Expand Down Expand Up @@ -246,6 +256,59 @@
printResult('func-auth-address-auth', { success: true, data: res }, debug);
},
},
{
id: 'mgmt-api',
label: 'Management API: fee limits CRUD',
run: async ({ client, apiKey, adminSecret, debug }) => {
if (!adminSecret) {
console.log(' ⏭ Skipped (requires --admin-secret)');
return;
}

const formatStroops = (s: number) => `${s.toLocaleString()} stroops (${(s / 10_000_000).toFixed(7)} XLM)`;

// 1. Get current fee usage
const usage = await client.getFeeUsage(apiKey);
printResult('getFeeUsage', { success: true, data: usage }, debug);
if (!debug) console.log(` consumed: ${formatStroops(usage.consumed)}`);

// 2. Get current fee limit
const limitBefore = await client.getFeeLimit(apiKey);
printResult('getFeeLimit', { success: true, data: limitBefore }, debug);
if (!debug) console.log(` limit: ${limitBefore.limit ? formatStroops(limitBefore.limit) : 'unlimited'}`);

// 3. Set a custom fee limit (5 XLM)
const testLimit = 50_000_000;
const setResult = await client.setFeeLimit(apiKey, testLimit);
if (!setResult.ok || setResult.limit !== testLimit) {
throw new Error(`setFeeLimit failed: expected ${testLimit}, got ${setResult.limit}`);
}
printResult('setFeeLimit', { success: true, data: setResult }, debug);
if (!debug) console.log(` limit: ${formatStroops(setResult.limit)}`);

// 4. Verify limit was set
const limitAfterSet = await client.getFeeLimit(apiKey);
if (limitAfterSet.limit !== testLimit) {
throw new Error(`getFeeLimit after set: expected ${testLimit}, got ${limitAfterSet.limit}`);
}
printResult('getFeeLimit (verify set)', { success: true, data: limitAfterSet }, debug);

// 5. Delete the custom limit
const deleteResult = await client.deleteFeeLimit(apiKey);
if (!deleteResult.ok) {
throw new Error('deleteFeeLimit failed');
}
printResult('deleteFeeLimit', { success: true, data: deleteResult }, debug);

// 6. Verify limit was deleted
const limitAfterDelete = await client.getFeeLimit(apiKey);
if (limitAfterDelete.limit === testLimit) {
throw new Error('getFeeLimit after delete: custom limit still present');
}
printResult('getFeeLimit (verify deleted)', { success: true, data: limitAfterDelete }, debug);
if (!debug) console.log(` limit: ${limitAfterDelete.limit ? formatStroops(limitAfterDelete.limit) : 'unlimited'}`);
},
},
Comment on lines +259 to +311
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Save and restore initial state to avoid side effects.

The management API test modifies state (sets and deletes fee limits) without restoring the original state. If a custom limit exists before the test runs, it will be lost after the test completes. This can affect subsequent tests or ongoing operations.

Additionally, the verification at lines 305-307 only checks that the test limit was removed, not that the original state was restored.

🔎 Apply this diff to save and restore the initial state:
     run: async ({ client, apiKey, adminSecret, debug }) => {
       if (!adminSecret) {
         console.log('   ⏭ Skipped (requires --admin-secret)');
         return;
       }

       const formatStroops = (s: number) => `${s.toLocaleString()} stroops (${(s / 10_000_000).toFixed(7)} XLM)`;

       // 1. Get current fee usage
       const usage = await client.getFeeUsage(apiKey);
       printResult('getFeeUsage', { success: true, data: usage }, debug);
       if (!debug) console.log(`      consumed: ${formatStroops(usage.consumed)}`);

       // 2. Get current fee limit (save for restoration)
       const limitBefore = await client.getFeeLimit(apiKey);
       printResult('getFeeLimit', { success: true, data: limitBefore }, debug);
       if (!debug) console.log(`      limit: ${limitBefore.limit ? formatStroops(limitBefore.limit) : 'unlimited'}`);
+      const originalLimit = limitBefore.limit;

       // 3. Set a custom fee limit (5 XLM)
       const testLimit = 50_000_000;
       const setResult = await client.setFeeLimit(apiKey, testLimit);
       if (!setResult.ok || setResult.limit !== testLimit) {
         throw new Error(`setFeeLimit failed: expected ${testLimit}, got ${setResult.limit}`);
       }
       printResult('setFeeLimit', { success: true, data: setResult }, debug);
       if (!debug) console.log(`      limit: ${formatStroops(setResult.limit)}`);

       // 4. Verify limit was set
       const limitAfterSet = await client.getFeeLimit(apiKey);
       if (limitAfterSet.limit !== testLimit) {
         throw new Error(`getFeeLimit after set: expected ${testLimit}, got ${limitAfterSet.limit}`);
       }
       printResult('getFeeLimit (verify set)', { success: true, data: limitAfterSet }, debug);

       // 5. Delete the custom limit
       const deleteResult = await client.deleteFeeLimit(apiKey);
       if (!deleteResult.ok) {
         throw new Error('deleteFeeLimit failed');
       }
       printResult('deleteFeeLimit', { success: true, data: deleteResult }, debug);

       // 6. Verify limit was deleted
       const limitAfterDelete = await client.getFeeLimit(apiKey);
       if (limitAfterDelete.limit === testLimit) {
         throw new Error('getFeeLimit after delete: custom limit still present');
       }
       printResult('getFeeLimit (verify deleted)', { success: true, data: limitAfterDelete }, debug);
       if (!debug) console.log(`      limit: ${limitAfterDelete.limit ? formatStroops(limitAfterDelete.limit) : 'unlimited'}`);
+
+      // 7. Restore original state
+      if (originalLimit !== undefined && originalLimit !== null) {
+        const restoreResult = await client.setFeeLimit(apiKey, originalLimit);
+        if (!restoreResult.ok || restoreResult.limit !== originalLimit) {
+          console.warn(`      ⚠ Failed to restore original limit: expected ${originalLimit}, got ${restoreResult.limit}`);
+        } else {
+          if (!debug) console.log(`      restored original limit: ${formatStroops(originalLimit)}`);
+        }
+      }
     },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{
id: 'mgmt-api',
label: 'Management API: fee limits CRUD',
run: async ({ client, apiKey, adminSecret, debug }) => {
if (!adminSecret) {
console.log(' ⏭ Skipped (requires --admin-secret)');
return;
}
const formatStroops = (s: number) => `${s.toLocaleString()} stroops (${(s / 10_000_000).toFixed(7)} XLM)`;
// 1. Get current fee usage
const usage = await client.getFeeUsage(apiKey);
printResult('getFeeUsage', { success: true, data: usage }, debug);
if (!debug) console.log(` consumed: ${formatStroops(usage.consumed)}`);
// 2. Get current fee limit
const limitBefore = await client.getFeeLimit(apiKey);
printResult('getFeeLimit', { success: true, data: limitBefore }, debug);
if (!debug) console.log(` limit: ${limitBefore.limit ? formatStroops(limitBefore.limit) : 'unlimited'}`);
// 3. Set a custom fee limit (5 XLM)
const testLimit = 50_000_000;
const setResult = await client.setFeeLimit(apiKey, testLimit);
if (!setResult.ok || setResult.limit !== testLimit) {
throw new Error(`setFeeLimit failed: expected ${testLimit}, got ${setResult.limit}`);
}
printResult('setFeeLimit', { success: true, data: setResult }, debug);
if (!debug) console.log(` limit: ${formatStroops(setResult.limit)}`);
// 4. Verify limit was set
const limitAfterSet = await client.getFeeLimit(apiKey);
if (limitAfterSet.limit !== testLimit) {
throw new Error(`getFeeLimit after set: expected ${testLimit}, got ${limitAfterSet.limit}`);
}
printResult('getFeeLimit (verify set)', { success: true, data: limitAfterSet }, debug);
// 5. Delete the custom limit
const deleteResult = await client.deleteFeeLimit(apiKey);
if (!deleteResult.ok) {
throw new Error('deleteFeeLimit failed');
}
printResult('deleteFeeLimit', { success: true, data: deleteResult }, debug);
// 6. Verify limit was deleted
const limitAfterDelete = await client.getFeeLimit(apiKey);
if (limitAfterDelete.limit === testLimit) {
throw new Error('getFeeLimit after delete: custom limit still present');
}
printResult('getFeeLimit (verify deleted)', { success: true, data: limitAfterDelete }, debug);
if (!debug) console.log(` limit: ${limitAfterDelete.limit ? formatStroops(limitAfterDelete.limit) : 'unlimited'}`);
},
},
{
id: 'mgmt-api',
label: 'Management API: fee limits CRUD',
run: async ({ client, apiKey, adminSecret, debug }) => {
if (!adminSecret) {
console.log(' ⏭ Skipped (requires --admin-secret)');
return;
}
const formatStroops = (s: number) => `${s.toLocaleString()} stroops (${(s / 10_000_000).toFixed(7)} XLM)`;
// 1. Get current fee usage
const usage = await client.getFeeUsage(apiKey);
printResult('getFeeUsage', { success: true, data: usage }, debug);
if (!debug) console.log(` consumed: ${formatStroops(usage.consumed)}`);
// 2. Get current fee limit (save for restoration)
const limitBefore = await client.getFeeLimit(apiKey);
printResult('getFeeLimit', { success: true, data: limitBefore }, debug);
if (!debug) console.log(` limit: ${limitBefore.limit ? formatStroops(limitBefore.limit) : 'unlimited'}`);
const originalLimit = limitBefore.limit;
// 3. Set a custom fee limit (5 XLM)
const testLimit = 50_000_000;
const setResult = await client.setFeeLimit(apiKey, testLimit);
if (!setResult.ok || setResult.limit !== testLimit) {
throw new Error(`setFeeLimit failed: expected ${testLimit}, got ${setResult.limit}`);
}
printResult('setFeeLimit', { success: true, data: setResult }, debug);
if (!debug) console.log(` limit: ${formatStroops(setResult.limit)}`);
// 4. Verify limit was set
const limitAfterSet = await client.getFeeLimit(apiKey);
if (limitAfterSet.limit !== testLimit) {
throw new Error(`getFeeLimit after set: expected ${testLimit}, got ${limitAfterSet.limit}`);
}
printResult('getFeeLimit (verify set)', { success: true, data: limitAfterSet }, debug);
// 5. Delete the custom limit
const deleteResult = await client.deleteFeeLimit(apiKey);
if (!deleteResult.ok) {
throw new Error('deleteFeeLimit failed');
}
printResult('deleteFeeLimit', { success: true, data: deleteResult }, debug);
// 6. Verify limit was deleted
const limitAfterDelete = await client.getFeeLimit(apiKey);
if (limitAfterDelete.limit === testLimit) {
throw new Error('getFeeLimit after delete: custom limit still present');
}
printResult('getFeeLimit (verify deleted)', { success: true, data: limitAfterDelete }, debug);
if (!debug) console.log(` limit: ${limitAfterDelete.limit ? formatStroops(limitAfterDelete.limit) : 'unlimited'}`);
// 7. Restore original state
if (originalLimit !== undefined && originalLimit !== null) {
const restoreResult = await client.setFeeLimit(apiKey, originalLimit);
if (!restoreResult.ok || restoreResult.limit !== originalLimit) {
console.warn(` ⚠ Failed to restore original limit: expected ${originalLimit}, got ${restoreResult.limit}`);
} else {
if (!debug) console.log(` restored original limit: ${formatStroops(originalLimit)}`);
}
}
},
},
🤖 Prompt for AI Agents
In scripts/smoke.ts around lines 259 to 311, the test mutates the fee limit
without restoring the original value; capture the initial limit (and whether it
was "unlimited") before changing anything, run the set/delete/verify steps
inside a try block, and in a finally block restore the original state: if there
was an original numeric limit re-apply it, otherwise ensure the custom limit is
deleted; update the final verification to assert that the post-test state
matches the captured original value rather than only checking that the test
value was removed.

];

const selected = testId ? TESTS.filter((t) => t.id === testId) : TESTS;
Expand Down Expand Up @@ -354,18 +417,24 @@

function printResult(label: string, envelope: any, debug: boolean) {
if (debug) {
// Pretty print the full envelope including data, metadata (logs/traces)
console.log(` ${label}:`);
console.log(JSON.stringify(envelope, null, 2));
return;
}

const data = envelope?.data || envelope?.result || {};
const hash = data?.hash;
const status = data?.status;
const ok = data?.ok;
const success = envelope?.success;

if (success) {
console.log(` ✓ ${label}: ${hash || status || 'confirmed'}`);
// For tx results, show hash; for mgmt results, show ok or just confirmed
const detail = hash || status || (ok !== undefined ? `ok=${ok}` : 'ok');
console.log(` ${label}: ${detail}`);
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The checkmark symbol (✓) has been removed from the success output. This changes the visual feedback users receive when tests pass successfully. The checkmark provided a clear, at-a-glance indication of success. Consider retaining the checkmark prefix for consistency with the original UX.

Suggested change
console.log(` ${label}: ${detail}`);
console.log(` ${label}: ${detail}`);

Copilot uses AI. Check for mistakes.
} else {
const error = envelope?.error || 'unknown error';
console.log(` ${label}: ${error}`);
console.log(` ${label}: FAILED - ${error}`);
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error output format has been changed from "✗" to no symbol and from just the error to "FAILED - " prefix. While this might be intentional, the removal of the "✗" symbol makes it less visually distinct from success messages. Consider retaining the "✗" symbol for better visual distinction between success and failure cases.

Suggested change
console.log(` ${label}: FAILED - ${error}`);
console.log(` ${label}: FAILED - ${error}`);

Copilot uses AI. Check for mistakes.
}
}
Loading