@rootzero/contracts 0.9.3 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/Schema.md ADDED
@@ -0,0 +1,194 @@
1
+ # Schema
2
+
3
+ Rootzero request and response data is encoded as a stream of typed blocks. A
4
+ schema string describes payload layout for discovery events and tooling; the
5
+ runtime block key is derived only from the block name.
6
+
7
+ ## Wire Format
8
+
9
+ Every block uses the same header:
10
+
11
+ ```txt
12
+ [bytes4 key][uint32 payloadLen][payload]
13
+ ```
14
+
15
+ `payloadLen` is big-endian and counts only payload bytes. Child blocks and list
16
+ items use the same header format.
17
+
18
+ The block key is:
19
+
20
+ ```txt
21
+ bytes4(keccak256("#name"))
22
+ ```
23
+
24
+ For example, `#amount { bytes32 asset, bytes32 meta, uint amount }` uses the key
25
+ derived from `#amount`. Blocks must not be overloaded: one block name should have
26
+ one protocol meaning.
27
+
28
+ ## Block Syntax
29
+
30
+ A block starts with `#`. Fixed fields are written in braces:
31
+
32
+ ```txt
33
+ #amount { bytes32 asset, bytes32 meta, uint amount }
34
+ #account { bytes32 account }
35
+ ```
36
+
37
+ A block without braces has no payload:
38
+
39
+ ```txt
40
+ #unit
41
+ #bytes
42
+ ```
43
+
44
+ Empty braces are invalid. A zero-payload block must omit braces.
45
+
46
+ A schema is a comma-separated list of items. Order is significant.
47
+
48
+ ```txt
49
+ #amount { bytes32 asset, bytes32 meta, uint amount },
50
+ maybe #account { bytes32 account }
51
+ ```
52
+
53
+ ## Payload Layout
54
+
55
+ A block payload has fixed fields first, followed by an optional child-block tail.
56
+ Once a child block appears, no more fixed fields may follow.
57
+
58
+ ```txt
59
+ #call { uint target, uint value, #bytes as payload }
60
+ #context { bytes32 account, uint value, #bytes as state, #bytes as request }
61
+ ```
62
+
63
+ The tail is embedded directly as child block bytes. There is no wrapper around a
64
+ child-block tail.
65
+
66
+ Raw dynamic bytes are represented with the reserved `#bytes` child block. Use an
67
+ alias to give those bytes a presentation name:
68
+
69
+ ```txt
70
+ #bytes as payload
71
+ ```
72
+
73
+ ## Modifiers
74
+
75
+ Cardinality is expressed with prefix keywords:
76
+
77
+ ```txt
78
+ #balance { bytes32 asset, bytes32 meta, uint amount }
79
+ maybe #balance { bytes32 asset, bytes32 meta, uint amount }
80
+ many #balance { bytes32 asset, bytes32 meta, uint amount }
81
+ maybe many #balance { bytes32 asset, bytes32 meta, uint amount }
82
+ ```
83
+
84
+ - no prefix: one required item
85
+ - `maybe`: optional item
86
+ - `many`: one `#list` block whose payload contains repeated items
87
+ - `maybe many`: optional `#list` block
88
+
89
+ `maybe` emits no placeholder when absent. `many` wraps repeated items in one
90
+ generic list block; it does not repeat the item in place.
91
+
92
+ ## Prime Items
93
+
94
+ The empty string `""` means no schema. Whitespace-only schemas are invalid.
95
+
96
+ For a non-empty schema, the first top-level item is the prime item. Prime items
97
+ may repeat at the top level for batching. Later top-level items are globals for
98
+ the whole batch and are not counted as per-operation prime blocks.
99
+
100
+ The prime item cannot be optional. If a command needs a per-operation marker with
101
+ no payload, use a zero-payload block such as `#unit`.
102
+
103
+ ## Aliases
104
+
105
+ Aliases are presentation metadata for tooling. They do not change payload layout
106
+ or runtime keys.
107
+
108
+ ```txt
109
+ maybe #account { bytes32 account } as recipient
110
+ #call { uint target, uint value, #bytes as payload }
111
+ ```
112
+
113
+ Aliases may be used on any block item, including child blocks and prime items.
114
+
115
+ ## Field Types
116
+
117
+ Supported field types are chain-neutral:
118
+
119
+ ```txt
120
+ uint, uint8, uint16, uint32, uint64, uint128, uint256
121
+ int, int8, int16, int32, int64, int128, int256
122
+ bool
123
+ bytes1 through bytes32
124
+ ```
125
+
126
+ `uint` means `uint256`; `int` means `int256`. Other integer widths, unsized
127
+ `bytes`, `string`, and array syntax are not part of the core schema DSL.
128
+
129
+ Integers are encoded big-endian. Signed integers use two's-complement encoding
130
+ for their declared width. `bool` is one byte: `0x00` for false and `0x01` for
131
+ true. `bytesN` values are encoded as exactly `N` bytes with no padding.
132
+
133
+ ## Identifiers
134
+
135
+ Block names, field names, and aliases use lower camelCase ASCII identifiers:
136
+
137
+ ```txt
138
+ [a-z][a-zA-Z0-9]*
139
+ ```
140
+
141
+ Invalid examples:
142
+
143
+ ```txt
144
+ Amount
145
+ asset_meta
146
+ asset-meta
147
+ 0account
148
+ ```
149
+
150
+ Reserved words include `maybe`, `many`, `as`, all field type names, and the
151
+ reserved block names `bytes`, `data`, and `list`.
152
+
153
+ ## Reserved Blocks
154
+
155
+ - `#bytes`: raw dynamic bytes, written without a body
156
+ - `#data`: generic/custom payload block
157
+ - `#list`: generic list wrapper emitted by `many`
158
+
159
+ Use `#data` when a local schema needs a stable generic key:
160
+
161
+ ```txt
162
+ #data { uint foo, bytes32 tag }
163
+ #data { #bytes as payload }
164
+ ```
165
+
166
+ If a schema string starts with a fixed field type, it is shorthand for one
167
+ top-level `#data` block:
168
+
169
+ ```txt
170
+ uint foo, bytes32 tag
171
+ ```
172
+
173
+ expands to:
174
+
175
+ ```txt
176
+ #data { uint foo, bytes32 tag }
177
+ ```
178
+
179
+ ## Standard Blocks
180
+
181
+ Common protocol schemas live in `contracts/blocks/Schema.sol`:
182
+
183
+ ```txt
184
+ #amount { bytes32 asset, bytes32 meta, uint amount }
185
+ #balance { bytes32 asset, bytes32 meta, uint amount }
186
+ #custody { uint host, bytes32 asset, bytes32 meta, uint amount }
187
+ #payout { bytes32 account, bytes32 asset, bytes32 meta, uint amount }
188
+ #call { uint target, uint value, #bytes as payload }
189
+ #step { uint target, uint value, #bytes as request }
190
+ #context { bytes32 account, uint value, #bytes as state, #bytes as request }
191
+ #auth { uint cid, uint deadline, #bytes as proof }
192
+ ```
193
+
194
+ `Keys.sol` contains the corresponding runtime keys.
@@ -14,8 +14,7 @@ abstract contract CommandEvent is EventEmitter {
14
14
  /// @param shape Per-operation prime block counts encoded as `request:state:output`.
15
15
  /// Blocks outside the prime runs are global batch blocks and are excluded
16
16
  /// from the counts.
17
- /// @param request Schema DSL string describing the request shape; use `empty;...`
18
- /// when the request has global blocks but no prime blocks.
17
+ /// @param request Schema string describing the request shape.
19
18
  /// @param state Block key expected for input state, or `Keys.Empty`.
20
19
  /// @param output Block key produced for output state, or `Keys.Empty`.
21
20
  /// @param acceptsValue Whether the command entrypoint accepts nonzero `msg.value`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rootzero/contracts",
3
- "version": "0.9.3",
3
+ "version": "0.9.4",
4
4
  "description": "Solidity contracts and protocol building blocks for rootzero hosts and commands.",
5
5
  "private": false,
6
6
  "license": "GPL-3.0-only",
@@ -9,7 +9,7 @@
9
9
  "**/*.sol",
10
10
  "README.md",
11
11
  "LICENSE",
12
- "docs/GETTING_STARTED.md"
12
+ "docs/Schema.md"
13
13
  ],
14
14
  "publishConfig": {
15
15
  "access": "public"
@@ -27,7 +27,7 @@ abstract contract PeerAllowAssets is PeerBase, AllowAssetsHook {
27
27
  allowAsset(asset, meta);
28
28
  }
29
29
 
30
- assets.complete();
30
+ assets.close();
31
31
  return "";
32
32
  }
33
33
  }
@@ -29,7 +29,7 @@ abstract contract PeerAllowance is PeerBase, AllowanceHook {
29
29
  allowance(peer, asset, meta, amount);
30
30
  }
31
31
 
32
- amounts.complete();
32
+ amounts.close();
33
33
  return "";
34
34
  }
35
35
  }
@@ -37,7 +37,7 @@ abstract contract PeerBalancePull is PeerBase, BalancePullHook {
37
37
  balancePull(peer, asset, meta, amount);
38
38
  }
39
39
 
40
- input.complete();
40
+ input.close();
41
41
  return "";
42
42
  }
43
43
  }
@@ -27,7 +27,7 @@ abstract contract PeerDenyAssets is PeerBase, DenyAssetsHook {
27
27
  denyAsset(asset, meta);
28
28
  }
29
29
 
30
- assets.complete();
30
+ assets.close();
31
31
  return "";
32
32
  }
33
33
  }
package/peer/Pipe.sol ADDED
@@ -0,0 +1,38 @@
1
+ // SPDX-License-Identifier: GPL-3.0-only
2
+ pragma solidity ^0.8.33;
3
+
4
+ import {PeerBase} from "./Base.sol";
5
+ import {Pipeline} from "../core/Pipeline.sol";
6
+ import {Cursors, Cur, Schemas} from "../Cursors.sol";
7
+ import {Budget} from "../utils/Value.sol";
8
+
9
+ using Cursors for Cur;
10
+
11
+ /// @title PeerPipePayable
12
+ /// @notice Peer that consumes CONTEXT blocks and executes each context request as a STEP stream.
13
+ /// Each CONTEXT block carries an account, state, and request; the request is passed to the
14
+ /// shared pipeline as the step stream.
15
+ abstract contract PeerPipePayable is PeerBase, Pipeline {
16
+ string private constant NAME = "peerPipePayable";
17
+ uint internal immutable peerPipePayableId = peerId(NAME);
18
+
19
+ constructor() {
20
+ emit Peer(host, peerPipePayableId, NAME, "1:0", Schemas.Context, "", true);
21
+ }
22
+
23
+ /// @notice Execute peer-supplied contexts through the shared payable pipe.
24
+ /// @dev Each context receives its own explicit value sub-budget. Any top-level
25
+ /// `msg.value` not assigned to a context remains on this host.
26
+ function peerPipePayable(bytes calldata request) external payable onlyPeer returns (bytes memory) {
27
+ (Cur memory input, ) = cursor(request, 1);
28
+ Budget memory budget = valueBudget();
29
+
30
+ while (input.i < input.bound) {
31
+ (bytes32 account, uint value, bytes calldata state, bytes calldata steps) = input.unpackContext();
32
+ pipe(account, state, steps, allocateValue(budget, value));
33
+ }
34
+
35
+ input.close();
36
+ return "";
37
+ }
38
+ }
package/peer/Settle.sol CHANGED
@@ -26,7 +26,7 @@ abstract contract PeerSettle is PeerBase, TransferHook {
26
26
  transfer(state.unpackTxValue());
27
27
  }
28
28
 
29
- state.complete();
29
+ state.close();
30
30
  return "";
31
31
  }
32
32
  }
@@ -30,8 +30,8 @@ abstract contract IsAllowedAsset is QueryBase, IsAllowedAssetHook {
30
30
  }
31
31
 
32
32
  /// @notice Resolve allowlist status for a run of requested `(asset, meta)` tuples.
33
- /// @param request Block-stream request consisting of `asset(asset, meta)*`.
34
- /// @return Block-stream response containing one `status(ok)` per asset block.
33
+ /// @param request Block-stream request consisting of `#asset { bytes32 asset, bytes32 meta }` blocks.
34
+ /// @return Block-stream response containing one `#status { bool ok }` per asset block.
35
35
  function isAllowedAsset(bytes calldata request) external view returns (bytes memory) {
36
36
  (Cur memory query, uint groups) = cursor(request, 1);
37
37
  Writer memory response = Writers.allocStatuses(groups);
@@ -42,6 +42,7 @@ abstract contract IsAllowedAsset is QueryBase, IsAllowedAssetHook {
42
42
  response.appendStatus(allowed);
43
43
  }
44
44
 
45
- return query.complete(response);
45
+ query.close();
46
+ return response.finish();
46
47
  }
47
48
  }
@@ -42,6 +42,7 @@ abstract contract GetBalances is QueryBase, GetBalancesHook {
42
42
  response.appendAccountAmount(account, asset, meta, amount);
43
43
  }
44
44
 
45
- return query.complete(response);
45
+ query.close();
46
+ return response.finish();
46
47
  }
47
48
  }
@@ -6,11 +6,12 @@ import {Forms} from "../blocks/Schema.sol";
6
6
  import {QueryBase} from "./Base.sol";
7
7
 
8
8
  using Cursors for Cur;
9
+ using Writers for Writer;
9
10
 
10
11
  abstract contract GetPositionHook {
11
- /// @notice Resolve the position payload for one requested position.
12
- /// Concrete implementations must append exactly one `RESPONSE` block whose payload
13
- /// length matches `positionResponseSize`.
12
+ /// @notice Append the position response for one requested position.
13
+ /// Concrete implementations must append exactly one response block matching
14
+ /// the query output schema.
14
15
  /// @param account Requested account identifier.
15
16
  /// @param asset Requested asset identifier.
16
17
  /// @param meta Requested asset metadata slot.
@@ -26,31 +27,30 @@ abstract contract GetPositionHook {
26
27
  /// @title GetPosition
27
28
  /// @notice Rootzero query that resolves one dynamic position response for each requested position.
28
29
  /// The request is a run of `ACCOUNT_ASSET` form blocks.
29
- /// The response returns one dynamic `RESPONSE` block per position entry, preserving request order.
30
+ /// The response returns one output-schema block per position entry, preserving request order.
30
31
  abstract contract GetPosition is QueryBase, GetPositionHook {
31
32
  string private constant NAME = "getPosition";
33
+
32
34
  uint public immutable getPositionId = queryId(NAME);
33
- uint internal immutable positionResponseSize;
34
35
 
35
- constructor(string memory output, uint responseSize) {
36
- positionResponseSize = responseSize;
36
+ constructor(string memory output) {
37
37
  emit Query(host, getPositionId, NAME, "1:1", Forms.AccountAsset, output);
38
38
  }
39
39
 
40
40
  /// @notice Resolve positions for a run of requested `(account, asset, meta)` tuples.
41
- /// @dev Allocates from the configured fixed response payload length so each hook call
42
- /// can append one `RESPONSE` block directly into the output stream.
41
+ /// @dev Allocates from a per-block capacity hint and grows when position outputs exceed it.
43
42
  /// @param request Block-stream request consisting of `accountAsset(account, asset, meta)*`.
44
- /// @return Block-stream response containing one `response(bytes data)` block per position block.
43
+ /// @return Block-stream response containing one output-schema block per position block.
45
44
  function getPosition(bytes calldata request) external view returns (bytes memory) {
46
45
  (Cur memory query, uint groups) = cursor(request, 1);
47
- Writer memory response = Writers.allocBytes(groups, positionResponseSize);
46
+ Writer memory response = Writers.allocAny(groups);
48
47
 
49
48
  while (query.i < query.bound) {
50
49
  (bytes32 account, bytes32 asset, bytes32 meta) = query.unpackAccountAsset();
51
50
  appendPosition(account, asset, meta, response);
52
51
  }
53
52
 
54
- return query.complete(response);
53
+ query.close();
54
+ return response.finish();
55
55
  }
56
56
  }
package/utils/Value.sol CHANGED
@@ -8,17 +8,11 @@ struct Budget {
8
8
  }
9
9
 
10
10
  /// @title Values
11
- /// @notice Native-value (ETH) budget tracking for commands that accept `msg.value`.
11
+ /// @notice Native-value budget mutation helpers.
12
12
  library Values {
13
13
  /// @dev Thrown when a call attempts to spend more native value than remains in the budget.
14
14
  error InsufficientValue();
15
15
 
16
- /// @notice Create a budget from the current call's `msg.value`.
17
- /// @return Budget initialised with the full `msg.value`.
18
- function fromMsg() internal view returns (Budget memory) {
19
- return Budget({remaining: msg.value});
20
- }
21
-
22
16
  /// @notice Deduct `amount` from the budget and return it.
23
17
  /// Reverts if `amount` exceeds `budget.remaining`.
24
18
  /// @param budget Mutable budget to deduct from.
@@ -30,12 +24,12 @@ library Values {
30
24
  return amount;
31
25
  }
32
26
 
33
- /// @notice Deduct all remaining native value from the budget and return it.
34
- /// @param budget Mutable budget to drain.
35
- /// @return Remaining native value before the budget was emptied.
36
- function drain(Budget memory budget) internal pure returns (uint) {
37
- uint amount = budget.remaining;
38
- budget.remaining = 0;
39
- return amount;
27
+ /// @notice Deduct `amount` from the budget and return it as a new sub-budget.
28
+ /// Reverts if `amount` exceeds `budget.remaining`.
29
+ /// @param budget Mutable parent budget to deduct from.
30
+ /// @param amount Native value to assign to the sub-budget, in wei.
31
+ /// @return A new budget with `amount` remaining.
32
+ function allocate(Budget memory budget, uint amount) internal pure returns (Budget memory) {
33
+ return Budget({remaining: use(budget, amount)});
40
34
  }
41
35
  }
package/commands/Pipe.sol DELETED
@@ -1,67 +0,0 @@
1
- // SPDX-License-Identifier: GPL-3.0-only
2
- pragma solidity ^0.8.33;
3
-
4
- import {CommandBase, CommandContext, CommandPayable, Keys} from "./Base.sol";
5
- import {Cursors, Cur, Schemas} from "../Cursors.sol";
6
- import {Accounts} from "../utils/Accounts.sol";
7
- import {Budget, Values} from "../utils/Value.sol";
8
-
9
- using Cursors for Cur;
10
-
11
- abstract contract PipePayableHook {
12
- /// @notice Override to dispatch one piped command step.
13
- /// Called once per STEP block. The returned bytes become the state passed to
14
- /// the next step.
15
- /// @param id Command node ID to invoke or handle.
16
- /// @param account Account identifier for the piped command context.
17
- /// @param state Current threaded state block stream.
18
- /// @param request Step request block stream.
19
- /// @param value Native value assigned to this step.
20
- /// @return Updated state block stream for the next step.
21
- function dispatchCommand(
22
- uint id,
23
- bytes32 account,
24
- bytes memory state,
25
- bytes calldata request,
26
- uint value
27
- ) internal virtual returns (bytes memory);
28
- }
29
-
30
- /// @title PipePayable
31
- /// @notice Command that sequences multiple sub-command STEP invocations in a single transaction.
32
- /// Each STEP block carries a command node, native value to forward, and an embedded request.
33
- /// State threads through the steps: each step's output becomes the next step's state.
34
- /// Admin accounts are not permitted to use `pipePayable`.
35
- abstract contract PipePayable is CommandPayable, PipePayableHook {
36
- string private constant NAME = "pipePayable";
37
-
38
- uint internal immutable pipePayableId = commandId(NAME);
39
-
40
- constructor() {
41
- emit Command(host, pipePayableId, NAME, "1:0:0", Schemas.Step, Keys.Empty, Keys.Empty, true);
42
- }
43
-
44
- function pipe(
45
- bytes32 account,
46
- bytes memory state,
47
- bytes calldata steps,
48
- Budget memory budget
49
- ) internal returns (bytes memory) {
50
- (Cur memory input, ) = cursor(steps, 1);
51
-
52
- while (input.i < input.bound) {
53
- (uint target, uint value, bytes calldata request) = input.unpackStep();
54
- uint spend = Values.use(budget, value);
55
- state = dispatchCommand(target, account, state, request, spend);
56
- }
57
-
58
- settleValue(account, budget);
59
- input.complete();
60
- return state;
61
- }
62
-
63
- /// @notice Execute the pipePayable command.
64
- function pipePayable(CommandContext calldata c) external payable onlyCommand(c.account) returns (bytes memory) {
65
- return pipe(Accounts.ensureNotAdmin(c.account), c.state, c.request, Values.fromMsg());
66
- }
67
- }