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
51 changes: 51 additions & 0 deletions crates/nexum-runtime/src/bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,54 @@ wasmtime::component::bindgen!({
imports: { default: async },
exports: { default: async },
});

/// Bindgen smoke for the `nexum:value-flow` types package. The package has
/// no host consumer yet (the intent router that will bind it lands later),
/// so this compiles it under test only, through a throwaway world that
/// imports the interface. Its value is the identifier-hygiene gate: the
/// test names every generated type, variant, and field by its plain Rust
/// spelling, so a WIT id that collided with a Rust keyword would surface as
/// an `r#` escape and fail to compile here rather than in a downstream
/// binding.
#[cfg(test)]
mod value_flow_smoke {
wasmtime::component::bindgen!({
inline: "
package nexum:value-flow-smoke;
world smoke {
import nexum:value-flow/types@0.1.0;
}
",
path: ["../../wit/nexum-value-flow"],
});

#[test]
fn identifiers_bind_unescaped() {
use nexum::value_flow::types::{Asset, AssetAmount, OffchainDesc, ServiceDesc, Settlement};

let _ = Settlement::EvmChain(1);
let _ = Settlement::Offchain(String::new());

let service = ServiceDesc {
kind: String::new(),
summary: String::new(),
};
let offchain = OffchainDesc {
domain: String::new(),
summary: String::new(),
};

let _ = Asset::NativeToken(Settlement::EvmChain(1));
let _ = Asset::Erc20((1, Vec::new()));
let _ = Asset::Erc721((1, Vec::new(), Vec::new()));
let _ = Asset::Erc1155((1, Vec::new(), Vec::new()));
let _ = Asset::Service(service);
let asset = Asset::Offchain(offchain);

let amount = AssetAmount {
asset,
amount: Vec::new(),
};
assert!(amount.amount.is_empty());
}
}
87 changes: 87 additions & 0 deletions wit/nexum-value-flow/types.wit
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package nexum:value-flow@0.1.0;

/// The egress-neutral vocabulary for value in motion: where a deal settles,
/// what asset moves, and how much of it. Shared by intent headers,
/// simulation balance diffs, and analyser verdict subjects so that a spend
/// is written the same way in all three places. This is the platform's
/// hardest-freezing contract; the package carries no dependency so it can
/// outlive any interface built on it.
///
/// Identifier hygiene is a freeze gate. Every identifier below is checked
/// against WIT keywords (including in-flight proposals -- the package is
/// `value-flow`, not `value`, because the component model's value-imports
/// feature is circling that word) and against the reserved words of the
/// nine binding-target languages (Rust, Python, JS, Go, C#, Java, Kotlin,
/// Swift, Dart). A two-word kebab id is preferred wherever a single word is
/// a keyword anywhere: `native-token` not `native` (a Java modifier),
/// `offchain` not `external` (a Dart keyword). WIT parses the rejected
/// spellings today; the cost lands later, as escaped identifiers in the
/// generated bindings for exactly the personas the SDK exists to serve.
interface types {
/// Where a deal comes to rest. A variant from day one: the chain-id
/// plumbing assumes every venue settles on an EVM chain, which the
/// off-chain marketplace target breaks.
variant settlement {
/// Settles on an EVM chain, identified by its chain id.
evm-chain(u64),
/// Settles off-chain: the payload is a jurisdiction or a
/// venue-defined domain. Settlement here is a legal or
/// out-of-band process, not a chain state transition.
offchain(string),
}

/// A non-token service obligation whose worth the host cannot compute,
/// e.g. Swarm postage: storage capacity for a duration. Policy on a
/// service asset is adapter-attested, not host-verified; the consent
/// surface renders `summary` and must say the host verifies nothing.
record service-desc {
/// Namespaced service kind the venue defines, e.g. `swarm:postage`.
/// Stable enough for policy to match on.
kind: string,
/// Adapter-supplied human-readable description for the consent sheet.
summary: string,
}

/// A real-world asset whose settlement is a legal process rather than a
/// chain state transition: a deed, a chattel, a registry entry. The host
/// verifies nothing about it. The case name mirrors `settlement.offchain`
/// deliberately: the same concept on two axes -- where the asset lives,
/// and where the deal settles.
record offchain-desc {
/// Jurisdiction or registry domain the asset lives in, e.g. an
/// ISO country code or a venue-defined registry name.
domain: string,
/// Adapter-supplied human-readable description for the consent sheet.
summary: string,
}

/// A kind of value that can move. The ERC cases carry a bare chain id
/// rather than a `settlement` because an ERC token is inherently EVM;
/// `native-token` wraps `settlement` because a chain's own gas token is
/// the one asset that exists on every settlement domain. Each token
/// tuple carries raw big-endian bytes: a 20-byte address, and for the
/// NFT cases a token id of arbitrary width.
variant asset {
/// The settlement domain's own gas token (ETH, BZZ, ...).
native-token(settlement),
Comment on lines +60 to +66

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Two issues with native-token(settlement):

  1. The BZZ example is wrong. BZZ is an ERC-20 on Ethereum/Gnosis Chain, not the native gas token of any chain. Correct examples: ETH, xDAI, MATIC, BNB.

  2. native-token(offchain(...)) is a structurally valid but semantically empty combination. No real domain has an off-chain-settled native gas token; an analyser receiving this case has no oracle, no AMM pool, and no contract address to price against. The rationale for wrapping settlement is that each EVM chain has its own native token — that maps only to settlement::evm-chain. Either restrict native-token to hold a bare u64 (consistent with ERC cases), or add a comment explicitly forbidding the offchain arm and stating what consumers must do if they see it.

/// An ERC-20 token: (chain id, 20-byte contract address).
erc20(tuple<u64, list<u8>>),
/// An ERC-721 NFT: (chain id, 20-byte contract address, token id).
erc721(tuple<u64, list<u8>, list<u8>>),
/// An ERC-1155 token: (chain id, 20-byte contract address, token id).
erc1155(tuple<u64, list<u8>, list<u8>>),
Comment on lines +68 to +72

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The ERC cases use anonymous positional tuples for a package described as the "hardest-freezing contract." The second position (address) is already opaque; at a frozen interface, adding a third field means a semver bump even for optional metadata. Named records would make positions self-documenting and leave room for a non-breaking extension:

record evm-token   { chain-id: u64, address: list<u8> }
record evm-nft     { chain-id: u64, address: list<u8>, token-id: list<u8> }

erc20(evm-token),
erc721(evm-nft),
erc1155(evm-nft),

This is easier pre-freeze than post.

/// A non-token service, e.g. storage capacity for a duration.
service(service-desc),
/// A real-world asset settled off-chain.
offchain(offchain-desc),
}

/// An amount of one asset. `amount` is a big-endian unsigned integer,
/// most-significant byte first, with no fixed width; an empty list is
/// zero. Amounts are never negative -- direction lives in whichever
/// field holds the pair (a header's `gives` vs `wants`), not here.
record asset-amount {
asset: asset,
amount: list<u8>,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The amount field is missing its doc comment — the encoding convention (big-endian, empty = zero, no negative) is documented on the record but not on the field itself. WIT tooling renders field docs separately.

Suggested change
amount: list<u8>,
/// Big-endian unsigned integer, most-significant byte first; empty encodes zero.
amount: list<u8>,

}
}
Loading