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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ value-encoding rules, and the no-real-user-data guarantee.

- [SDK Integration Fixtures](docs/sdk-fixtures.md) — deterministic example outputs for compliance, minting, transfer, event, error, and capability scenarios, for cross-repo testing

- [Contract Capability Flags](docs/capabilities.md) — read-only descriptor of which modules and protocol behaviours a deployment supports, for SDK/dashboard feature gating

- [Public Interface Compatibility Checks](docs/interface-compatibility.md) — how SDK/dashboard clients verify their required capabilities and schema version against a deployment before integrating

- [Investor Holding Restriction Checks](docs/investor-holding-restrictions.md) — per-investor holding cap workflow and enforcement

Expand Down
8 changes: 8 additions & 0 deletions docs/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,14 @@ enumerate the registry rather than hardcode it — and detect at runtime that a
deployment is older or newer than the keys it knows about. Order is stable
within a schema version.

### `check_interface_compatibility(client_schema_version, required_capabilities) -> InterfaceCompatibilityReport`

Checks a client's required capability keys against this deployment in one
call and reports the schema-version relationship, so an SDK or dashboard can
answer "can I safely integrate with this deployment?" without hand-rolling
the comparison. See [`docs/interface-compatibility.md`](interface-compatibility.md)
for the full field reference and usage guidance.

## Versioning

`capability_version` is the schema version of the response
Expand Down
1 change: 1 addition & 0 deletions docs/contract-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ key registry, and versioning rules.
* `get_capabilities(env)`: Returns a `ContractCapabilities` struct describing compliance, minting, transfer, pause, metadata, and event support, plus `capability_version` / `contract_version`. Each behaviour is a `CapabilityStatus` — `Supported`, `Planned`, or `Unsupported` — alongside runtime switches (`paused`, `operations_enabled`, `supply_cap_enforced`, `holding_cap_enforced`, `metadata_configured`, `initialized`).
* `supports_capability(env, capability)`: Returns the `CapabilityStatus` for a single capability key. Unknown keys return `Unsupported` instead of reverting, so newer clients fail safe against older deployments.
* `get_capability_keys(env)`: Returns every capability key understood by this contract version.
* `check_interface_compatibility(env, client_schema_version, required_capabilities)`: Returns an `InterfaceCompatibilityReport` — whether every key in `required_capabilities` resolves to `Supported`, plus how `client_schema_version` relates to this deployment's schema version. See [`docs/interface-compatibility.md`](interface-compatibility.md).

> A capability indicates the protocol *implements* a behaviour — not that the
> caller is authorized to perform it, nor that it will succeed against current
Expand Down
122 changes: 122 additions & 0 deletions docs/interface-compatibility.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Public Interface Compatibility Checks

This document describes `check_interface_compatibility`, a read-only entrypoint
that lets an SDK or dashboard client verify its required capabilities against
a specific Aegis deployment **before** it starts building transactions
against it.

It builds directly on [`docs/capabilities.md`](capabilities.md) — read that
first if you are not already familiar with `CapabilityStatus`,
`get_capabilities`, and the append-only versioning rules. This document only
covers the compatibility check itself.

> **Not a permission or compliance check.** Like the capability flags it is
> built on, this only reports what the *protocol* implements. It is not
> legal, financial, or compliance advice, and it never determines whether a
> specific caller is authorized to do anything — see
> [`docs/admin-roles.md`](admin-roles.md) and
> [`docs/investor-eligibility.md`](investor-eligibility.md) for that.

## Why

`get_capabilities` and `supports_capability` already let a client *ask* what
a deployment supports. What they don't do is give a client a single,
actionable **yes/no plus a reason** for "can I safely integrate with this
deployment at all?" Without that, every integrator re-implements the same
comparison logic — or skips it, and discovers a gap only when a transaction
it assumed would succeed reverts. That is a worse failure mode for
RWA/compliance tooling than for a typical dApp: a dashboard that silently
renders a "supported" control for a capability the deployment doesn't
actually have can walk an investor into building a transaction that reverts,
or worse, mask a compliance-relevant feature gap.

`check_interface_compatibility` answers the question directly, in one call,
from the deployment itself — the same design principle as the capability
flags it depends on.

## API

Pure read: **no storage writes, no events, no authorization required, and it
never panics** — including before `initialize` has been called and while the
contract is paused. Safe to call from a read-only RPC simulation at any time.

### `check_interface_compatibility(client_schema_version: u32, required_capabilities: Vec<Symbol>) -> InterfaceCompatibilityReport`

```rust
pub struct InterfaceCompatibilityReport {
pub contract_schema_version: u32, // this deployment's CAPABILITY_SCHEMA_VERSION
pub client_schema_version: u32, // echoed back from the call
pub schema_relation: SchemaVersionRelation,
pub unsupported_required: Vec<Symbol>, // subset of the input not Supported
pub compatible: bool, // true iff unsupported_required is empty
}

pub enum SchemaVersionRelation {
Matching, // client_schema_version == contract_schema_version
ClientOlder, // client_schema_version < contract_schema_version
ClientNewer, // client_schema_version > contract_schema_version
}
```

* `client_schema_version` — the [`CAPABILITY_SCHEMA_VERSION`](capabilities.md#versioning)
the calling SDK/dashboard build was written against. Pass the constant your
generated client was built with.
* `required_capabilities` — the capability keys (see the
[key registry](capabilities.md#supports_capabilitycapability-symbol---capabilitystatus))
your client build cannot function without. Pass only what is actually
required for the feature set you are about to enable — not every key in the
registry.
* `unsupported_required` is derived by calling `supports_capability` for each
requested key, so it can never disagree with `get_capabilities` /
`supports_capability`. A key resolves into this list if it is
`Planned`, `Unsupported`, **or unknown to this deployment** — an unknown key
fails safe exactly like `supports_capability` does.
* `compatible` is `true` **iff `unsupported_required` is empty.** A schema
version mismatch alone never makes a client incompatible: schema fields and
keys are append-only (see [Versioning](capabilities.md#versioning)), so the
only thing that can actually break a client is a *specific capability it
depends on* not being `Supported`.

## Reading `schema_relation`

| Relation | Meaning | What to do |
| --- | --- | --- |
| `Matching` | Client and deployment were built against the same schema. | Nothing extra — the two evolved together. |
| `ClientOlder` | The deployment may advertise fields/keys the client predates. | Safe on its own. Fields are append-only, so nothing the client already understands has moved or been repurposed. |
| `ClientNewer` | The client may expect fields/keys this deployment predates. | Not automatically fatal — check `unsupported_required`. If it's empty, everything the client actually asked for is present; the client simply also knows about capabilities this deployment hasn't shipped yet. |

`schema_relation` is a diagnostic signal, not a pass/fail gate by itself —
`compatible` is the field to branch on.

## SDK and dashboard usage

* **Call once per deployment, before first use.** Build `required_capabilities`
from the feature set your build actually depends on (e.g. `whitelist`,
`transfers`, `holding_cap`), not the full registry.
* **Branch only on `compatible`.** If `false`, block the affected flows and
surface `unsupported_required` to the integrator/operator — it is the exact
list to act on, not a hint to go re-derive.
* **Treat `ClientNewer` with an otherwise-empty `unsupported_required` as
fine.** It only means the client's build knows about capabilities this
particular deployment hasn't shipped — none of which the client currently
requires.
* **Re-check after a contract upgrade**, the same way you would re-read
`get_capabilities` — static capabilities are fixed per build, so cache
results for the lifetime of a deployment, not across upgrades.

## Compatibility

* **Purely additive.** No existing function, error code, event, or storage
key changed. The check re-derives every answer from the existing
`supports_capability` helper, so it cannot disagree with `get_capabilities`
or the key registry.
* **No new storage keys and no new error codes.** The function is a pure
computation over its inputs and existing capability state.
* **Not a state-changing call**, exempt from the pause guard by design,
consistent with the other read helpers in
[`contract-spec.md`](contract-spec.md#read-functions).
* Tests covering matching/older/newer schema relations, aggregation of
multiple unsupported keys, agreement with `supports_capability`, the
empty-requirements case, and the no-mutation/pre-`initialize` guarantee
live in [`src/test.rs`](../src/test.rs) under
"Public interface compatibility checks (#37)".
107 changes: 107 additions & 0 deletions src/capabilities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,98 @@ pub fn get_capability_keys(env: &Env) -> Vec<Symbol> {
]
}

// ─── Interface compatibility checks ────────────────────────────────────────────

/// How a client's known schema version relates to this deployment's.
///
/// Derived purely from comparing two `u32`s against the append-only
/// versioning contract described in `docs/capabilities.md`.
#[contracttype]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SchemaVersionRelation {
/// The client was built against exactly this schema version.
Matching,
/// The client is older than this deployment: the contract may advertise
/// fields the client has never heard of. Safe — schema fields are
/// append-only, so nothing the client already understands has moved.
ClientOlder,
/// The client is newer than this deployment: the client may expect
/// fields or keys this deployment predates. Check `unsupported_required`
/// rather than assuming the mismatch alone is fatal.
ClientNewer,
}

/// Result of checking an SDK/dashboard's expected interface against this
/// deployment's actual capability surface.
///
/// See [`check_interface_compatibility`]. This is a diagnostic, not a
/// permission check — like [`ContractCapabilities`], it never gates
/// authorization, only feature availability.
#[contracttype]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct InterfaceCompatibilityReport {
/// This deployment's [`CAPABILITY_SCHEMA_VERSION`].
pub contract_schema_version: u32,
/// The schema version the calling client was built against.
pub client_schema_version: u32,
/// How the two versions relate.
pub schema_relation: SchemaVersionRelation,
/// The subset of `required_capabilities` (from the call) that this
/// deployment does **not** resolve to `Supported` — including any key
/// the deployment has never heard of, per the fail-safe rule in
/// `supports_capability`. Empty means every requirement is met.
pub unsupported_required: Vec<Symbol>,
/// `true` iff `unsupported_required` is empty. A schema-version mismatch
/// alone does not make a client incompatible — only a missing required
/// capability does, since fields are append-only.
pub compatible: bool,
}

/// Checks whether a client's required capabilities are all `Supported` by
/// this deployment, and reports how the client's schema version compares.
///
/// `required_capabilities` is the set of capability keys (see
/// `get_capability_keys`) the calling SDK/dashboard build cannot function
/// without. This lets integrators — including RWA/compliance tooling that
/// must not silently degrade — fail fast with a precise, actionable list
/// instead of discovering a gap mid-transaction.
///
/// Pure read: no storage writes, no events, no authorization, never panics.
/// Always available, including before `initialize`.
pub fn check_interface_compatibility(
env: &Env,
client_schema_version: u32,
required_capabilities: &Vec<Symbol>,
) -> InterfaceCompatibilityReport {
let contract_schema_version = CAPABILITY_SCHEMA_VERSION;

let schema_relation = if client_schema_version == contract_schema_version {
SchemaVersionRelation::Matching
} else if client_schema_version < contract_schema_version {
SchemaVersionRelation::ClientOlder
} else {
SchemaVersionRelation::ClientNewer
};

// Re-derive each requirement from the single source of truth so this
// can never disagree with `supports_capability` / `get_capabilities`.
let mut unsupported_required: Vec<Symbol> = vec![env];
for i in 0..required_capabilities.len() {
let key = required_capabilities.get(i).unwrap();
if supports_capability(env, &key) != CapabilityStatus::Supported {
unsupported_required.push_back(key);
}
}

InterfaceCompatibilityReport {
contract_schema_version,
client_schema_version,
schema_relation,
compatible: unsupported_required.is_empty(),
unsupported_required,
}
}

// ─── Public API ───────────────────────────────────────────────────────────────

#[contractimpl]
Expand Down Expand Up @@ -605,4 +697,19 @@ impl AegisContract {
pub fn get_capability_keys(env: Env) -> Vec<Symbol> {
get_capability_keys(&env)
}

/// Checks a client's required capability keys against this deployment
/// and reports the schema-version relationship, for public-interface
/// compatibility checks ahead of integration. See
/// `docs/interface-compatibility.md`.
///
/// Never mutates state, emits no events, requires no authorization, and
/// remains callable before `initialize` and while paused.
pub fn check_interface_compatibility(
env: Env,
client_schema_version: u32,
required_capabilities: Vec<Symbol>,
) -> InterfaceCompatibilityReport {
check_interface_compatibility(&env, client_schema_version, &required_capabilities)
}
}
Loading