@rootzero/contracts 0.9.2 → 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/Commands.sol +4 -3
- package/Core.sol +3 -0
- package/Cursors.sol +1 -1
- package/README.md +18 -24
- package/blocks/Cursors.sol +332 -335
- package/blocks/Keys.sol +38 -57
- package/blocks/Schema.sol +55 -114
- package/blocks/Writers.sol +361 -255
- package/commands/Base.sol +6 -48
- package/commands/Burn.sol +4 -4
- package/commands/Credit.sol +5 -4
- package/commands/Debit.sol +6 -5
- package/commands/Deposit.sol +17 -14
- package/commands/Provision.sol +17 -14
- package/commands/Transfer.sol +4 -4
- package/commands/Withdraw.sol +5 -4
- package/commands/admin/AllowAssets.sol +3 -3
- package/commands/admin/Allowance.sol +3 -3
- package/commands/admin/Authorize.sol +3 -3
- package/commands/admin/DenyAssets.sol +3 -3
- package/commands/admin/Destroy.sol +1 -1
- package/commands/admin/Execute.sol +9 -8
- package/commands/admin/Init.sol +1 -1
- package/commands/admin/Unauthorize.sol +3 -3
- package/core/Access.sol +11 -0
- package/core/Context.sol +11 -13
- package/core/Payable.sol +57 -0
- package/core/Pipeline.sol +55 -0
- package/docs/Schema.md +194 -0
- package/events/Admin.sol +5 -1
- package/events/Command.sol +6 -2
- package/events/Listing.sol +3 -4
- package/events/Peer.sol +5 -3
- package/events/Query.sol +5 -2
- package/package.json +2 -2
- package/peer/AllowAssets.sol +3 -3
- package/peer/Allowance.sol +3 -3
- package/peer/BalancePull.sol +43 -0
- package/peer/DenyAssets.sol +3 -3
- package/peer/Pipe.sol +38 -0
- package/peer/Settle.sol +3 -3
- package/queries/Assets.sol +7 -6
- package/queries/Balances.sol +5 -4
- package/queries/Positions.sol +14 -14
- package/utils/Value.sol +8 -14
- package/commands/Pipe.sol +0 -67
- package/docs/GETTING_STARTED.md +0 -294
- package/peer/AssetPull.sol +0 -43
package/queries/Balances.sol
CHANGED
|
@@ -26,15 +26,15 @@ abstract contract GetBalances is QueryBase, GetBalancesHook {
|
|
|
26
26
|
uint public immutable getBalancesId = queryId(NAME);
|
|
27
27
|
|
|
28
28
|
constructor() {
|
|
29
|
-
emit Query(host, getBalancesId, NAME, Forms.AccountAsset, Forms.AccountAmount);
|
|
29
|
+
emit Query(host, getBalancesId, NAME, "1:1", Forms.AccountAsset, Forms.AccountAmount);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
/// @notice Resolve balances for a run of requested `(account, asset, meta)` tuples.
|
|
33
33
|
/// @param request Block-stream request consisting of `accountAsset(account, asset, meta)*`.
|
|
34
34
|
/// @return Block-stream response containing one `accountAmount(account, asset, meta, amount)` block per request block.
|
|
35
35
|
function getBalances(bytes calldata request) external view returns (bytes memory) {
|
|
36
|
-
(Cur memory query, uint
|
|
37
|
-
Writer memory response = Writers.allocAccountAmounts(
|
|
36
|
+
(Cur memory query, uint groups) = cursor(request, 1);
|
|
37
|
+
Writer memory response = Writers.allocAccountAmounts(groups);
|
|
38
38
|
|
|
39
39
|
while (query.i < query.bound) {
|
|
40
40
|
(bytes32 account, bytes32 asset, bytes32 meta) = query.unpackAccountAsset();
|
|
@@ -42,6 +42,7 @@ abstract contract GetBalances is QueryBase, GetBalancesHook {
|
|
|
42
42
|
response.appendAccountAmount(account, asset, meta, amount);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
query.close();
|
|
46
|
+
return response.finish();
|
|
46
47
|
}
|
|
47
48
|
}
|
package/queries/Positions.sol
CHANGED
|
@@ -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
|
|
12
|
-
/// Concrete implementations must append exactly one
|
|
13
|
-
///
|
|
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
|
|
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
|
|
36
|
-
|
|
37
|
-
emit Query(host, getPositionId, NAME, Forms.AccountAsset, output);
|
|
36
|
+
constructor(string memory output) {
|
|
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
|
|
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
|
|
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
|
-
(Cur memory query, uint
|
|
47
|
-
Writer memory response = Writers.
|
|
45
|
+
(Cur memory query, uint groups) = cursor(request, 1);
|
|
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
|
-
|
|
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
|
|
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
|
|
34
|
-
///
|
|
35
|
-
/// @
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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, 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
|
-
}
|
package/docs/GETTING_STARTED.md
DELETED
|
@@ -1,294 +0,0 @@
|
|
|
1
|
-
# Getting Started With rootzero
|
|
2
|
-
|
|
3
|
-
This guide is for developers who want to build on rootzero without reading the whole codebase first.
|
|
4
|
-
|
|
5
|
-
If you remember only one thing, remember this:
|
|
6
|
-
|
|
7
|
-
- A `Host` is your application contract.
|
|
8
|
-
- A command is an entrypoint the rootzero runtime is allowed to call.
|
|
9
|
-
- Requests and responses are passed around as typed byte blocks.
|
|
10
|
-
|
|
11
|
-
## The Mental Model
|
|
12
|
-
|
|
13
|
-
rootzero moves data through a small command context:
|
|
14
|
-
|
|
15
|
-
```solidity
|
|
16
|
-
struct CommandContext {
|
|
17
|
-
bytes32 account;
|
|
18
|
-
bytes state;
|
|
19
|
-
bytes request;
|
|
20
|
-
}
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
In practice:
|
|
24
|
-
|
|
25
|
-
- `account` is the user account the command is acting for.
|
|
26
|
-
- `request` is the new input for this command.
|
|
27
|
-
- `state` is live pipeline state produced by an earlier command.
|
|
28
|
-
|
|
29
|
-
Most built-in commands follow a simple pattern:
|
|
30
|
-
|
|
31
|
-
- read blocks from `request` or `state`
|
|
32
|
-
- apply your host logic
|
|
33
|
-
- return new blocks
|
|
34
|
-
|
|
35
|
-
## Step 1: Start With A Host
|
|
36
|
-
|
|
37
|
-
The smallest useful rootzero app is a host contract.
|
|
38
|
-
|
|
39
|
-
```solidity
|
|
40
|
-
// SPDX-License-Identifier: MIT
|
|
41
|
-
pragma solidity ^0.8.33;
|
|
42
|
-
|
|
43
|
-
import {Host} from "@rootzero/contracts/Core.sol";
|
|
44
|
-
|
|
45
|
-
contract ExampleHost is Host {
|
|
46
|
-
constructor(address rootzero)
|
|
47
|
-
Host(rootzero, 1, "example")
|
|
48
|
-
{}
|
|
49
|
-
}
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
What the constructor arguments mean:
|
|
53
|
-
|
|
54
|
-
- `rootzero`: the trusted rootzero runtime allowed to call commands
|
|
55
|
-
- `1`: your host version
|
|
56
|
-
- `"example"`: your host namespace
|
|
57
|
-
|
|
58
|
-
If `rootzero` is a contract, the host announces itself there during deployment. If you pass `address(0)`, the host becomes self-managed and does not auto-register.
|
|
59
|
-
|
|
60
|
-
## Step 2: Reuse A Built-In Command
|
|
61
|
-
|
|
62
|
-
The easiest way to integrate is to inherit a built-in command module and implement its hook.
|
|
63
|
-
|
|
64
|
-
This example adds `DebitAccount`, which turns `AMOUNT` blocks in `request` into `BALANCE` blocks in the response:
|
|
65
|
-
|
|
66
|
-
```solidity
|
|
67
|
-
// SPDX-License-Identifier: MIT
|
|
68
|
-
pragma solidity ^0.8.33;
|
|
69
|
-
|
|
70
|
-
import {Host} from "@rootzero/contracts/Core.sol";
|
|
71
|
-
import {DebitAccount} from "@rootzero/contracts/Commands.sol";
|
|
72
|
-
import {Assets} from "@rootzero/contracts/Utils.sol";
|
|
73
|
-
|
|
74
|
-
contract ExampleHost is Host, DebitAccount {
|
|
75
|
-
mapping(bytes32 account => mapping(bytes32 assetKey => uint amount)) internal balances;
|
|
76
|
-
|
|
77
|
-
constructor(address rootzero)
|
|
78
|
-
Host(rootzero, 1, "example")
|
|
79
|
-
{}
|
|
80
|
-
|
|
81
|
-
function debitAccount(
|
|
82
|
-
bytes32 account,
|
|
83
|
-
bytes32 asset,
|
|
84
|
-
bytes32 meta,
|
|
85
|
-
uint amount
|
|
86
|
-
) internal override {
|
|
87
|
-
bytes32 key = Assets.slot(asset, meta);
|
|
88
|
-
balances[account][key] -= amount;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
Why this is a good first step:
|
|
94
|
-
|
|
95
|
-
- you do not need to write block parsing yourself
|
|
96
|
-
- you get the standard rootzero command surface
|
|
97
|
-
- you only implement the business rule that is unique to your app
|
|
98
|
-
|
|
99
|
-
## Step 3: Understand What Built-In Commands Consume
|
|
100
|
-
|
|
101
|
-
The built-in commands are easiest to use when you know which blocks they expect.
|
|
102
|
-
|
|
103
|
-
### Commands That Read `request`
|
|
104
|
-
|
|
105
|
-
- `deposit`: reads `AMOUNT` blocks, returns `BALANCE`
|
|
106
|
-
- `transfer`: reads `PAYOUT` blocks
|
|
107
|
-
- `debitAccount`: reads `AMOUNT`, returns `BALANCE`
|
|
108
|
-
- `provision`: reads `ALLOCATION`, returns `CUSTODY` state
|
|
109
|
-
- `pipePayable`: reads `STEP` blocks and runs them in order
|
|
110
|
-
|
|
111
|
-
### Commands That Read `state`
|
|
112
|
-
|
|
113
|
-
- `withdraw`: reads `BALANCE`, optionally reads `ACCOUNT` from `request`
|
|
114
|
-
- `creditAccount`: reads `BALANCE`, optionally reads `ACCOUNT` from `request`
|
|
115
|
-
|
|
116
|
-
This is the main pattern to keep in mind:
|
|
117
|
-
|
|
118
|
-
- use `request` for the command's direct input
|
|
119
|
-
- use `state` for live value threaded through a pipeline, currently `BALANCE` and `CUSTODY`
|
|
120
|
-
- use peer commands such as `peerSettle` for settlement messages such as `TRANSACTION`
|
|
121
|
-
|
|
122
|
-
## Step 4: Send A Simple Request
|
|
123
|
-
|
|
124
|
-
For a host that supports `deposit`, a request with one `AMOUNT` block is enough.
|
|
125
|
-
|
|
126
|
-
TypeScript helper example:
|
|
127
|
-
|
|
128
|
-
```ts
|
|
129
|
-
import { ethers } from "ethers";
|
|
130
|
-
import { encodeAmountBlock } from "../test/helpers/blocks.js";
|
|
131
|
-
|
|
132
|
-
const asset = ethers.zeroPadValue("0x01", 32);
|
|
133
|
-
const meta = ethers.ZeroHash;
|
|
134
|
-
const amount = 100n;
|
|
135
|
-
|
|
136
|
-
const ctx = {
|
|
137
|
-
account: "0x...", // 32-byte rootzero account id
|
|
138
|
-
state: "0x",
|
|
139
|
-
request: encodeAmountBlock(asset, meta, amount),
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
await host.deposit(ctx);
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
What happens:
|
|
146
|
-
|
|
147
|
-
1. `deposit` reads the `AMOUNT` block from `ctx.request`.
|
|
148
|
-
2. Your host applies its deposit logic.
|
|
149
|
-
3. The command returns one `BALANCE` block for each deposited amount.
|
|
150
|
-
|
|
151
|
-
## Step 5: Create A Custom Command
|
|
152
|
-
|
|
153
|
-
When the built-in modules are not enough, add your own command entrypoint.
|
|
154
|
-
|
|
155
|
-
```solidity
|
|
156
|
-
// SPDX-License-Identifier: MIT
|
|
157
|
-
pragma solidity ^0.8.33;
|
|
158
|
-
|
|
159
|
-
import {CommandBase, CommandContext, Keys} from "@rootzero/contracts/Commands.sol";
|
|
160
|
-
import {Cursors, Cur, Schemas} from "@rootzero/contracts/Cursors.sol";
|
|
161
|
-
|
|
162
|
-
using Cursors for Cur;
|
|
163
|
-
|
|
164
|
-
string constant NAME = "myCommand";
|
|
165
|
-
string constant ROUTE = "route(uint foo, uint bar)";
|
|
166
|
-
string constant INPUT = string.concat(ROUTE, "&", Schemas.Amount);
|
|
167
|
-
|
|
168
|
-
abstract contract MyCommand is CommandBase {
|
|
169
|
-
uint internal immutable myCommandId = commandId(NAME);
|
|
170
|
-
|
|
171
|
-
constructor() {
|
|
172
|
-
emit Command(host, myCommandId, NAME, INPUT, Keys.Empty, Keys.Balance, false);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function myCommand(
|
|
176
|
-
CommandContext calldata c
|
|
177
|
-
) external onlyCommand(c.account) returns (bytes memory) {
|
|
178
|
-
Cur memory input = cursor(c.request);
|
|
179
|
-
uint next = input.bundle();
|
|
180
|
-
|
|
181
|
-
bytes calldata route = input.unpackRaw(Keys.Route);
|
|
182
|
-
(bytes32 asset, bytes32 meta, uint amount) = input.unpackAmount();
|
|
183
|
-
input.ensure(next);
|
|
184
|
-
|
|
185
|
-
route;
|
|
186
|
-
return Cursors.toBalanceBlock(asset, meta, amount);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
There are three important ideas here:
|
|
192
|
-
|
|
193
|
-
- every custom command gets a deterministic command id
|
|
194
|
-
- you announce it with the `Command` event
|
|
195
|
-
- `onlyCommand(c.account)` ensures the caller is trusted and the calldata account matches `c.account`
|
|
196
|
-
|
|
197
|
-
## Step 6: Read Input With A Cursor
|
|
198
|
-
|
|
199
|
-
Cursor parsing is the nicest way to read structured command input.
|
|
200
|
-
|
|
201
|
-
If your request contains a bundled input like:
|
|
202
|
-
|
|
203
|
-
- `route(uint foo) & amount(bytes32 asset, bytes32 meta, uint amount)`
|
|
204
|
-
|
|
205
|
-
your command can:
|
|
206
|
-
|
|
207
|
-
- open it with `cursor(c.request)` or `Cursors.open(...)`
|
|
208
|
-
- consume the route first
|
|
209
|
-
- then consume the amount
|
|
210
|
-
- keep parsing in bundle/member order without indexing helpers
|
|
211
|
-
|
|
212
|
-
For simple projects, it is perfectly fine to:
|
|
213
|
-
|
|
214
|
-
- publish the full input schema string in the `Command` event
|
|
215
|
-
- encode bundled input blocks off-chain
|
|
216
|
-
- decode them sequentially with cursor helpers inside the command
|
|
217
|
-
|
|
218
|
-
## Step 7: Return State With Writers
|
|
219
|
-
|
|
220
|
-
When your command needs to build response blocks manually, use `Writers`.
|
|
221
|
-
|
|
222
|
-
```solidity
|
|
223
|
-
// SPDX-License-Identifier: MIT
|
|
224
|
-
pragma solidity ^0.8.33;
|
|
225
|
-
|
|
226
|
-
import {Writers, Writer} from "@rootzero/contracts/Cursors.sol";
|
|
227
|
-
|
|
228
|
-
using Writers for Writer;
|
|
229
|
-
|
|
230
|
-
function buildBalances() internal pure returns (bytes memory) {
|
|
231
|
-
Writer memory writer = Writers.allocBalances(2);
|
|
232
|
-
writer.appendBalance(bytes32(uint256(1)), bytes32(0), 50);
|
|
233
|
-
writer.appendBalance(bytes32(uint256(2)), bytes32(0), 75);
|
|
234
|
-
return writer.finish();
|
|
235
|
-
}
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
Use this when your command needs to return:
|
|
239
|
-
|
|
240
|
-
- balances
|
|
241
|
-
- custody state
|
|
242
|
-
|
|
243
|
-
If you are only consuming built-in commands, you often will not need to touch writers directly.
|
|
244
|
-
|
|
245
|
-
## Query Forms
|
|
246
|
-
|
|
247
|
-
Queries use reusable `Forms` as their input and output schema vocabulary. For example, `getBalances` accepts `Forms.AccountAsset` blocks and returns `Forms.AccountAmount` blocks:
|
|
248
|
-
|
|
249
|
-
```solidity
|
|
250
|
-
emit Query(host, NAME, Forms.AccountAsset, Forms.AccountAmount, getBalancesId);
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
This keeps query payloads structural: the query name describes what is being asked, while the form describes the fields carried by each block.
|
|
254
|
-
|
|
255
|
-
## A Tiny End-To-End Example
|
|
256
|
-
|
|
257
|
-
Imagine you want a host that keeps internal balances and lets rootzero debit them.
|
|
258
|
-
|
|
259
|
-
1. Deploy a host that inherits `Host` and `DebitAccount`.
|
|
260
|
-
2. Store balances in your own mapping.
|
|
261
|
-
3. Implement `debitAccount(account, asset, meta, amount)`.
|
|
262
|
-
4. Send `debitAccount` a request containing one or more `AMOUNT` blocks.
|
|
263
|
-
5. rootzero returns `BALANCE` blocks representing the debited amounts.
|
|
264
|
-
|
|
265
|
-
That is already a valid and useful integration.
|
|
266
|
-
|
|
267
|
-
## Which Files To Open Next
|
|
268
|
-
|
|
269
|
-
If you want to learn by example, these are the best files to read next:
|
|
270
|
-
|
|
271
|
-
- `examples/1-Host.sol`: smallest host
|
|
272
|
-
- `examples/2-Basic.sol`: host plus a built-in command hook
|
|
273
|
-
- `examples/3-Command.sol`: custom command id and command event
|
|
274
|
-
- `examples/4-Batch.sol`: batching request input and building balance output
|
|
275
|
-
- `examples/5-Route.sol`: bundled route input plus protocol blocks
|
|
276
|
-
- `test/commands.test.ts`: concrete request and response examples
|
|
277
|
-
- `test/helpers/blocks.ts`: block encoders you can reuse in off-chain tooling
|
|
278
|
-
|
|
279
|
-
## Common Mistakes
|
|
280
|
-
|
|
281
|
-
- Passing data in `state` when the command expects it in `request`
|
|
282
|
-
- Forgetting to emit a `Command` event for a custom command
|
|
283
|
-
- Using an admin account with user-only command flows such as `pipePayable`
|
|
284
|
-
- Trying to parse raw bytes manually when a built-in reader already exists
|
|
285
|
-
- Starting with a custom command when a built-in module already matches the job
|
|
286
|
-
|
|
287
|
-
## Recommended Learning Order
|
|
288
|
-
|
|
289
|
-
1. Deploy a plain `Host`.
|
|
290
|
-
2. Add one built-in command such as `DebitAccount` or `Deposit`.
|
|
291
|
-
3. Use the TypeScript block helpers to build requests.
|
|
292
|
-
4. Only then add a custom command with bundled input and cursor parsing.
|
|
293
|
-
|
|
294
|
-
That path keeps the first integration small and easy to debug.
|
package/peer/AssetPull.sol
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: GPL-3.0-only
|
|
2
|
-
pragma solidity ^0.8.33;
|
|
3
|
-
|
|
4
|
-
import {PeerBase} from "./Base.sol";
|
|
5
|
-
import {Cursors, Cur, Schemas} from "../Cursors.sol";
|
|
6
|
-
|
|
7
|
-
using Cursors for Cur;
|
|
8
|
-
|
|
9
|
-
abstract contract AssetPullHook {
|
|
10
|
-
/// @notice Override to process one incoming amount-based asset pull request from a peer host.
|
|
11
|
-
/// @param peer Peer host node ID for this request.
|
|
12
|
-
/// @param asset Requested asset identifier.
|
|
13
|
-
/// @param meta Requested asset metadata slot.
|
|
14
|
-
/// @param amount Requested amount in the asset's native units.
|
|
15
|
-
function assetPull(uint peer, bytes32 asset, bytes32 meta, uint amount) internal virtual;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/// @title PeerAssetPull
|
|
19
|
-
/// @notice Peer that pulls requested asset amounts from a peer host into this one.
|
|
20
|
-
/// Each AMOUNT block in the request calls `assetPull(peer, asset, meta, amount)`.
|
|
21
|
-
/// Restricted to trusted peers.
|
|
22
|
-
abstract contract PeerAssetPull is PeerBase, AssetPullHook {
|
|
23
|
-
string private constant NAME = "peerAssetPull";
|
|
24
|
-
uint internal immutable peerAssetPullId = peerId(NAME);
|
|
25
|
-
|
|
26
|
-
constructor() {
|
|
27
|
-
emit Peer(host, peerAssetPullId, NAME, Schemas.Amount, false);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/// @notice Execute the asset-pull peer call.
|
|
31
|
-
function peerAssetPull(bytes calldata request) external onlyPeer returns (bytes memory) {
|
|
32
|
-
(Cur memory assets, , ) = cursor(request, 1);
|
|
33
|
-
uint peer = caller();
|
|
34
|
-
|
|
35
|
-
while (assets.i < assets.bound) {
|
|
36
|
-
(bytes32 asset, bytes32 meta, uint amount) = assets.unpackAmount();
|
|
37
|
-
assetPull(peer, asset, meta, amount);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
assets.complete();
|
|
41
|
-
return "";
|
|
42
|
-
}
|
|
43
|
-
}
|