From b09c749c4ea9e7a7c1be3ad442acf58f0cc532f0 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Thu, 16 Jul 2026 14:54:52 +0200 Subject: [PATCH] feat: add collections for sui --- content/contracts-sui/1.x/api/collections.mdx | 902 ++++++++++++++++++ content/contracts-sui/1.x/collections.mdx | 112 +++ content/contracts-sui/1.x/index.mdx | 7 +- content/contracts-sui/index.mdx | 6 + public/llms.txt | 2 + src/navigation/sui/current.json | 10 + 6 files changed, 1036 insertions(+), 3 deletions(-) create mode 100644 content/contracts-sui/1.x/api/collections.mdx create mode 100644 content/contracts-sui/1.x/collections.mdx diff --git a/content/contracts-sui/1.x/api/collections.mdx b/content/contracts-sui/1.x/api/collections.mdx new file mode 100644 index 00000000..aca42299 --- /dev/null +++ b/content/contracts-sui/1.x/api/collections.mdx @@ -0,0 +1,902 @@ +--- +title: Collections API Reference +--- + +This page documents the public API of `openzeppelin_collections` for OpenZeppelin Contracts for Sui `v1.x`. + +### `sorted_map` [toc] [#sorted_map] + + + +```move +use openzeppelin_collections::sorted_map; +``` + +A generic, ordered key-value map with O(log N) lookup and ordered navigation (head/tail, floor/ceiling, next key, sorted pages). Every operation loads exactly one stored object, so capacity is bounded by Sui's object size limit (~250 KB), not by any per-transaction dynamic-field cap. `SortedMap` is a UID-less value shaped like `sui::vec_map::VecMap` — no identity of its own, no dynamic fields — so you embed it in your own `has key` object and gate access with your own entry functions. The module emits no events. + +The API splits into bare and `_by` forms. Bare macros (`upsert!`, `borrow!`, ...) assume the built-in integer `<` and compile only for integer keys; `_by` macros take a strict less-than comparator lambda and are required for `address`, struct, or any other non-integer key. Values only need `store`, so a resource `V` like `Coin` is supported: such a map cannot be dropped — drain every value via `remove!`/`pop_front`/`pop_back`, then call `destroy_empty`. + + +The map stores no comparator. Ordering is defined per call by the strict less-than lambda you supply, and it MUST be a strict total order threaded consistently to every call on a given map. The library cannot detect a violation, and the failure is silent: a non-strict `<=` causes duplicate inserts and missed lookups/removes, and mixing `<` with `>` across calls corrupts order. In tests, call `is_well_formed_by!` after a `_by` sequence to catch comparator mistakes. + + +Types + +- [`Entry`](#sorted_map-Entry) +- [`SortedMap`](#sorted_map-SortedMap) + +Functions + +- [`new()`](#sorted_map-new) +- [`singleton(key, value)`](#sorted_map-singleton) +- [`destroy_empty(map)`](#sorted_map-destroy_empty) +- [`length(map)`](#sorted_map-length) +- [`is_empty(map)`](#sorted_map-is_empty) +- [`head(map)`](#sorted_map-head) +- [`tail(map)`](#sorted_map-tail) +- [`pop_front(map)`](#sorted_map-pop_front) +- [`pop_back(map)`](#sorted_map-pop_back) +- [`keys(map)`](#sorted_map-keys) + +Macros + +- [`from_sorted_keys_values_by!(keys, values, lt)`](#sorted_map-from_sorted_keys_values_by) +- [`from_sorted_keys_values!(keys, values)`](#sorted_map-from_sorted_keys_values) +- [`contains_by!(map, key, lt)`](#sorted_map-contains_by) +- [`contains!(map, key)`](#sorted_map-contains) +- [`borrow_by!(map, key, lt)`](#sorted_map-borrow_by) +- [`borrow!(map, key)`](#sorted_map-borrow) +- [`borrow_mut_by!(map, key, lt)`](#sorted_map-borrow_mut_by) +- [`borrow_mut!(map, key)`](#sorted_map-borrow_mut) +- [`add_by!(map, key, value, lt)`](#sorted_map-add_by) +- [`add!(map, key, value)`](#sorted_map-add) +- [`upsert_by!(map, key, value, lt)`](#sorted_map-upsert_by) +- [`upsert!(map, key, value)`](#sorted_map-upsert) +- [`remove_by!(map, key, lt)`](#sorted_map-remove_by) +- [`remove!(map, key)`](#sorted_map-remove) +- [`find_next_by!(map, key, include, lt)`](#sorted_map-find_next_by) +- [`find_next!(map, key, include)`](#sorted_map-find_next) +- [`find_prev_by!(map, key, include, lt)`](#sorted_map-find_prev_by) +- [`find_prev!(map, key, include)`](#sorted_map-find_prev) +- [`next_key_by!(map, key, lt)`](#sorted_map-next_key_by) +- [`next_key!(map, key)`](#sorted_map-next_key) +- [`prev_key_by!(map, key, lt)`](#sorted_map-prev_key_by) +- [`prev_key!(map, key)`](#sorted_map-prev_key) +- [`keys_from_by!(map, from, include, limit, lt)`](#sorted_map-keys_from_by) +- [`keys_from!(map, from, include, limit)`](#sorted_map-keys_from) +- [`is_well_formed_by!(map, lt)`](#sorted_map-is_well_formed_by) +- [`is_well_formed!(map)`](#sorted_map-is_well_formed) + +Errors + +- [`EKeyNotFound`](#sorted_map-EKeyNotFound) +- [`ENotEmpty`](#sorted_map-ENotEmpty) +- [`EEmpty`](#sorted_map-EEmpty) +- [`EKeysNotStrictlyIncreasing`](#sorted_map-EKeysNotStrictlyIncreasing) +- [`EUnequalLengths`](#sorted_map-EUnequalLengths) +- [`EKeyAlreadyExists`](#sorted_map-EKeyAlreadyExists) + +#### Types [!toc] [#sorted_map-Types] + + +One key-value pair, stored inline in the map's vector. Its fields are private; read them through the forced-public `key`/`value` accessors if a macro body requires it. + +Abilities materialize jointly over `K` and `V`: `Entry` is `copy + drop + store`, while `Entry>` is store-only. + + + +A map kept sorted by key. Every operation loads exactly one stored object, so it is bounded by object size (~250 KB), not by any per-transaction dynamic-field cap; lookup is O(log N) and writes are O(N). + +A pure value — no `UID`, no dynamic fields — so it embeds directly in an integrator's object, exactly like `sui::vec_map::VecMap`. `copy`/`drop` materialize only when both `K` and `V` allow them: `SortedMap` is `copy + drop + store`; `SortedMap>` is store-only and must be drained then `destroy_empty`'d. + +Across the supported (macro) API the backing vector is strictly increasing under the consistently supplied comparator: sorted, with no duplicate keys. + + +#### Functions [!toc] [#sorted_map-Functions] + + +Creates a new, empty map. Takes no `&mut TxContext`: a `SortedMap` is a value, not an object. + + + +Creates a map holding a single `key`/`value` entry. O(1); needs no comparator, since one entry is trivially sorted. For more than one entry from pre-sorted data, use `from_sorted_keys_values!`. + + + +Destroys an empty map. This is the only terminal for a map whose value type lacks `drop` (e.g. `SortedMap>`): drain every value via `remove!`/`pop_front`/`pop_back` first, then call this. + +Aborts with `ENotEmpty` if the map still holds entries. + + + +Returns the number of entries. + + + +Returns `true` iff the map holds no entries. + + + +Returns the smallest key under the comparator, or `none` if the map is empty. O(1). With a reverse comparator this returns the largest numeric key. + + + +Returns the largest key under the comparator, or `none` if the map is empty. O(1). + + + +Removes and returns the smallest entry as a `(key, value)` pair. O(N), as it shifts every remaining entry — prefer `pop_back` for bulk drains, which is O(1). + +Aborts with `EEmpty` if the map is empty. + + + +Removes and returns the largest entry as a `(key, value)` pair. O(1) (no shift). + +Aborts with `EEmpty` if the map is empty. + + + +Returns all keys in ascending comparator order as an owned `vector`. O(N) in output size with no `limit`; for large or near-ceiling maps prefer the paged `keys_from!`. Loads exactly one stored object regardless of N. + + +#### Macros [!toc] [#sorted_map-Macros] + + +Builds a map from parallel `keys`/`values` that MUST already be strictly increasing under `lt` (sorted, no duplicate keys). O(n): one pass validates each adjacent pair, then appends at the back — no per-element search. Prefer this to a loop of `upsert_by!`, which is O(n^2) for unsorted input. If your keys are not yet ordered, sort them under the same comparator first, then call this. + +Unlike `sorted_set::from_keys!` (which de-duplicates), this aborts on any out-of-order or duplicate key: values are conserved (a resource `V` can never be silently displaced), so a duplicate cannot be collapsed away. + +Aborts with `EUnequalLengths` if `keys` and `values` differ in length. + +Aborts with `EKeysNotStrictlyIncreasing` if `keys` is not strictly increasing under `lt`. + + + +`from_sorted_keys_values_by!` with the built-in integer `<`. + +Aborts with `EUnequalLengths` or `EKeysNotStrictlyIncreasing` — see `from_sorted_keys_values_by!`. + + + +Returns `true` iff `key` is present, under `lt`. Pure, total read: agrees exactly with `borrow_by!` succeeding, since both route through the same binary search. + + + +`contains_by!` with the built-in integer `<`. + + + +Immutably borrows `key`'s value, under `lt`. There is deliberately no non-aborting `Option<&V>` variant (Move cannot put a reference in an `Option`) — use `contains_by!` first when the key may be absent. + +Aborts with `EKeyNotFound` if `key` is absent. + + + +`borrow_by!` with the built-in integer `<`. + +Aborts with `EKeyNotFound` if `key` is absent. + + + +Mutably borrows `key`'s value, under `lt`. Yields `&mut V`, never a mutable entry reference, so the key cannot be desynced from its sorted position. + +Aborts with `EKeyNotFound` if `key` is absent. + + + +`borrow_mut_by!` with the built-in integer `<`. + +Aborts with `EKeyNotFound` if `key` is absent. + + + +Inserts `key`/`value` under `lt`, aborting if `key` is already present (length + 1). `key` is taken by value and moved into storage; nothing is returned. + +Matches `sui::vec_map::insert`: a strict insert that refuses to touch an existing entry, so a duplicate is always a caller bug rather than a silent overwrite. Use `upsert_by!` instead when replacing an existing value is the intended behavior — it returns the displaced value rather than aborting. + +Aborts with `EKeyAlreadyExists` if `key` is already present. + + + +`add_by!` with the built-in integer `<`. + +Aborts with `EKeyAlreadyExists` if `key` is already present. + + + +Inserts `key`/`value`, or replaces the value if `key` is already present, under `lt`. Returns `some(old_value)` on replace (length unchanged), `none` on a fresh insert (length + 1). The displaced value is returned — never dropped — so a resource `V` is safe; `K` must be `drop` only because the previously stored key is dropped on replace (last-write-wins for the key bytes, observable only under a coarse comparator). + +This is a deliberate divergence from `sui::vec_map::insert`, which aborts on a duplicate key: `upsert_by!` is a total upsert. For abort-on-duplicate, prefer `add!` (a strict insert); or, when `V` has `drop`, assert on the returned option (`assert!(m.upsert!(k, v).is_none(), E)`). For a resource `V` the returned option cannot be dropped, so bind it, assert `is_none()`, and consume it with `destroy_none()`. + + + +`upsert_by!` with the built-in integer `<`. Returns `some(old)` on replace, `none` on a fresh insert. + + + +Removes `key`'s entry and returns the removed `(key, value)` tuple, under `lt` (length - 1, order preserved). Both the key and the value are returned, so a non-`drop` key or resource value is conserved, not destroyed. + +Aborts with `EKeyNotFound` if `key` is absent. + + + +`remove_by!` with the built-in integer `<`. + +Aborts with `EKeyNotFound` if `key` is absent. + + + +Returns the smallest key `>= key` when `include` (the ceiling), else the smallest key `> key` (strict next); `none` if there is no such key. Pure, total read. Any returned key satisfies `contains_by!`. + + + +`find_next_by!` with the built-in integer `<`. + + + +Returns the largest key `<= key` when `include` (the floor), else the largest key `< key` (strict prev); `none` if there is no such key. Pure, total read. Any returned key satisfies `contains_by!`. + + + +`find_prev_by!` with the built-in integer `<`. + + + +Returns the smallest key strictly greater than `key`, or `none`. Sugar for `find_next_by!` with `include = false`. `next_key!` of the tail returning `none` is the forward-cursor termination signal. + + + +`next_key_by!` with the built-in integer `<`. + + + +Returns the largest key strictly less than `key`, or `none`. Sugar for `find_prev_by!` with `include = false`. `prev_key!` of the head returning `none` is the backward-cursor termination signal. + + + +`prev_key_by!` with the built-in integer `<`. + + + +Returns up to `limit` keys in strict ascending order: a contiguous run starting at the first key `>= from` (when `include`) or `> from` (strict). Returns at most `limit` keys; fewer if the tail is reached. + +Resume a page by passing the last returned key back as `from` with `include == false`: successive pages have no overlap and no gap, so concatenating them reconstructs the tail exactly. `limit == 0`, an empty map, or `from` past the tail all yield the empty vector. There is no descending pagination — use a reverse comparator. + + + +`keys_from_by!` with the built-in integer `<`. + + + +Returns `true` iff the map's entries are strictly increasing under `lt` — i.e. sorted with no duplicate keys. This is the order check for tests, directly re-verifying the sorted-order property that production code maintains by construction but never re-validates. Call it after a `_by` sequence to catch a comparator mistake. Returns `bool` rather than asserting, so callers compose it into their own assertions. + +`#[test_only]`: absent from published bytecode and not callable from production consumer code, but visible to a dependent's test code. + + + +`is_well_formed_by!` with the built-in integer `<`. `#[test_only]`. + + +#### Errors [!toc] [#sorted_map-Errors] + + +A key was not present in the map. Raised by `borrow!`/`borrow_mut!`/`remove!` (and their `_by` forms) on an absent key. + + + +`destroy_empty` was called on a non-empty map. + + + +`pop_front`/`pop_back` was called on an empty map. + + + +A bulk constructor (`from_sorted_keys_values!`/`_by`) received keys that are not strictly increasing under the comparator (out of order, or a duplicate). + + + +A bulk constructor (`from_sorted_keys_values!`/`_by`) received `keys` and `values` of different lengths. + + + +A key was already present in the map. Raised by `add!`/`add_by!` on a duplicate key. + + +#### Internal helpers [!toc] [#sorted_map-Internals] + + +Move 2024 macro hygiene requires every symbol a macro body references to be `public` at the consumer's expansion site, so the following helpers are `public` but are NOT part of the supported API. In particular, `insert_at`/`remove_at` write at a caller-given index with no ordering check, so calling them directly can silently corrupt sorted order and invalidate every subsequent lookup, insertion, and removal. Use the macro API instead. + + +Forced-public internals: `search!`, `entries`, `key`, `value`, `insert_at`, `remove_at`, `value_at`, `value_at_mut`, `assert_key_found`, `assert_key_absent`, `assert_equal_lengths`, `assert_strictly_increasing`. + +### `sorted_set` [toc] [#sorted_set] + + + +```move +use openzeppelin_collections::sorted_set; +``` + +A generic, ordered set of unique keys that iterates in comparator order — the gap `sui::vec_set` leaves. `SortedSet` is a thin wrapper over `SortedMap` (a set is a map whose values carry no information, exactly as `BTreeSet = BTreeMap` in Rust). It is a UID-less value with every key inline in one vector; a bare set is not `key`, so embed it in your own `has key` object. Because the value is always the trivial `copy + drop + store` `Unit`, `SortedSet` has exactly the abilities `K` does. + +The set stores no comparator. Order is defined per call by a strict less-than lambda `lt` you supply, and it must be the same strict total order on every call for a given set — the library cannot detect a violation. An inconsistent or non-strict comparator desorts the set (wrong membership answers, though no key's bytes are lost and all keys remain recoverable via `keys`); a coarse (non-injective) comparator collapses byte-distinct compare-equal keys into one element, keeping the last-written key's bytes. The bare (non-`_by`) macros use the built-in integer `<` and remove the footgun for integer keys. + +`upsert!` returns `bool` and never aborts on a duplicate (diverging from `sui::vec_set::insert`); `add!` is the strict counterpart that aborts on a duplicate, and `remove!` aborts on an absent key, matching `vec_set`. Costs mirror the wrapped map: O(log N) membership, O(N) insert/remove, exactly one stored-object access per operation, and the same ~250 KB object-size ceiling. The module emits no events — a UID-less value has no on-chain identity, so emit events from your own entry functions. + +Types + +- [`Unit`](#sorted_set-Unit) +- [`SortedSet`](#sorted_set-SortedSet) + +Functions + +- [`new()`](#sorted_set-new) +- [`singleton(key)`](#sorted_set-singleton) +- [`destroy_empty(set)`](#sorted_set-destroy_empty) +- [`length(set)`](#sorted_set-length) +- [`is_empty(set)`](#sorted_set-is_empty) +- [`head(set)`](#sorted_set-head) +- [`tail(set)`](#sorted_set-tail) +- [`pop_front(set)`](#sorted_set-pop_front) +- [`pop_back(set)`](#sorted_set-pop_back) +- [`keys(set)`](#sorted_set-keys) + +Macros + +- [`from_keys_by!(keys, lt)`](#sorted_set-from_keys_by) +- [`from_keys!(keys)`](#sorted_set-from_keys) +- [`from_sorted_keys_by!(keys, lt)`](#sorted_set-from_sorted_keys_by) +- [`from_sorted_keys!(keys)`](#sorted_set-from_sorted_keys) +- [`contains_by!(set, key, lt)`](#sorted_set-contains_by) +- [`contains!(set, key)`](#sorted_set-contains) +- [`add_by!(set, key, lt)`](#sorted_set-add_by) +- [`add!(set, key)`](#sorted_set-add) +- [`upsert_by!(set, key, lt)`](#sorted_set-upsert_by) +- [`upsert!(set, key)`](#sorted_set-upsert) +- [`remove_by!(set, key, lt)`](#sorted_set-remove_by) +- [`remove!(set, key)`](#sorted_set-remove) +- [`find_next_by!(set, key, include, lt)`](#sorted_set-find_next_by) +- [`find_next!(set, key, include)`](#sorted_set-find_next) +- [`find_prev_by!(set, key, include, lt)`](#sorted_set-find_prev_by) +- [`find_prev!(set, key, include)`](#sorted_set-find_prev) +- [`next_key_by!(set, key, lt)`](#sorted_set-next_key_by) +- [`next_key!(set, key)`](#sorted_set-next_key) +- [`prev_key_by!(set, key, lt)`](#sorted_set-prev_key_by) +- [`prev_key!(set, key)`](#sorted_set-prev_key) +- [`keys_from_by!(set, from, include, limit, lt)`](#sorted_set-keys_from_by) +- [`keys_from!(set, from, include, limit)`](#sorted_set-keys_from) + +Errors + +- [`EEmpty`](#sorted_set-EEmpty) +- [`EKeysNotSorted`](#sorted_set-EKeysNotSorted) +- [`ENotEmpty`](#sorted_set-ENotEmpty) + +#### Types [!toc] [#sorted_set-Types] + + +Internal membership marker so a set can reuse `SortedMap`. A zero-field empty struct carrying no data — it serializes to 1 BCS byte and has nothing to read or destructure. + +Declared `copy, drop, store` so `SortedSet` has exactly the abilities `K` does. It is `public` only because it appears in the forced-public accessors' signatures (macro hygiene); consumers never construct it or read it. Its layout is frozen at first publish and must stay a zero-field empty struct. + + + +An ordered set of unique keys, backed by one sorted vector via `SortedMap`. + +A pure value — no `UID`, no dynamic fields — so it embeds directly in an integrator's object, exactly like `sui::vec_set::VecSet`. Because the value is always the `copy + drop + store` `Unit`, `SortedSet` has exactly the abilities `K` does: with a `copy + drop + store` key it is `copy + drop + store`; with a non-`drop` (e.g. resource) key it is store-only and must be drained then `destroy_empty`'d. + +Across every public operation, the keys are strictly increasing under the (consistently supplied) comparator: sorted, no duplicates. + + +#### Functions [!toc] [#sorted_set-Functions] + + +Creates a new, empty set. Takes no `&mut TxContext`: a `SortedSet` is a value, not an object. `length` is 0 and `is_empty` is true. + + + +Returns a set containing exactly the one given key. + + + +Destroys an empty set. + +Only needed when `K` lacks `drop` (e.g. a resource key): such a `SortedSet` is itself non-`drop` and cannot fall out of scope, so drain every key via `remove!`/`pop_front`/`pop_back` first, then call this. A set of droppable keys never needs it. + +Aborts with `ENotEmpty` if the set still holds keys. The abort fires at this module's location, not the wrapped map's. + + + +Returns the number of distinct keys. + + + +Returns `true` iff the set holds no keys. + + + +Returns the smallest key under the comparator, or `none` if the set is empty. O(1). With a reverse comparator this is the largest numeric key. + + + +Returns the largest key under the comparator, or `none` if the set is empty. O(1). + + + +Removes and returns the smallest key. Length decreases by 1. O(N): it shifts every remaining entry (`pop_back` is O(1)), so a front-heavy drain loop is quadratic. + +Aborts with `EEmpty` if the set is empty, at this module's location (the wrapped map's `EEmpty` is never reached through here). + + + +Removes and returns the largest key. Length decreases by 1. O(1). + +Aborts with `EEmpty` if the set is empty, at this module's location (the wrapped map's `EEmpty` is never reached through here). + + + +Returns all keys in strict ascending (comparator) order as an owned `vector` — sorted and duplicate-free. Not a reference: the set stores `Entry` internally, so there is no `vector` to borrow (contrast `vec_set::keys`). + +O(N) in output size with no limit — the one read whose result scales with N. For large or near-ceiling sets, prefer the paged `keys_from!`. + + +#### Macros [!toc] [#sorted_set-Macros] + + +Builds a set from a vector of keys by idempotent insertion, under `$lt`. O(N^2) (one search per key). Duplicates are silently collapsed — the result holds each distinct key once, in comparator order, so the result's `length` is the number of distinct keys, not the input length. This diverges from `sui::vec_set::from_keys`, which aborts on a duplicate; to reject duplicates instead, build then assert `length` equals the input length. + +Under a coarse (non-injective) comparator, byte-distinct compare-equal keys collapse to one element keeping the last one's bytes (each re-insert is a last-write-wins upsert). + + + +`from_keys_by!` with the built-in integer `<`. De-duplicates — see [`from_keys_by!`](#sorted_set-from_keys_by). Returns a set of the distinct keys, in ascending order. + + + +Builds a set from keys that are already sorted (non-decreasing) under `$lt`. O(N): one pass validates each adjacent pair and appends at the back — no per-element search — so prefer this over the O(N^2) `from_keys!` when the input is pre-sorted. + +De-duplicates exactly like `from_keys!`: a run of compare-equal keys collapses to one element, keeping the last key's bytes (observable only under a coarse, non-injective comparator), matching `upsert!`'s last-write-wins rule. A set has no value to lose, so a duplicate collapses rather than aborts — a divergence from the map's `from_sorted_keys_values!`, which aborts on a duplicate to conserve values. The only rejection is genuinely unsorted input; if your keys are not yet ordered, use `from_keys!` or sort them first. + +Aborts with `EKeysNotSorted` if `keys` has an adjacent pair not sorted under `lt` (a strictly decreasing pair), at this module's location. + + + +`from_sorted_keys_by!` with the built-in integer `<`. De-duplicates — see [`from_sorted_keys_by!`](#sorted_set-from_sorted_keys_by). Returns a set of the distinct keys, in ascending order. + +Aborts with `EKeysNotSorted` if `keys` is not ascending. + + + +Returns `true` iff `key` is present, under `$lt`. Pure, total read (never aborts). O(log N). + + + +`contains_by!` with the built-in integer `<`. Returns `true` iff `key` is present. + + + +Inserts `key`, under `$lt`, aborting if it is already present (length + 1; `contains!(key)` flips false to true). Nothing is returned. The strict counterpart to the total `upsert_by!`: this matches `sui::vec_set::insert`, which also aborts on a duplicate, so a duplicate is a caller bug rather than a silent no-op. Use `upsert!`/`upsert_by!` when a duplicate should be absorbed silently (and reported via the returned bool) instead of aborting. + +Aborts with `sorted_map::EKeyAlreadyExists` if `key` is already present (delegated: the abort fires at the wrapped map's location). + + + +`add_by!` with the built-in integer `<`. + +Aborts with `sorted_map::EKeyAlreadyExists` if `key` is already present. + + + +Inserts `key`, under `$lt`. Idempotent and total (never aborts); `key` is taken by value. On a fresh insert: length + 1 and `contains!(key)` flips false to true. On a duplicate: length unchanged, but the stored key is overwritten with this one (last-write-wins for the key bytes, observable only under a coarse comparator). Returns `true` iff the key was newly added, `false` if it was already present. + +Diverges from `vec_set::insert` (which aborts on a duplicate); for that behavior call `add!`, or assert on the returned bool. + + + +`upsert_by!` with the built-in integer `<`. Returns `true` iff the key was newly added. + + + +Removes `key`, under `$lt`, and returns the removed key. On success: length - 1 and `contains!(key)` flips true to false. Matches `vec_set::remove`, which also aborts when the key is absent. + +Aborts with `sorted_map::EKeyNotFound` if `key` is absent (delegated: the abort fires at the wrapped map's location). + + + +`remove_by!` with the built-in integer `<`. Returns the removed key. + +Aborts with `sorted_map::EKeyNotFound` if `key` is absent. + + + +Returns the smallest key `>= key` when `include` is true (the ceiling), else the smallest key `> key` (strict next); `none` if there is no such key. Pure, total read; any returned key satisfies `contains!`. + + + +`find_next_by!` with the built-in integer `<`. Returns the ceiling/strict-next key, or `none`. + + + +Returns the largest key `<= key` when `include` is true (the floor), else the largest key `< key` (strict prev); `none` if there is no such key. Pure, total read; any returned key satisfies `contains!`. + + + +`find_prev_by!` with the built-in integer `<`. Returns the floor/strict-prev key, or `none`. + + + +Returns the smallest key strictly greater than `key`, or `none`. Sugar for `find_next_by!` with `include == false`. `next_key!` of the tail returning `none` is the forward-cursor termination signal. + + + +`next_key_by!` with the built-in integer `<`. Returns the strict-next key, or `none`. + + + +Returns the largest key strictly less than `key`, or `none`. Sugar for `find_prev_by!` with `include == false`. `prev_key!` of the head returning `none` is the backward-cursor termination signal. + + + +`prev_key_by!` with the built-in integer `<`. Returns the strict-prev key, or `none`. + + + +Returns up to `limit` keys in strict ascending order — a contiguous run starting at the first key `>= from` (when `include`) or `> from` (strict); fewer than `limit` only at the tail. Resume a page by passing the last returned key back as `from` with `include == false`: successive pages have no overlap and no gap. `limit == 0`, an empty set, or `from` past the tail all yield the empty vector. + + + +`keys_from_by!` with the built-in integer `<`. Returns up to `limit` keys in ascending order. + + +#### Errors [!toc] [#sorted_set-Errors] + + +Raised when `pop_front`/`pop_back` is called on an empty set. Asserted at this module's location — distinct from the wrapped map's `EEmpty`. + + + +Raised when a sorted constructor (`from_sorted_keys!`/`from_sorted_keys_by!`) receives keys that are not sorted under the comparator — a strictly decreasing adjacent pair. Asserted at this module's location. + + + +Raised when `destroy_empty` is called on a set that still holds keys. Asserted at this module's location — distinct from the wrapped map's `ENotEmpty`. + + +#### Internal helpers [!toc] [#sorted_set-Internals] + + +The functions `inner`, `inner_mut`, `unit`, and `assert_sorted` are `public` only because Move 2024 macro hygiene requires every symbol a macro body references to be public at the consumer's expansion site. They are not part of the supported API. In particular, `inner_mut` hands out `&mut SortedMap`: driving map operations on it directly with an inconsistent comparator (or `insert_at`/`remove_at` at a wrong index) can desort the set and invalidate every subsequent lookup, insertion, and removal. The corruption is order-only and local to that one set — no value is ever lost. Use the macro API instead. + diff --git a/content/contracts-sui/1.x/collections.mdx b/content/contracts-sui/1.x/collections.mdx new file mode 100644 index 00000000..e2832fc1 --- /dev/null +++ b/content/contracts-sui/1.x/collections.mdx @@ -0,0 +1,112 @@ +--- +title: Collections +--- + +The `openzeppelin_collections` package is the ordered-collections family for OpenZeppelin Contracts for Sui. It provides `SortedMap`, an ordered key/value collection kept in one sorted vector, and `SortedSet`, a thin set wrapper over `SortedMap` — the same relationship as Rust's `BTreeSet` over `BTreeMap`. + +Both are UID-less value types, shaped like `sui::vec_map::VecMap` and `sui::vec_set::VecSet` but kept in key order: you embed them as fields in your own `has key` objects, and every operation touches exactly one stored object. Beyond point lookups they answer ordered questions — head/tail, floor/ceiling, next/previous key, and sorted pages via `keys_from!`. + +Order is comparator-driven. The bare macros (`upsert!`, `contains!`, `remove!`, ...) use the built-in integer `<` for unsigned-integer keys; the `_by` variants take a strict less-than comparator for custom key types or descending order. + + +Collections store no comparator. Order is defined per call by the `lt` closure you supply to the `_by` macros, and it must be a strict total order threaded consistently to every call on a given collection. The library cannot detect a violation: a non-strict (`<=`) or inconsistent comparator silently corrupts order — duplicate inserts, missed removes, wrong membership answers. For integer keys, the bare (non-`_by`) macros remove this footgun entirely. + + +## Usage + +The package is not yet published to the Move Registry (MVR), so install it as a git dependency pinned to the release tag in `Move.toml`: + +```toml +[dependencies] +openzeppelin_collections = { git = "https://github.com/OpenZeppelin/contracts-sui.git", subdir = "collections", rev = "v1.5.0" } +``` + +Import the module you need: + +```move +use openzeppelin_collections::sorted_map; +// or +use openzeppelin_collections::sorted_set; +``` + +## Examples + +### Price book with a `SortedMap` + +A shared book embeds a `SortedMap` (price to resting size). `u64` keys sort under the built-in integer `<`, so every call uses the bare macros. + +```move +module my_protocol::price_book; + +use openzeppelin_collections::sorted_map::{Self, SortedMap}; + +public struct PriceBook has key { + id: UID, + levels: SortedMap, // price -> resting size, ascending (best = head) +} + +public fun create_and_share(ctx: &mut TxContext) { + let book = PriceBook { id: object::new(ctx), levels: sorted_map::new() }; + transfer::share_object(book); +} + +/// Add `size` at `price`, merging into an existing level if present. +public fun place(book: &mut PriceBook, price: u64, size: u64) { + if (book.levels.contains!(&price)) { + let level = book.levels.borrow_mut!(&price); + *level = *level + size; + } else { + book.levels.upsert!(price, size); + }; +} + +/// Best (lowest) price, or `none` if the book is empty. +public fun best_price(book: &PriceBook): Option { + book.levels.head() +} + +/// Up to `limit` prices ascending from the first price `>= from`. Resume a page by +/// passing the last returned price back as `from` with `include = false`. +public fun page(book: &PriceBook, from: u64, include: bool, limit: u64): vector { + book.levels.keys_from!(&from, include, limit) +} +``` + +### Watchlist with a `SortedSet` + +A watchlist embeds a `SortedSet` and uses `upsert!`'s `bool` return — `true` only on a fresh insert, never aborting on a duplicate — to emit an event exactly the first time an id is watched. + +```move +module my_app::watchlist; + +use openzeppelin_collections::sorted_set::{Self, SortedSet}; +use sui::event; + +public struct Watchlist has key { + id: UID, + ids: SortedSet, +} + +public struct IdWatched has copy, drop { id: u64 } + +public fun create(ctx: &mut TxContext): Watchlist { + Watchlist { id: object::new(ctx), ids: sorted_set::new() } +} + +/// Add a token id; emit only the first time it is watched. +public fun watch(watchlist: &mut Watchlist, id: u64) { + if (watchlist.ids.upsert!(id)) { + event::emit(IdWatched { id }); + } +} +``` + +## Choosing between `sorted_map` and `sorted_set` + +- Use `sorted_map` when each key carries a value — price levels, tick registries, leaderboards, payout vaults. Values may be resources like `Coin`: drain every entry, then call `destroy_empty`. +- Use `sorted_set` when only ordered membership matters — watchlists, allow/deny lists, deduplicated id registries. Its `upsert!` returns a `bool` instead of aborting on duplicates. +- Both answer the same ordered queries (`head`/`tail`, `find_next!`/`find_prev!`, `keys_from!` pages) and keep a single-object footprint; byte size is the only capacity ceiling. + +## API Reference + +Use the full function-level reference here: [Collections API](/contracts-sui/1.x/api/collections). diff --git a/content/contracts-sui/1.x/index.mdx b/content/contracts-sui/1.x/index.mdx index 9bae3e79..c1e7c21a 100644 --- a/content/contracts-sui/1.x/index.mdx +++ b/content/contracts-sui/1.x/index.mdx @@ -2,10 +2,11 @@ title: Contracts for Sui 1.x --- -**OpenZeppelin Contracts for Sui v1.x** ships two core packages: +**OpenZeppelin Contracts for Sui v1.x** ships the following core packages: - `openzeppelin_math` for deterministic arithmetic, configurable rounding, and decimal scaling. - `openzeppelin_access` for ownership-transfer wrappers around privileged `key + store` objects. +- `openzeppelin_collections` for ordered key/value and set data structures (`sorted_map`, `sorted_set`). ## Quickstart @@ -71,5 +72,5 @@ sui move test ## Next Steps -- Read package guides: [Integer Math](/contracts-sui/1.x/math), [Access](/contracts-sui/1.x/access). -- Use API docs: [Integer Math](/contracts-sui/1.x/api/math), [Access](/contracts-sui/1.x/api/access). +- Read package guides: [Integer Math](/contracts-sui/1.x/math), [Access](/contracts-sui/1.x/access), [Collections](/contracts-sui/1.x/collections). +- Use API docs: [Integer Math](/contracts-sui/1.x/api/math), [Access](/contracts-sui/1.x/api/access), [Collections](/contracts-sui/1.x/api/collections). diff --git a/content/contracts-sui/index.mdx b/content/contracts-sui/index.mdx index 038dfaf9..054d72fa 100644 --- a/content/contracts-sui/index.mdx +++ b/content/contracts-sui/index.mdx @@ -26,6 +26,9 @@ import { latestStable } from "./latest-versions.js"; Package-level guide for access primitives, including transfer policy selection and safety models. + + Package-level guide for ordered data structures, including the comparator model and usage boundaries. + ## API Reference @@ -37,4 +40,7 @@ import { latestStable } from "./latest-versions.js"; Explore the complete access API reference, including module-level functions, core types, emitted events, and expected error conditions for integration. + + Explore the complete collections API, including all functions, macros, types, and expected error conditions for integration. + diff --git a/public/llms.txt b/public/llms.txt index 748e141d..0c09b525 100644 --- a/public/llms.txt +++ b/public/llms.txt @@ -314,8 +314,10 @@ Each ecosystem section lists the smart-contract libraries and language-specific - [Learn — Access Walkthrough](https://docs.openzeppelin.com/contracts-sui/1.x/learn/access-walkthrough) - [Packages — Integer Math](https://docs.openzeppelin.com/contracts-sui/1.x/math) - [Packages — Access](https://docs.openzeppelin.com/contracts-sui/1.x/access) +- [Packages — Collections](https://docs.openzeppelin.com/contracts-sui/1.x/collections) - [API Reference — Integer Math](https://docs.openzeppelin.com/contracts-sui/1.x/api/math) - [API Reference — Access](https://docs.openzeppelin.com/contracts-sui/1.x/api/access) +- [API Reference — Collections](https://docs.openzeppelin.com/contracts-sui/1.x/api/collections) ## Midnight diff --git a/src/navigation/sui/current.json b/src/navigation/sui/current.json index f2781876..a6d942c2 100644 --- a/src/navigation/sui/current.json +++ b/src/navigation/sui/current.json @@ -49,6 +49,11 @@ "type": "page", "name": "Access", "url": "/contracts-sui/1.x/access" + }, + { + "type": "page", + "name": "Collections", + "url": "/contracts-sui/1.x/collections" } ] }, @@ -65,6 +70,11 @@ "type": "page", "name": "Access", "url": "/contracts-sui/1.x/api/access" + }, + { + "type": "page", + "name": "Collections", + "url": "/contracts-sui/1.x/api/collections" } ] }