@rootzero/contracts 1.11.0 → 1.13.0

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/CHANGELOG.md CHANGED
@@ -3,6 +3,58 @@
3
3
  Until the protocol reaches integration-stable status, minor versions may include
4
4
  breaking API changes. Breaking changes are called out explicitly.
5
5
 
6
+ ## 1.13.0
7
+
8
+ ### Breaking Changes
9
+
10
+ - Replaced the virtual `Pipeline.settle(bytes transactions)` stream hook with
11
+ decoded settlement through `Settlement`. Pipeline implementations now provide
12
+ `debitAccount` and `creditAccount` hooks, while the pipeline decodes each
13
+ returned TRANSACTION block and settles it before dispatching the next step.
14
+ - Moved `DebitAccountHook` and `CreditAccountHook` from their command modules to
15
+ `core/Settlement.sol`. They remain available from the `Core.sol` and
16
+ `Endpoints.sol` package entry points.
17
+
18
+ ### Added
19
+
20
+ - Added the memory-backed `Reader` and `Readers` block-stream API with balance
21
+ and transaction unpackers, bounds and block validation, and positive
22
+ iteration through `more()`.
23
+ - Added the shared `Settlement` core mixin used by pipelines and `PortSettle` to
24
+ debit transaction sources and credit destinations.
25
+
26
+ ### Changed
27
+
28
+ - Unified pipeline and port transaction handling through `Settlement`,
29
+ including zero-account handling and zero-amount no-op settlement.
30
+
31
+ ## 1.12.0
32
+
33
+ ### Breaking Changes
34
+
35
+ - Removed `Keys.Local` and replaced `EndpointBase.localSchema(...)` with the
36
+ explicit `EndpointBase.schema(uint32 key, string body)` helper for
37
+ context-local endpoint schemas.
38
+ - Changed `Introduction` to emit the receiving `host` and introduced `peer`:
39
+ `Introduction(uint indexed host, uint peer, uint blocknum)`.
40
+ - Replaced `isDebitAccount`, `isCreditAccount`, `isAuthorize`, and
41
+ `isUnauthorize` helper predicates with internal command ID fields:
42
+ `debitAccountId`, `creditAccountId`, `authorizeId`, and `unauthorizeId`.
43
+
44
+ ### Added
45
+
46
+ - Added the `EndpointBase.schema(...)` helper for publishing local endpoint
47
+ schemas.
48
+ - Added the standard `#schema` block, `unpackSchema`, and an opt-in
49
+ `publishSchema` admin command for emitting schema claims from `#schema`
50
+ blocks.
51
+
52
+ ### Changed
53
+
54
+ - Updated the schema DSL documentation to allow any number of child blocks in
55
+ declaration order, including fixed fields before, after, or between child
56
+ blocks.
57
+
6
58
  ## 1.11.0
7
59
 
8
60
  ### Breaking Changes
package/Core.sol CHANGED
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: GPL-3.0-only
2
2
  pragma solidity ^0.8.33;
3
3
 
4
- // Aggregator: re-exports the core host, runtime, access, ledger, node-call, and validation layer.
4
+ // Aggregator: re-exports the core host, runtime, access, ledger, settlement, pipeline, node-call, and validation layer.
5
5
  // Import this file to bring the full rootzero host base layer into scope.
6
6
 
7
7
  import { AccessControl } from "./core/Access.sol";
@@ -13,6 +13,7 @@ import { CommandCalls, FailedCall, NodeCalls, PortCalls } from "./core/Calls.sol
13
13
  import { EndpointBase, Lane } from "./core/Endpoint.sol";
14
14
  import { Payable } from "./core/Payable.sol";
15
15
  import { Pipeline } from "./core/Pipeline.sol";
16
+ import { CreditAccountHook, DebitAccountHook, Settlement } from "./core/Settlement.sol";
16
17
  import { Portal } from "./core/Portal.sol";
17
18
  import { RecoverHook } from "./commands/Recover.sol";
18
19
  import { AssetAmount, AccountAsset, AccountAmount, HostAmount, HostAccountAsset, HostAccountAmount, Tx } from "./core/Types.sol";
package/Cursors.sol CHANGED
@@ -1,14 +1,14 @@
1
1
  // SPDX-License-Identifier: GPL-3.0-only
2
2
  pragma solidity ^0.8.33;
3
3
 
4
- // Aggregator: re-exports all block stream primitives (Cursors, Writers, Schema, Keys, Sizes).
4
+ // Aggregator: re-exports all block stream primitives (Cursors, Readers, Writers, Schema, Keys, Sizes).
5
5
  // Import this file to get access to the full block encoding/decoding surface in one import.
6
6
 
7
7
  import { AssetAmount, AccountAsset, AccountAmount, HostAmount, HostAccountAsset, HostAccountAmount, Tx } from "./core/Types.sol";
8
8
  import { Forms, Sizes } from "./blocks/Schema.sol";
9
9
  import { Keys } from "./blocks/Keys.sol";
10
10
  import { Schemas } from "./blocks/Schema.sol";
11
- import { Cursors, Cur } from "./blocks/Cursors.sol";
11
+ import { Cursors, Cur, Readers, Reader } from "./blocks/Cursors.sol";
12
12
  import { Writer, Writers, Hints } from "./blocks/Writers.sol";
13
13
 
14
14
 
package/Endpoints.sol CHANGED
@@ -9,11 +9,13 @@ import { Keys } from "./blocks/Keys.sol";
9
9
  import { CommandBase, CommandContext } from "./commands/Base.sol";
10
10
  import { EndpointBase, Lane } from "./core/Endpoint.sol";
11
11
  import { Payable } from "./core/Payable.sol";
12
+ import { CreditAccountHook, DebitAccountHook } from "./core/Settlement.sol";
12
13
 
13
14
  // Commands
15
+ import { Allocate, AllocateHook } from "./commands/Allocate.sol";
14
16
  import { Burn, BurnHook } from "./commands/Burn.sol";
15
- import { CreditAccount, CreditAccountHook } from "./commands/Credit.sol";
16
- import { DebitAccount, DebitAccountHook } from "./commands/Debit.sol";
17
+ import { CreditAccount } from "./commands/Credit.sol";
18
+ import { DebitAccount } from "./commands/Debit.sol";
17
19
  import { Deposit, DepositHook, DepositPayable, DepositPayableHook } from "./commands/Deposit.sol";
18
20
  import { Payout, PayoutHook } from "./commands/Payout.sol";
19
21
  import { Provision, ProvisionHook, ProvisionPayable, ProvisionPayableHook } from "./commands/Provision.sol";
@@ -31,6 +33,7 @@ import { DenyAssets, DenyAssetsHook } from "./commands/admin/DenyAssets.sol";
31
33
  import { Dismiss } from "./commands/admin/Dismiss.sol";
32
34
  import { ExecutePayable } from "./commands/admin/Execute.sol";
33
35
  import { Label } from "./commands/admin/Label.sol";
36
+ import { PublishSchema } from "./commands/admin/Schemas.sol";
34
37
  import { Unauthorize } from "./commands/admin/Unauthorize.sol";
35
38
 
36
39
  // Port endpoints
package/README.md CHANGED
@@ -89,8 +89,8 @@ request built for an EVM host is byte-for-byte the request a CosmWasm or Solana
89
89
  port would parse; what differs per chain is how a host *resolves* the
90
90
  identifiers inside, never how the bytes are laid out.
91
91
 
92
- Schemas can express more than flat fields: a block may end in nested child
93
- blocks (`#bytes as payload` names a run of raw dynamic bytes), items can be
92
+ Schemas can express more than flat fields: a block may contain any number of
93
+ nested child blocks (`#bytes as payload` names raw dynamic bytes), items can be
94
94
  marked `maybe` (optional) or `many` (a list), and aliases and dotted field
95
95
  paths give off-chain tooling presentation names without changing a single byte
96
96
  on the wire. The full schema language is specified in
@@ -121,7 +121,8 @@ const request = concat([
121
121
  encodeAmountBlock(usdc, 250_000_000n),
122
122
  encodeAmountBlock(dai, 250n * 10n ** 18n),
123
123
  ]);
124
- // deposit(request) returns two #balance blocks, one per #amount
124
+ // deposit(request) returns two #balance blocks in its state output and an
125
+ // empty transaction output
125
126
  ```
126
127
 
127
128
  Everything downstream keeps this shape: commands loop over request blocks,
@@ -216,6 +217,11 @@ struct CommandContext {
216
217
  }
217
218
  ```
218
219
 
220
+ Every command returns two block streams: `state`, which is threaded into the
221
+ next pipeline step, and `transactions`, which contains `#transaction` blocks
222
+ for the pipeline host to settle outside the state lane. Either stream may be
223
+ empty.
224
+
219
225
  The input carries instructions; the state carries live value. While a
220
226
  sequence of commands executes, `#balance` and `#custody` blocks in the state
221
227
  are the funds being moved — produced by one command, consumed by the next.
@@ -224,7 +230,9 @@ The standard `Deposit` mixin shows the canonical shape — open the input,
224
230
  loop the batch, call the hook, write the output run:
225
231
 
226
232
  ```solidity
227
- function deposit(CommandContext calldata c) external onlyCommand returns (bytes memory) {
233
+ function deposit(
234
+ CommandContext calldata c
235
+ ) external onlyCommand returns (bytes memory, bytes memory) {
228
236
  (Cur memory input, uint outputs) = openInput(c.input, descriptor);
229
237
  Writer memory output = Writers.allocBalances(outputs);
230
238
 
@@ -234,7 +242,7 @@ function deposit(CommandContext calldata c) external onlyCommand returns (bytes
234
242
  output.appendBalance(asset, amount);
235
243
  }
236
244
 
237
- return output.finish();
245
+ return (output.finish(), "");
238
246
  }
239
247
  ```
240
248
 
@@ -250,8 +258,10 @@ abstract contract MyCommand is CommandBase {
250
258
  (, descriptor) = command("myCommand", Keys.Empty, Keys.Amount, Keys.Balance, 0, false, false);
251
259
  }
252
260
 
253
- function myCommand(CommandContext calldata c) external onlyCommand returns (bytes memory) {
254
- // parse c.input, loop, return the output state run
261
+ function myCommand(
262
+ CommandContext calldata c
263
+ ) external onlyCommand returns (bytes memory, bytes memory) {
264
+ // parse c.input, loop, return the output state run and any transactions
255
265
  }
256
266
  }
257
267
  ```
@@ -259,8 +269,9 @@ abstract contract MyCommand is CommandBase {
259
269
  The standard commands cover the common ledger movements: `deposit` and
260
270
  `depositPayable` (external funds in), `withdraw` and `burn` (funds out),
261
271
  `debitAccount` and `creditAccount` (internal movements), `payout` (deliver
262
- state to other accounts), `provision` (allocate custody on another host), and
263
- `relayPayable` (hand a pipeline to another portal).
272
+ state to other accounts), `allocate` (turn balance state into custody),
273
+ `provision` (provision custody from an external allocation), and `relayPayable`
274
+ (hand a pipeline to another portal).
264
275
 
265
276
  ## Pipelines
266
277
 
@@ -272,14 +283,26 @@ step { uint target, uint resources, #bytes as request }
272
283
  ```
273
284
 
274
285
  Each step names a target command, the resources it may spend, and its request.
275
- The state threads through: whatever one command returns becomes the input
276
- state of the next, and the final state must be empty. This is the core of
277
- `Pipeline.pipe`:
286
+ The returned state threads into the next command and the final state must be
287
+ empty. Returned transactions do not enter the state lane; the pipeline passes
288
+ each decoded transaction to the shared settlement implementation before
289
+ running the next step. This is the core of `Pipeline.pipe`:
278
290
 
279
291
  ```solidity
280
292
  while (input.i < input.len) {
281
293
  (uint target, uint resources, bytes calldata request) = input.unpackStep();
282
- state = dispatch(target, account, state, request, useValue(budget, resources));
294
+ Reader memory transactions;
295
+ (state, transactions.source) = dispatch(
296
+ target,
297
+ account,
298
+ state,
299
+ request,
300
+ useValue(budget, resources)
301
+ );
302
+ while (transactions.more()) {
303
+ (bytes32 from, bytes32 to, bytes32 asset, uint amount) = transactions.unpackTransaction();
304
+ settle(from, to, asset, amount);
305
+ }
283
306
  }
284
307
  if (state.length != 0) revert UnexpectedState();
285
308
  ```
@@ -332,7 +355,7 @@ bytes and produce the same output bytes for every endpoint.
332
355
 
333
356
  Admin commands use the regular command shape but are gated to the host's admin
334
357
  account: trust management (`authorize`, `unauthorize`), guardian management
335
- (`appoint`, `dismiss`), naming (`label`), asset gating (`allowAssets`,
358
+ (`appoint`, `dismiss`), metadata (`label`, `publishSchema`), asset gating (`allowAssets`,
336
359
  `denyAssets`, `allowance`), lifecycle (`init`, `destroy`), and raw calls
337
360
  (`executePayable`). Guards go the other way: direct actions guardians can take
338
361
  without any command context — the default is `revoke`, which lets a guardian
@@ -353,11 +376,11 @@ names, access sets, balances — from logs alone, with no artifact files.
353
376
  Import from the package entry points rather than deep paths:
354
377
 
355
378
  - `@rootzero/contracts/Core.sol` — `Host`, access control, `Balances`,
356
- `Pipeline`, `Portal`, validator
379
+ `Settlement`, `Pipeline`, `Portal`, validator
357
380
  - `@rootzero/contracts/Endpoints.sol` — command, admin, port, guard, and query
358
381
  mixins and their hooks
359
- - `@rootzero/contracts/Cursors.sol` — `Cur` cursor reader, `Writers`, `Schemas`,
360
- `Keys`
382
+ - `@rootzero/contracts/Cursors.sol` — calldata `Cur`/`Cursors`, memory
383
+ `Reader`/`Readers`, `Writers`, `Schemas`, `Keys`
361
384
  - `@rootzero/contracts/Utils.sol` — `Ids`, `Nodes`, `Assets`, `Accounts`,
362
385
  layout and value helpers
363
386
  - `@rootzero/contracts/Events.sol` — protocol event contracts
@@ -17,7 +17,109 @@ struct Cur {
17
17
  uint len;
18
18
  }
19
19
 
20
+ /// @notice Mutable reader over a block stream stored in memory.
21
+ /// All positions (`i`) are byte offsets relative to the start of `source`.
22
+ struct Reader {
23
+ /// @dev Current read position, relative to the source start.
24
+ uint i;
25
+ /// @dev Memory bytes containing the complete source region.
26
+ bytes source;
27
+ }
28
+
20
29
  using Cursors for Cur;
30
+ using Readers for Reader;
31
+
32
+ /// @title Readers
33
+ /// @notice Memory block stream parser for the rootzero protocol.
34
+ /// A `Reader` advances through an existing `bytes memory` source without copying its contents.
35
+ /// Blocks are encoded as `[bytes4 key][bytes4 payloadLen][payload]`.
36
+ library Readers {
37
+ /// @dev The current block has a truncated header or payload, an unexpected key,
38
+ /// or a payload size outside the accepted range.
39
+ error InvalidBlock();
40
+
41
+ /// @notice Create a reader backed by a memory byte array.
42
+ /// @param source Memory bytes containing the block stream.
43
+ /// @return cur Reader positioned at the beginning of `source`.
44
+ function open(bytes memory source) internal pure returns (Reader memory cur) {
45
+ cur.source = source;
46
+ }
47
+
48
+ /// @notice Return whether the reader has consumed its entire source.
49
+ /// @param cur Reader whose position should be checked.
50
+ /// @return Whether `cur.i` equals the source length.
51
+ function done(Reader memory cur) internal pure returns (bool) {
52
+ return cur.i == cur.source.length;
53
+ }
54
+
55
+ /// @notice Return whether the reader has bytes left to consume.
56
+ /// @param cur Reader whose position should be checked.
57
+ /// @return Whether `cur.i` differs from the source length.
58
+ function more(Reader memory cur) internal pure returns (bool) {
59
+ return cur.i != cur.source.length;
60
+ }
61
+
62
+ /// @notice Validate and consume the current block, advancing `cur.i` past it.
63
+ /// @param cur Reader to advance.
64
+ /// @param key Expected block key.
65
+ /// @param min Minimum payload length.
66
+ /// @param max Maximum payload length; zero means unbounded.
67
+ /// @return abs Absolute memory address of the payload start.
68
+ function consume(
69
+ Reader memory cur,
70
+ bytes4 key,
71
+ uint min,
72
+ uint max
73
+ ) internal pure returns (uint abs) {
74
+ bytes memory source = cur.source;
75
+ uint i = cur.i;
76
+
77
+ if (i > source.length || source.length - i < Sizes.Header) revert InvalidBlock();
78
+
79
+ bytes4 current;
80
+ uint len;
81
+ assembly ("memory-safe") {
82
+ let header := mload(add(add(source, 0x20), i))
83
+ current := header
84
+ len := and(shr(192, header), 0xffffffff)
85
+ abs := add(add(source, 0x28), i)
86
+ }
87
+
88
+ if (current != key || len < min || (max != 0 && len > max)) revert InvalidBlock();
89
+ if (len > source.length - i - Sizes.Header) revert InvalidBlock();
90
+ cur.i = i + Sizes.Header + len;
91
+ }
92
+
93
+ /// @notice Consume a BALANCE block and return its fields.
94
+ /// @param cur Reader; advanced past the block.
95
+ /// @return asset Asset identifier.
96
+ /// @return amount Token amount.
97
+ function unpackBalance(Reader memory cur) internal pure returns (bytes32 asset, uint amount) {
98
+ uint abs = consume(cur, Keys.Balance, 64, 64);
99
+ assembly ("memory-safe") {
100
+ asset := mload(abs)
101
+ amount := mload(add(abs, 0x20))
102
+ }
103
+ }
104
+
105
+ /// @notice Consume a TRANSACTION block and return its fields.
106
+ /// @param cur Reader; advanced past the block.
107
+ /// @return from Source account identifier.
108
+ /// @return to Destination account identifier.
109
+ /// @return asset Asset identifier.
110
+ /// @return amount Token amount.
111
+ function unpackTransaction(
112
+ Reader memory cur
113
+ ) internal pure returns (bytes32 from, bytes32 to, bytes32 asset, uint amount) {
114
+ uint abs = consume(cur, Keys.Transaction, 128, 128);
115
+ assembly ("memory-safe") {
116
+ from := mload(abs)
117
+ to := mload(add(abs, 0x20))
118
+ asset := mload(add(abs, 0x40))
119
+ amount := mload(add(abs, 0x60))
120
+ }
121
+ }
122
+ }
21
123
 
22
124
  /// @title Cursors
23
125
  /// @notice Calldata block stream parser for the rootzero protocol.
@@ -493,6 +595,21 @@ library Cursors {
493
595
  return createBlock96(Keys.Custody, bytes32(host), asset, bytes32(amount));
494
596
  }
495
597
 
598
+ /// @notice Encode a TRANSACTION block.
599
+ /// @param from Source account identifier.
600
+ /// @param to Destination account identifier.
601
+ /// @param asset Asset identifier.
602
+ /// @param amount Transfer amount.
603
+ /// @return Encoded TRANSACTION block bytes.
604
+ function toTransactionBlock(
605
+ bytes32 from,
606
+ bytes32 to,
607
+ bytes32 asset,
608
+ uint amount
609
+ ) internal pure returns (bytes memory) {
610
+ return createBlock128(Keys.Transaction, from, to, asset, bytes32(amount));
611
+ }
612
+
496
613
  /// @notice Encode a STEP block.
497
614
  /// @param target Command target identifier.
498
615
  /// @param resources Packed resources assigned to the step.
@@ -726,6 +843,19 @@ library Cursors {
726
843
  cur.ensureAt(end);
727
844
  }
728
845
 
846
+ /// @notice Consume a SCHEMA block and return its fields.
847
+ /// @param cur Cursor; advanced past the SCHEMA block.
848
+ /// @return key Block key being defined.
849
+ /// @return body Schema DSL string describing the block payload body.
850
+ /// @return name Optional block alias.
851
+ function unpackSchema(Cur memory cur) internal pure returns (bytes4 key, string memory body, bytes32 name) {
852
+ uint end = cur.enter(Keys.Schema, 36 + Sizes.Header, 0);
853
+ key = cur.read4();
854
+ body = cur.unpackString();
855
+ name = cur.read32();
856
+ cur.ensureAt(end);
857
+ }
858
+
729
859
  /// @notice Consume a dynamic block with a single bytes32 payload.
730
860
  /// @param cur Cursor; advanced past the block.
731
861
  /// @param key Expected dynamic block key.
@@ -1326,6 +1456,18 @@ library Cursors {
1326
1456
  // Transform helpers
1327
1457
  // -------------------------------------------------------------------------
1328
1458
 
1459
+ /// @notice Consume a BALANCE block and scope its amount to a host.
1460
+ /// @param cur Cursor; advanced past the BALANCE block.
1461
+ /// @param host Host node ID to attach to the decoded balance.
1462
+ /// @return value Host-scoped balance amount.
1463
+ function unpackBalanceForHost(
1464
+ Cur memory cur,
1465
+ uint host
1466
+ ) internal pure returns (HostAmount memory value) {
1467
+ value.host = host;
1468
+ (value.asset, value.amount) = cur.unpackBalance();
1469
+ }
1470
+
1329
1471
  /// @notice Consume a RELAY block and encode its destination context payload.
1330
1472
  /// @param cur Cursor; advanced past the RELAY block.
1331
1473
  /// @param account Account identifier to embed in the destination context.
package/blocks/Keys.sol CHANGED
@@ -7,16 +7,6 @@ pragma solidity ^0.8.33;
7
7
  /// Custom block keys only need to be unique in the context where they are used;
8
8
  /// hosts may publish custom key meanings with the `Schema` event.
9
9
  library Keys {
10
- /// @notice Create a context-local block key.
11
- /// @dev Local keys are opaque tags for host- or endpoint-specific schemas.
12
- /// The caller is responsible for choosing values that are unique in the
13
- /// context where they are used and publishing their meaning with `Schema`.
14
- /// @param value Opaque local key value.
15
- /// @return Context-local block key.
16
- function local(uint32 value) internal pure returns (bytes4) {
17
- return bytes4(value);
18
- }
19
-
20
10
  /// @dev Empty / unset key.
21
11
  bytes4 constant Empty = bytes4(0);
22
12
  /// @dev Wildcard key used in discovery when any block stream is accepted.
@@ -71,6 +61,8 @@ library Keys {
71
61
  bytes4 constant Bounty = bytes4(keccak256("#bounty"));
72
62
  /// @dev Mutable node label - (uint id, bytes32 namespace, #string as name)
73
63
  bytes4 constant Label = bytes4(keccak256("#label"));
64
+ /// @dev Block schema publication - (bytes4 key, #string as body, bytes32 name)
65
+ bytes4 constant Schema = bytes4(keccak256("#schema"));
74
66
 
75
67
  /// @dev Structural status form - (uint code)
76
68
  bytes4 constant Status = bytes4(keccak256("#status"));
package/blocks/Schema.sol CHANGED
@@ -28,9 +28,9 @@ pragma solidity ^0.8.33;
28
28
  // are offchain projection metadata only and do not change runtime encoding
29
29
  // - child blocks resolve by alias in the active schema context; unresolved aliases are invalid
30
30
  // - schema strings describe the payload body only; the `Block` event carries the alias
31
- // - fixed fields are packed in declaration order
32
- // - blocks have fixed fields followed by a dynamic child-block tail
33
- // - child block tails are embedded directly, without an extra stream wrapper
31
+ // - items are encoded in declaration order
32
+ // - fixed fields are packed inline and any number of child blocks are embedded directly
33
+ // - child blocks may appear between fixed fields because each block carries its own length
34
34
  // - `#bytes` is a reserved child block that stores raw bytes and has no body
35
35
  // - `#string` is a reserved child block that stores UTF-8 string bytes and has no body
36
36
  // - generic lists use the stable key derived from `#list`
@@ -87,6 +87,7 @@ library Schemas {
87
87
  string constant Fee = "{ uint amount }";
88
88
  string constant Auth = "{ uint cid, uint deadline, #bytes as proof }";
89
89
  string constant Label = "{ uint id, bytes32 namespace, #string as name }";
90
+ string constant Schema = "{ bytes4 key, #string as body, bytes32 name }";
90
91
  string constant Bytes = "";
91
92
  string constant String = "";
92
93
  string constant List = "";
@@ -993,19 +993,27 @@ library Writers {
993
993
  appendCustody(writer, value.host, value.asset, value.amount);
994
994
  }
995
995
 
996
+ /// @notice Append a TRANSACTION block using separate field values.
997
+ /// @param writer Destination writer; `i` is advanced by `Sizes.Transaction`.
998
+ /// @param from Source account identifier.
999
+ /// @param to Destination account identifier.
1000
+ /// @param asset Asset identifier.
1001
+ /// @param amount Transfer amount.
1002
+ function appendTransaction(
1003
+ Writer memory writer,
1004
+ bytes32 from,
1005
+ bytes32 to,
1006
+ bytes32 asset,
1007
+ uint amount
1008
+ ) internal pure {
1009
+ appendBlock128(writer, Keys.Transaction, from, to, asset, bytes32(amount), 32);
1010
+ }
1011
+
996
1012
  /// @notice Append a TRANSACTION block from a struct.
997
1013
  /// @param writer Destination writer; `i` is advanced by `Sizes.Transaction`.
998
1014
  /// @param value Transfer record fields to encode.
999
1015
  function appendTransaction(Writer memory writer, Tx memory value) internal pure {
1000
- appendBlock128(
1001
- writer,
1002
- Keys.Transaction,
1003
- bytes32(value.from),
1004
- bytes32(value.to),
1005
- value.asset,
1006
- bytes32(value.amount),
1007
- 32
1008
- );
1016
+ appendTransaction(writer, value.from, value.to, value.asset, value.amount);
1009
1017
  }
1010
1018
 
1011
1019
  // -------------------------------------------------------------------------
@@ -0,0 +1,49 @@
1
+ // SPDX-License-Identifier: GPL-3.0-only
2
+ pragma solidity ^0.8.33;
3
+
4
+ import {CommandContext, CommandBase, Keys} from "./Base.sol";
5
+ import {HostAmount, Cursors, Cur, Writer, Writers} from "../Cursors.sol";
6
+
7
+ using Cursors for Cur;
8
+ using Writers for Writer;
9
+
10
+ /// @notice Shared allocation hook used by `Allocate`.
11
+ abstract contract AllocateHook {
12
+ /// @notice Override to allocate a live balance into custody on a host.
13
+ /// Called once per paired BALANCE state block and NODE request block.
14
+ /// Implementations should perform only the custody side effect; output
15
+ /// blocks are written by the caller.
16
+ /// @param account Caller's account identifier.
17
+ /// @param custody Host-scoped amount to allocate into custody.
18
+ function allocate(bytes32 account, HostAmount memory custody) internal virtual;
19
+ }
20
+
21
+ /// @title Allocate
22
+ /// @notice Command that allocates BALANCE state to custody on requested hosts.
23
+ /// Each BALANCE state block is paired with one NODE request block at the same
24
+ /// position; the output is a matching CUSTODY state stream.
25
+ abstract contract Allocate is CommandBase, AllocateHook {
26
+ bytes32 private immutable descriptor;
27
+
28
+ constructor() {
29
+ (, descriptor) = command("allocate", Keys.Balance, Keys.Node, Keys.Custody, 0, false, false);
30
+ }
31
+
32
+ /// @notice Allocate BALANCE state blocks to matching NODE request blocks.
33
+ /// @param c Command context; `c.state` must contain BALANCE blocks and
34
+ /// `c.input` must contain the same number of NODE blocks.
35
+ /// @return CUSTODY block stream matching the allocated balances.
36
+ /// @return Empty transaction stream.
37
+ function allocate(CommandContext calldata c) external onlyCommand returns (bytes memory, bytes memory) {
38
+ (Cur memory input, Cur memory state, uint outputs) = openCommand(c, descriptor);
39
+ Writer memory output = Writers.allocCustodies(outputs);
40
+
41
+ while (state.i < state.len) {
42
+ HostAmount memory custody = state.unpackBalanceForHost(input.unpackNode());
43
+ allocate(c.account, custody);
44
+ output.appendCustody(custody);
45
+ }
46
+
47
+ return (output.finish(), "");
48
+ }
49
+ }
package/commands/Burn.sol CHANGED
@@ -28,14 +28,15 @@ abstract contract Burn is CommandBase, BurnHook {
28
28
  /// @notice Burn each BALANCE block from the command state.
29
29
  /// @param c Command context; `c.state` must contain BALANCE blocks.
30
30
  /// @return Empty output state.
31
- function burn(CommandContext calldata c) external onlyCommand returns (bytes memory) {
31
+ /// @return Empty transaction stream.
32
+ function burn(CommandContext calldata c) external onlyCommand returns (bytes memory, bytes memory) {
32
33
  (Cur memory state, ) = openState(c.state, descriptor);
33
34
 
34
35
  while (state.i < state.len) {
35
36
  (bytes32 asset, uint amount) = state.unpackBalance();
36
37
  burn(c.account, asset, amount);
37
38
  }
38
- return "";
39
+ return ("", "");
39
40
  }
40
41
  }
41
42
 
@@ -3,47 +3,35 @@ pragma solidity ^0.8.33;
3
3
 
4
4
  import { CommandBase, CommandContext, Keys } from "./Base.sol";
5
5
  import { Cursors, Cur } from "../Cursors.sol";
6
+ import { CreditAccountHook } from "../core/Settlement.sol";
6
7
 
7
8
  using Cursors for Cur;
8
9
 
9
- abstract contract CreditAccountHook {
10
- /// @notice Override to credit externally managed funds to `account`.
11
- /// Called once per BALANCE block in state.
12
- /// @param account Destination account identifier.
13
- /// @param asset Asset identifier.
14
- /// @param amount Amount to credit.
15
- function creditAccount(bytes32 account, bytes32 asset, uint amount) internal virtual;
16
- }
17
-
18
10
  /// @title CreditAccount
19
11
  /// @notice Command that delivers BALANCE state blocks to an account via a virtual hook.
20
12
  /// Use for internally recording credits that have already been settled externally.
21
13
  abstract contract CreditAccount is CommandBase, CreditAccountHook {
22
14
  bytes32 private immutable descriptor;
23
- uint private immutable id;
15
+ uint internal immutable creditAccountId;
24
16
 
25
17
  constructor() {
26
- (id, descriptor) = command("creditAccount", Keys.Balance, Keys.Empty, Keys.Empty, 0, false, false);
27
- }
28
-
29
- /// @notice Return true if `candidate` is this command's credit account ID.
30
- function isCreditAccount(uint candidate) internal view returns (bool) {
31
- return candidate == id;
18
+ (creditAccountId, descriptor) = command("creditAccount", Keys.Balance, Keys.Empty, Keys.Empty, 0, false, false);
32
19
  }
33
20
 
34
21
  /// @notice Credit each BALANCE block from the command state to the command account.
35
22
  /// @param c Command context; `c.state` must contain BALANCE blocks.
36
23
  /// @return Empty output state.
24
+ /// @return Empty transaction stream.
37
25
  function creditAccount(
38
26
  CommandContext calldata c
39
- ) external onlyCommand returns (bytes memory) {
27
+ ) external onlyCommand returns (bytes memory, bytes memory) {
40
28
  (Cur memory state, ) = openState(c.state, descriptor);
41
29
 
42
30
  while (state.i < state.len) {
43
31
  (bytes32 asset, uint amount) = state.unpackBalance();
44
32
  creditAccount(c.account, asset, amount);
45
33
  }
46
- return "";
34
+ return ("", "");
47
35
  }
48
36
  }
49
37