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
96 changes: 49 additions & 47 deletions PR_DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -1,89 +1,91 @@
## Summary

This PR resolves three independently implementable issues across UI Kit components, Core SDK documentation and runnable examples, and cross-package testing fixtures.
This PR resolves four independently implementable issues across relayer tests, indexer persistence, extension security, and the Stellar SDK.

- **UI Kit Button Loading State (#630)**: Added `loading` prop, Loader2 spinner, `aria-busy` attribute, and click prevention to the UI Kit `Button` component, alongside exhaustive tests and stories.
- **Core SDK Documentation & CI Verification (#587)**: Expanded `packages/core-sdk/README.md` to accurately document all `AncoreClient` methods and standalone exports, added a fully runnable session key lifecycle example, and wrote a CI script to enforce documentation compliance.
- **Canonical Cross-Package Relay Payload Test (#687)**: Scaffolded a shared `@ancore/test-fixtures` package with a JSON payload schema, linked it to the workspace, and integrated cross-package unit tests in both `services/relayer` and `packages/account-abstraction` to assert structure alignment.

---
- Adds missing `/relay/validate` auth integration coverage with supertest
- Replaces the in-memory ingest checkpoint stub with a durable Postgres-backed repository
- Throttles failed wallet unlock attempts in the extension service worker using session-scoped exponential backoff
- Introduces a multi-network `createStellarClient` factory with `futurenet` support

## Purpose / Motivation

- **#630**: Interactive elements should prevent double-submissions, visually reflect pending operations via animations, and remain fully accessible (`aria-busy`).
- **#587**: Prevent API drifts in `core-sdk` by explicitly documenting its modular exports and providing realistic developer-facing integration examples.
- **#687**: Establish type and contract alignment between relayer execution payloads and smart account-abstraction parsing via shared testing fixtures.

---
Relayer validation lacked parity with execute-route auth testing, ingest cursors were lost on restart, the extension allowed unlimited password guessing, and Stellar client creation required manual network configuration. These changes close those gaps while keeping each fix scoped to its own module.

## Changes Made

### #630UI Kit Button Loading State
### #654Relayer integration tests

- Modified `packages/ui-kit/src/components/ui/button.tsx` to accept a `loading` prop.
- Added a spinning `Loader2` icon from `lucide-react` when loading is active, applied `aria-busy` for screen readers, and disabled event emission to prevent duplicate form submissions or actions.
- Added comprehensive unit tests in `packages/ui-kit/src/components/ui/button.test.tsx` verifying spinner rendering, disabled attribute application, and click event suppression.
- Added new stories (`Loading`, `DestructiveLoading`) in `packages/ui-kit/src/components/ui/button.stories.tsx` for manual UI verification.
- Extended `relay.test.ts` with a missing-auth `401` case for `POST /relay/validate`
- Existing invalid-body `400` coverage and CI relayer test job remain unchanged

### #587Core SDK Documentation & CI Verification
### #670Postgres checkpoint repository

- Updated `packages/core-sdk/README.md` with accurate mappings of `AncoreClient` methods and all 100+ modular package exports (wallet helpers, payments, storage managers, etc.).
- Created a fully executable developer integration walkthrough in `packages/core-sdk/examples/01-session-key-lifecycle.ts` using `@stellar/stellar-sdk` and `@ancore/core-sdk`.
- Fixed a contract-derivation bug in `deriveContractId` that constructed invalid 58-character contract IDs, changing it to use `StrKey` encoding to produce valid 56-character Stellar/Soroban contract addresses.
- Created `scripts/verify-readme-exports.ts` to automatically scan `index.ts` exports and fail the build if any export is not fully cataloged in the README.
- Added `CheckpointStore` trait with `PostgresCheckpointStore` and async `MemoryCheckpointStore`
- Genericized `IngestWorker` to accept any checkpoint store implementation
- Wired checkpoint loading into indexer startup in `main.rs`
- Added ignored Postgres integration tests and documented `ingest_checkpoints` schema in README

### #687Canonical Cross-Package Relay Payload Test
### #678Unlock rate limiting

- Scaffolded `@ancore/test-fixtures` package in `packages/test-fixtures/` with a standard `relay-payload-v1.json` schema.
- Added the package to the root workspace and declared it as a dependency in both `packages/account-abstraction` and `services/relayer`.
- Added unit tests in `packages/account-abstraction/src/__tests__/relay-payload.test.ts` and `services/relayer/tests/unit/relay-payload.test.ts` importing the shared JSON fixture and validating schema parity.
- Added `unlock-rate-limit.ts` with session-scoped failure tracking and exponential backoff
- Updated `UNLOCK_WALLET` handler to return `retryAfterMs` and a user-visible `message` during lockout
- Added fake-timer unit tests for rate-limit logic and service-worker behavior

---
### #681 — Stellar client factory

- Added `futurenet` to shared `Network` type and Stellar network config
- Exported `createStellarClient(network)` and `NetworkId` from `@ancore/stellar`
- Invalid networks now throw `NetworkError` at construction time

## How to Test

### UI Kit Button (#630)
### Relayer (#654)

```bash
npx pnpm --filter @ancore/ui-kit test
cd services/relayer
pnpm test tests/integration/relay.test.ts
```

Expected: All 146 tests pass successfully, including all loading-state assertions.
Expected: validate-route tests pass, including `401 when Authorization header is missing`.

### Core SDK & Verification (#587)
### Indexer (#670)

```bash
# Run unit tests
npx pnpm --filter @ancore/core-sdk test
cd services/indexer
cargo test --lib
# Optional with Postgres:
TEST_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/ancore_test cargo test postgres_checkpoint -- --ignored
```

Expected: unit tests pass; ignored integration tests persist and reload checkpoints across restart.

# Run runnable lifecycle example
npx tsx packages/core-sdk/examples/01-session-key-lifecycle.ts
### Extension wallet (#678)

# Run README compliance check
npx tsx scripts/verify-readme-exports.ts
```bash
cd apps/extension-wallet
pnpm test src/background/__tests__/unlock-rate-limit.test.ts
pnpm test src/background/__tests__/service-worker.test.ts
```

Expected: All tests pass, lifecycle example completes successfully, and verify-readme-exports confirms 100% API coverage.
Expected: lockout after repeated failures, success resets counter, expired lockout allows retry.

### Cross-Package Relay Payload (#687)
### Stellar SDK (#681)

```bash
# Run account abstraction tests
npx pnpm --filter @ancore/account-abstraction test

# Run relayer tests
npx pnpm --filter @ancore/relayer test
cd packages/stellar
pnpm test src/__tests__/client.test.ts
```

Expected: All tests pass, ensuring complete canonical payload parity between relayer ingestion and core execution layers.
Expected: factory tests pass for testnet/mainnet/futurenet; invalid network throws.

---

## Related Issues

- Closes ancore-org/ancore#630
- Closes ancore-org/ancore#587
- Closes ancore-org/ancore#687
- Closes ancore-org/ancore#654
- Closes ancore-org/ancore#670
- Closes ancore-org/ancore#678
- Closes ancore-org/ancore#681

---

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,15 +129,15 @@ pnpm contracts:test

### Updating WASM Size Budgets

WASM contract sizes are monitored in CI to prevent regression. The budget for each contract is defined in `contracts/budgets/wasm-budgets.json`.
WASM contract sizes are monitored in CI to prevent regression. The budget for each contract is defined in `contracts/budgets/wasm-budgets.json`.

If your changes intentionally increase the contract size beyond the current budget:

1. Ensure your contract builds locally: `pnpm contracts:build`
2. Check the new size of the optimized `.wasm` files in `contracts/target/wasm32-unknown-unknown/release/`.
3. You can run the local size check with: `node scripts/check-wasm-size.js`
4. Update `contracts/budgets/wasm-budgets.json` with the new size budget, and commit the changes.


## Contributing

We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,7 @@ function makeAuthState(overrides: Partial<AuthState> = {}): AuthState {
};
}

async function loadServiceWorker(
authState: AuthState,
options: { unlockReturns?: boolean } = {}
) {
async function loadServiceWorker(authState: AuthState, options: { unlockReturns?: boolean } = {}) {
vi.doMock('@/router/AuthGuard', () => ({
readAuthState: vi.fn(() => authState),
DEFAULT_AUTH_STATE: makeAuthState(),
Expand Down Expand Up @@ -462,15 +459,16 @@ describe('UNLOCK_WALLET', () => {
}

unlockResult = true;
const successResp = await dispatch(chromeMock, 'UNLOCK_WALLET', { password: 'correct-password' });
const successResp = await dispatch(chromeMock, 'UNLOCK_WALLET', {
password: 'correct-password',
});
expect((successResp.payload as any).success).toBe(true);

unlockResult = false;
for (let i = 0; i < 4; i += 1) {
await dispatch(chromeMock, 'UNLOCK_WALLET', { password: 'wrong-password' });
const resp = await dispatch(chromeMock, 'UNLOCK_WALLET', { password: 'wrong-password' });
expect((resp.payload as any).retryAfterMs).toBeUndefined();
}
const resp = await dispatch(chromeMock, 'UNLOCK_WALLET', { password: 'wrong-password' });
expect((resp.payload as any).retryAfterMs).toBeUndefined();
_resetHandlers();
});

Expand Down
122 changes: 53 additions & 69 deletions apps/extension-wallet/src/components/TransferNoteInput.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import React from 'react';
import {
validateTransferNote,
getRemainingCharacters,
MAX_NOTE_LENGTH,
} from '@/utils/note-validation';
import { getRemainingCharacters, MAX_NOTE_LENGTH } from '@/utils/note-validation';
import { Field } from '@ancore/ui-kit';

interface TransferNoteInputProps {
value: string;
onChange: (value: string) => void;
error?: string;
className?: string;
label?: string;
placeholder?: string;
disabled?: boolean;
required?: boolean;
Expand All @@ -29,6 +27,7 @@ export function TransferNoteInput({
onChange,
error,
className = '',
label = 'Note',
placeholder = 'Add a note (optional)',
disabled = false,
required = false,
Expand All @@ -52,83 +51,68 @@ export function TransferNoteInput({
}
};

const warning = isOverLimit && !error && (
<div className="flex items-center gap-2 text-amber-400 text-[10px] font-medium">
<svg
xmlns="http://www.w3.org/2000/svg"
className="w-3 h-3"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
/>
</svg>
Note exceeds character limit and will be truncated
</div>
);

return (
<div className={`space-y-2 ${className}`}>
<div className="relative">
<textarea
value={value}
onChange={handleChange}
onBlur={handleBlur}
placeholder={placeholder}
disabled={disabled}
required={required}
className={`
<Field label={label} error={error} required={required} className={className}>
{({ controlProps }) => (
<>
<div className="relative">
<textarea
{...controlProps}
value={value}
onChange={handleChange}
onBlur={handleBlur}
placeholder={placeholder}
disabled={disabled}
required={required}
className={`
w-full bg-white/5 border border-white/10 rounded-2xl px-5 py-4
text-white placeholder:text-slate-600 resize-none
focus:border-cyan-400 focus:outline-none transition-all
disabled:opacity-50 disabled:cursor-not-allowed
${error ? 'border-red-400 focus:border-red-400' : ''}
${isOverLimit ? 'border-red-400 focus:border-red-400' : ''}
`}
rows={3}
maxLength={MAX_NOTE_LENGTH + 10} // Allow slight over-typing for better UX
/>
rows={3}
maxLength={MAX_NOTE_LENGTH + 10} // Allow slight over-typing for better UX
/>

{/* Character counter */}
<div className="absolute bottom-3 right-3 flex items-center gap-1">
<span
className={`
{/* Character counter */}
<div className="absolute bottom-3 right-3 flex items-center gap-1">
<span
className={`
text-[10px] font-mono font-medium transition-colors
${isOverLimit ? 'text-red-400' : isNearLimit ? 'text-amber-400' : 'text-slate-600'}
`}
>
{Math.abs(remainingChars)}
</span>
<span className="text-[8px] text-slate-600 font-medium">/{MAX_NOTE_LENGTH}</span>
</div>
</div>
>
{Math.abs(remainingChars)}
</span>
<span className="text-[8px] text-slate-600 font-medium">/{MAX_NOTE_LENGTH}</span>
</div>
</div>

{/* Error message */}
{error && (
<div className="flex items-center gap-2 text-red-400 text-[10px] font-medium">
<svg
xmlns="http://www.w3.org/2000/svg"
className="w-3 h-3"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
{error}
</div>
{warning}
</>
)}

{/* Character limit warning */}
{isOverLimit && !error && (
<div className="flex items-center gap-2 text-amber-400 text-[10px] font-medium">
<svg
xmlns="http://www.w3.org/2000/svg"
className="w-3 h-3"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
/>
</svg>
Note exceeds character limit and will be truncated
</div>
)}
</div>
</Field>
);
}
5 changes: 1 addition & 4 deletions apps/extension-wallet/src/config/__tests__/urls.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,7 @@ describe('probeServiceHealth', () => {
vi.mocked(fetch).mockResolvedValueOnce(new Response('{}', { status: 200 }));

await probeServiceHealth('https://relayer.ancore.io/', 'relayer');
expect(fetch).toHaveBeenCalledWith(
'https://relayer.ancore.io/health',
expect.anything()
);
expect(fetch).toHaveBeenCalledWith('https://relayer.ancore.io/health', expect.anything());
});
});

Expand Down
7 changes: 4 additions & 3 deletions apps/extension-wallet/src/config/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,10 @@ export interface ConfigError {
// ---------------------------------------------------------------------------

const RELAYER_URLS: Record<string, string> = {
production: typeof import.meta !== 'undefined' && import.meta.env?.VITE_RELAYER_URL
? import.meta.env.VITE_RELAYER_URL
: 'https://relayer.ancore.io',
production:
typeof import.meta !== 'undefined' && import.meta.env?.VITE_RELAYER_URL
? import.meta.env.VITE_RELAYER_URL
: 'https://relayer.ancore.io',
staging: 'https://relayer-staging.ancore.io',
local: 'http://localhost:3000',
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ describe('validateSchedule', () => {
});

it('accepts a valid schedule', () => {
const startAt = new Date(Date.now() + 2 * 60 * 60 * 1000).toISOString().slice(0, 16);
const future = new Date(Date.now() + 2 * 60 * 60 * 1000);
// datetime-local expects local time without a timezone suffix; toISOString() emits UTC.
const offsetMs = future.getTimezoneOffset() * 60 * 1000;
const startAt = new Date(future.getTime() - offsetMs).toISOString().slice(0, 16);

expect(
validateSchedule({
frequency: 'weekly',
Expand Down
Loading