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
3 changes: 2 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ See [docs/payment-period-conduct.md](docs/payment-period-conduct.md) for the ful

## Updating API Reference Documentation

When you add or change a public method on `ComplianceModule`, `AssetModule`, `InvestorModule`, `EventsModule`, or an exported utility/type, update `docs/api-reference.md` (and `docs/investor-portfolio.md` if the investor read model changes; `docs/contract-events.md` if event decoding changes). Review checklist:
When you add or change a public method on `ComplianceModule`, `AssetModule`, `InvestorModule`, `EventsModule`, or an exported utility/type, update `docs/api-reference.md` (and `docs/compliance-batch-queries.md` if compliance batching changes; `docs/investor-portfolio.md` if the investor read model changes; `docs/contract-events.md` if event decoding changes). Review checklist:

- [ ] The signature block matches the method's actual TypeScript signature (parameter names, types, return type).
- [ ] The Parameters section lists every parameter, including optional ones and their defaults.
Expand All @@ -54,4 +54,5 @@ When you add or change a public method on `ComplianceModule`, `AssetModule`, `In
- [ ] The example uses only placeholder keys/addresses (`G...`, `C...`, `S...`) — never a real secret key or mainnet contract ID.
- [ ] Anything the source leaves ambiguous, incomplete, or marked with a `// TODO` is called out as an explicit note rather than assumed or omitted.
- [ ] If the change affects compliance/whitelist-gated behavior, the compliance disclaimer at the top of `docs/api-reference.md` still accurately describes it.
- [ ] If the change affects compliance batching, follow the checklist in `docs/compliance-batch-queries.md` (partial failures, concurrency/rate limits, and address-free diagnostics).
- [ ] If the change affects contract event decoding, follow the checklist in `docs/contract-events.md` (edge cases, unknown fallback, and security/compliance assumptions).
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,27 @@ const aegis = new AegisClient({
});
```

## Batch Compliance Queries

Check an admin compliance table without unbounded RPC fan-out or all-or-nothing
failure:

```typescript
const batch = await aegis.compliance.checkWhitelistBatch(
['G_INVESTOR_ONE', 'G_INVESTOR_TWO', 'invalid-input'],
{ concurrency: 4 },
);

for (const item of batch.items) {
console.log(item.index, item.status, item.isWhitelisted);
}
```

Results preserve input order, invalid addresses stay per-item, and query failures
carry safe diagnostics. The SDK deduplicates by default and does not retry
automatically during rate limiting. See
[Compliance Batch Queries](./docs/compliance-batch-queries.md).

## Role Discovery & Capability Checks
Check what an address is classified as, and what it can currently attempt through the SDK.
This is a client-side convenience for UI gating, not on-chain authorization — see the
Expand Down
4 changes: 4 additions & 0 deletions docs/acceptance-criteria-traceability.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ Below is the complete map of every public SDK module. Reference this when comple
|---|---|---|---|
| Client | `src/client.ts` | `AegisClient` | Top-level SDK client; initializes RPC, configures modules, provides `runNetworkOperation()` |
| Compliance | `src/compliance.ts` | `ComplianceModule` | Queries the Aegis Soroban contract for KYC/whitelist status |
| Compliance Batch | `src/compliance/batch.ts` | `ComplianceModule.checkWhitelistBatch` | Validates and queries multiple whitelist states with bounded concurrency and partial-failure isolation |
| Compliance Diagnostics | `src/diagnostics/compliance.ts` | `buildComplianceBatchDiagnostic` | Builds address-free batch failure roll-ups |
| Asset | `src/asset.ts` | `AssetModule` | Submits mint and transfer transactions to the Soroban contract |
| Role Discovery | `src/role.ts` | `RoleModule` | Client-side role classification and capability gating |
| Admin Receipts | `src/admin/receipts.ts` | `normalizeAdminActionStatus`, `buildAdminTransactionExplorerUrl`, `buildAdminActionReceipt` | Builds serializable admin action receipts |
Expand All @@ -66,6 +68,7 @@ Reference this when completing the "Test(s)" column.
| Test File | Covers |
|---|---|
| `tests/client.test.ts` | Client configuration, module instantiation, signer requirements |
| `tests/compliance-batch.test.ts` | Batch validation, ordering, concurrency, partial failures, rate limits, safe diagnostics |
| `tests/config.test.ts` | Environment presets, config validation, error cases |
| `tests/role.test.ts` | Role discovery, capability checks, capability matrix |
| `tests/investor.test.ts` | Portfolio fetching, balance calculations, status mapping |
Expand All @@ -85,6 +88,7 @@ Reference this when completing the "Doc(s)" column.
| Document | Content |
|---|---|
| `docs/api-reference.md` | Full API reference for all public modules |
| `docs/compliance-batch-queries.md` | Batch compliance usage, performance, rate limits, diagnostics, and security |
| `docs/testing.md` | Mock client setup and testing patterns |
| `docs/contract-events.md` | Event decoder usage and supported topics |
| `docs/role-discovery.md` | Role discovery and capability gating |
Expand Down
62 changes: 61 additions & 1 deletion docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public async checkWhitelist(address: string): Promise<boolean>
`Promise<boolean>` — `true` if the simulated call to `is_whitelisted` succeeds and decodes to `true`. Resolves to `false` both when the contract reports the address is not whitelisted, *and* when the simulation does not succeed or returns no result — the current implementation does not distinguish those two cases in its return value.

**Errors**
* If `simulateTransaction` itself throws (network failure, malformed request, etc.), the error is logged via `console.error` and then re-thrown as-is. It is the raw error from the underlying `@stellar/stellar-sdk` RPC call — `checkWhitelist` does not wrap it in `PortfolioError` or any other typed error.
* If `simulateTransaction` throws, the client network boundary classifies it as a typed, safe `NetworkFailure`. Synchronous address/XDR construction and result parsing remain outside that boundary and may throw their underlying error.
* A failed/unsuccessful simulation that does *not* throw is swallowed and reported as `false` (see Returns above), not as an error.

**Example**
Expand All @@ -70,6 +70,66 @@ try {

> **Open note:** `checkWhitelist` passes the raw invocation object returned by `contract.call(...)` directly as the `transaction` field to `simulateTransaction` (cast through `as any`), rather than assembling a full `Transaction` via `TransactionBuilder` the way `AssetModule.mint`/`transfer` do. The source itself flags this with a comment ("Cast required depending on SDK version wrapper"), so the exact request shape expected by `simulateTransaction` across `@stellar/stellar-sdk` versions is not fully confirmed — verify against the installed SDK version rather than assuming it's stable.

### `checkWhitelistBatch(addresses, options?): Promise<ComplianceBatchResult>`

Checks multiple investor addresses with per-item validation, bounded
concurrency, input-order preservation, and safe partial-failure mapping.

**Signature**
```typescript
public async checkWhitelistBatch(
addresses: readonly string[],
options: ComplianceBatchOptions = {},
): Promise<ComplianceBatchResult>
```

**Parameters**
* `addresses` (`readonly string[]`): Stellar account public keys (`G...`). Runtime non-string, malformed, muxed (`M...`), and contract (`C...`) inputs become per-item `invalid-address` results without reaching RPC.
* `options.concurrency` (number, optional): Maximum simultaneous checks. Defaults to `4`; integer range 1–20.
* `options.deduplicate` (boolean, optional): Query identical valid addresses once and fan out the result. Defaults to `true`.
* `options.maxBatchSize` (number, optional): Maximum accepted input length. Defaults to `100`; integer range 1–1000.

**Returns**
`Promise<ComplianceBatchResult>` with:

* one frozen item per input, in original order;
* item statuses `whitelisted`, `not-whitelisted`, `invalid-address`, or `failed`;
* `isWhitelisted: false` on invalid/failed items (fail closed, but callers must branch on `status`);
* safe `NetworkFailureDiagnostic` data on failed items;
* counts, actual RPC query count, duplicate count, partial/exhausted flags, rate-limit flag, duration, and fetch timestamp.

An empty input returns a successful empty result. Individual invalid inputs,
network failures, and parse failures do not reject the batch. Invalid arbitrary
input is not echoed; correlate through `item.index`.

**Errors**
Throws `ComplianceBatchError` before RPC when the runtime input is not an array
(`INVALID_BATCH_INPUT`), exceeds `maxBatchSize` (`BATCH_TOO_LARGE`), or contains
invalid options (`INVALID_BATCH_OPTIONS`). Expected per-item failures are returned,
not thrown.

**Example**
```typescript
const result = await client.compliance.checkWhitelistBatch(
['G_INVESTOR_ONE', 'G_INVESTOR_TWO'],
{ concurrency: 4 },
);

for (const item of result.items) {
console.log(item.index, item.status, item.isWhitelisted);
}
```

### `buildComplianceBatchDiagnostic(result): ComplianceBatchDiagnostic`

Builds a frozen, address-free roll-up for telemetry and support reports. It
contains counts, classified failure-code counts, a recovery action, and the
largest safe `retryAfterSeconds` value. It never includes item addresses,
original invalid input, raw errors, RPC URLs, headers, or credentials.

See [Compliance Batch Queries](./compliance-batch-queries.md) for performance,
rate-limit, retry, dashboard, and security guidance.

---

## `AssetModule`
Expand Down
173 changes: 173 additions & 0 deletions docs/compliance-batch-queries.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# Compliance batch queries

Admin dashboards often need whitelist status for many investor rows. Calling
`checkWhitelist` in an unbounded `Promise.all` can overload an RPC provider and
makes one rejected request difficult to represent without losing the rest.

`ComplianceModule.checkWhitelistBatch` validates each input, limits concurrent
RPC work, and returns exactly one typed item per input in the original order.

## Quickstart

```typescript
const result = await client.compliance.checkWhitelistBatch(
['G_INVESTOR_ONE', 'G_INVESTOR_TWO', 'invalid-input'],
{ concurrency: 4 },
);

for (const item of result.items) {
if (item.status === 'whitelisted') {
renderApprovedRow(item.index);
} else if (item.status === 'failed') {
renderRetryRow(item.index, item.diagnostic.code);
} else {
renderNotApprovedRow(item.index, item.code);
}
}
```

Use `item.index` to correlate with the original array. Valid addresses are
included on resolved/failed items. Invalid input is deliberately omitted from
the result because callers can accidentally pass tokens, URLs, or other
sensitive strings into a bulk-input field.

## Per-item states

| `status` | Meaning |
| --- | --- |
| `whitelisted` | Contract query resolved `true`. |
| `not-whitelisted` | Contract query resolved `false`. |
| `invalid-address` | Input was rejected before RPC. |
| `failed` | Valid address, but its query could not be evaluated. |

Invalid-item codes distinguish:

- `INVALID_ADDRESS`
- `MUXED_ADDRESS_UNSUPPORTED`
- `CONTRACT_ADDRESS_UNSUPPORTED`

Failures use `COMPLIANCE_QUERY_FAILED` and carry a safe
`NetworkFailureDiagnostic`. Raw provider messages, request payloads, RPC URLs,
headers, and credentials are never copied into an item.

`isWhitelisted` is present on every item and fails closed (`false`) for invalid
and failed items. Dashboards should still branch on `status`: `false` does not
distinguish a confirmed non-whitelisted result from an unavailable result.

## Partial failures

The batch promise does not reject because one address is invalid or one RPC
request fails. Instead:

- successful addresses retain their resolved status;
- invalid addresses receive an `invalid-address` item;
- RPC/parse failures receive a `failed` item with a safe diagnostic.

`summary.partial` is true when at least one query resolved and at least one
failed. `summary.exhausted` is true when every valid item failed.

Batch-level configuration errors still throw `ComplianceBatchError`:

- `INVALID_BATCH_INPUT`: runtime input is not an array;
- `BATCH_TOO_LARGE`: input exceeds `maxBatchSize`;
- `INVALID_BATCH_OPTIONS`: concurrency, deduplication, or size options are invalid.

## Performance assumptions

Soroban RPC does not provide one batch simulation call for this contract method.
Therefore **N unique valid addresses require N `simulateTransaction` requests**.

Defaults:

- `concurrency: 4` (allowed range 1–20);
- `deduplicate: true`;
- `maxBatchSize: 100` (configurable up to 1000).

Deduplication queries an identical valid address once and fans the same result
back to every original position. Duplicate items set `duplicate: true`;
`summary.queried` records actual RPC requests rather than input rows.

Choose concurrency based on the provider's documented quota, deployment
latency, and other traffic sharing the same API key. A larger value can reduce
wall-clock latency but increases burst load; it does not reduce total RPC calls.
Start with the default or lower it for shared/public endpoints.

## Rate limits and retries

The SDK deliberately performs **no automatic retry** inside a batch. Hidden
retries can multiply provider load precisely when it is already rate limiting.

If an item is rate limited:

1. Keep all successfully resolved items.
2. Build the address-free batch diagnostic.
3. Honor `retryAfterSeconds` when present.
4. Retry only the failed source rows after backoff, not the entire batch.

```typescript
import { buildComplianceBatchDiagnostic } from '@aegis/sdk';

const diagnostic = buildComplianceBatchDiagnostic(result);

if (diagnostic.action === 'retry-with-backoff') {
scheduleFailedRows(diagnostic.retryAfterSeconds);
}
```

`buildComplianceBatchDiagnostic` contains counts and classified failure codes
only. It intentionally excludes every address and original input, making it
suitable for telemetry and support reports.

## Validation rules

Only Stellar Ed25519 account public keys (`G...`) are accepted. Validation uses
`StrKey.isValidEd25519PublicKey` before contract/XDR construction:

- empty, malformed, and non-string runtime input is invalid;
- muxed (`M...`) addresses are currently unsupported;
- contract (`C...`) addresses cannot identify investors.

Validation is per item, so mixed input does not prevent valid rows from being
queried.

## Mock-client usage

The test-only client exposes the same method:

```typescript
import {
createMockAegisClient,
createMockFixtures,
} from '@aegis/sdk/testing';

const fixtures = createMockFixtures();
const client = createMockAegisClient();
client.setWhitelisted(fixtures.investorAddress, true);

const result = await client.compliance.checkWhitelistBatch([
fixtures.investorAddress,
fixtures.secondaryInvestorAddress,
]);
```

This uses in-memory state and makes no network calls.

## Security and compliance notes

- Public keys are identifiers, but a list of investors under compliance review
can still be sensitive operational data. Keep batch results access-controlled.
- Use the address-free roll-up for logs and support tickets.
- Whitelist status reports protocol state; it is not legal, financial, or
regulatory advice.
- `not-whitelisted`, `invalid-address`, and `failed` are distinct. Do not display
an RPC outage as a compliance denial.

## Contributor checklist

- [ ] Results remain one-to-one with input and preserve order.
- [ ] Invalid inputs never reach RPC and are not echoed in results/diagnostics.
- [ ] One item failure never rejects otherwise valid batch work.
- [ ] Concurrency stays bounded and defaults remain documented.
- [ ] No automatic retry is introduced without explicit rate-limit analysis.
- [ ] Diagnostics remain free of addresses, raw errors, URLs, headers, and secrets.
- [ ] Tests cover mixed input, partial failure, rate limiting, ordering, and bounds.
6 changes: 5 additions & 1 deletion docs/test-first-contribution-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,18 @@ file must cover **happy-path** and **negative-path** scenarios.

### Compliance Checks

Tests for `ComplianceModule` methods (`checkWhitelist`) must cover:
Tests for `ComplianceModule` methods (`checkWhitelist`, `checkWhitelistBatch`) must cover:

| Scenario | Expectation |
|---|---|
| Whitelisted address | Returns `true` |
| Non-whitelisted address | Returns `false` |
| RPC failure / timeout | Throws or returns fallback (module-dependent) |
| Invalid address (empty string) | Returns `false` or typed error |
| Mixed valid and invalid batch input | Preserves order; invalid items do not reach RPC |
| Partial batch RPC failure | Successful items remain resolved; failure is safe per-item |
| Batch concurrency / duplicates | Configured bound is honored; default deduplication avoids duplicate RPC work |
| Batch rate limiting | Safe diagnostic recommends backoff without exposing addresses or raw errors |

```typescript
// Happy-path: whitelisted investor
Expand Down
5 changes: 5 additions & 0 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ client.setBalance(fixtures.investorAddress, '5000000000'); // raw integer (7 dec

// Compliance
const isApproved = await client.compliance.checkWhitelist(fixtures.investorAddress);
const batch = await client.compliance.checkWhitelistBatch([
fixtures.investorAddress,
fixtures.secondaryInvestorAddress,
]);

// Investor portfolio read model
const portfolio = await client.investor.getPortfolio(fixtures.investorAddress);
Expand All @@ -47,6 +51,7 @@ console.log(client.transactions[0]); // { hash, type, from, to, amount, timestam
Creates an in-memory `MockAegisClient` with the same module surface as `AegisClient`:

* `client.compliance.checkWhitelist(address)` → `Promise<boolean>`
* `client.compliance.checkWhitelistBatch(addresses, options?)` → `Promise<ComplianceBatchResult>`
* `client.asset.mint(to, amount)` → `Promise<string>` (mock tx hash)
* `client.asset.transfer(to, amount)` → `Promise<string>` (mock tx hash)
* `client.investor.getPortfolio(address, options?)` → `Promise<InvestorPortfolio>`
Expand Down
2 changes: 1 addition & 1 deletion src/client-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export interface AegisReadOnlyClient {
readonly capabilities: RoleCapabilities;
/** The underlying `AegisClient` instance. */
readonly client: AegisClient;
/** Compliance query module (read-only: checkWhitelist). */
/** Compliance query module (read-only: single and batch whitelist checks). */
readonly compliance: ComplianceModule;
/** Investor portfolio read module. */
readonly investor: InvestorModule;
Expand Down
Loading
Loading