Add local instaswap E2E workspace harness

This commit is contained in:
Daniel Taghavi
2026-04-03 17:17:40 -04:00
commit 16d5eb3a86
17 changed files with 2625 additions and 0 deletions

79
.env.local.example Normal file
View File

@@ -0,0 +1,79 @@
COMPOSE_PROJECT_NAME=skunk-net
# Compose runtime user mapping. `./local-compose.sh` overwrites these with your
# current uid/gid at runtime so generated files are not owned by root.
LOCAL_UID=1000
LOCAL_GID=1000
# Branch-aligned repo paths. For coworker handoff, clone each dependency repo
# into these top-level directories and check out the branch recorded in
# WORKSPACE-MANIFEST.md.
WIRE_SYSIO_REPO_DIR=wire-sysio
WIRE_SYSIO_CONTEXT=./wire-sysio
WIRE_ETHEREUM_REPO_DIR=wire-ethereum
CAPITAL_STAKING_REPO_DIR=capital-staking
CAPITAL_STAKING_BUILD_CONTEXT=./capital-staking
WIRE_HUB_WEBAPP_REPO_DIR=wire-hub-webapp
# Wire SysIO
WIRE_SYSIO_IMAGE=wire/sysio:platform-dev-batchop-local
WIRE_NODE_ROOT=/srv/wire-node/chain-001
WIRE_HTTP_PORT=8887
WIRE_P2P_PORT=4443
WIRE_ETH_DEPOT_ACCOUNT=sysio.dpeth
WIRE_SOL_DEPOT_ACCOUNT=sysio.dpsol
WIRE_INSTASWAP_ACCOUNT=sysio.iswap
WIRE_WYIELD_ACCOUNT=sysio.wyield
WIRE_TEST_USER_ACCOUNT=userinsta1
WIRE_BATCH_OPERATOR_ETH_ACCOUNT=bopeth111111
WIRE_BATCH_OPERATOR_SOL_ACCOUNT=bopsol111111
WIRE_BATCH_OPERATOR_ETH_WIRE_PROVIDER_ID=wire-bopeth-1
WIRE_BATCH_OPERATOR_SOL_WIRE_PROVIDER_ID=wire-bopsol-1
# Solana / Anchor
CAPITAL_STAKING_IMAGE=skunk-net/capital-staking-dev:local
SOLANA_DOCKER_IMAGE=skunk-net/solana-validator:local
SOLANA_RPC=http://solana-validator:8899
RPC_URL=http://solana-validator:8899
ANCHOR_PROVIDER_URL=http://solana-validator:8899
ANCHOR_WALLET=/workspace/capital-staking/wallets/deploymentWallet/universalDeploymentWallet.json
REQUIRED_VALIDATORS=1
SOLANA_SLOTS_PER_EPOCH=64
SOLANA_GOSSIP_PORT=8001
SOLANA_RPC_PORT=8899
SOLANA_WS_PORT=8900
SOLANA_FAUCET_PORT=9900
SOLANA_RUN_SH_VALIDATOR_ARGS=
# Ethereum / Hardhat
HARDHAT_PORT=8545
# Batch operator relay
WIRE_BATCH_OPERATOR_WIRE_RPC_URL=http://wire-nodeop:8887
WIRE_BATCH_OPERATOR_POLL_INTERVAL_MS=5000
# ETH route operator
WIRE_BATCH_OPERATOR_ETH_WIRE_DEPOT_ACCOUNT=sysio.dpeth
WIRE_BATCH_OPERATOR_ETH_WIRE_ACCOUNT=
WIRE_BATCH_OPERATOR_ETH_WIRE_PROVIDER_ID=
WIRE_BATCH_OPERATOR_ETH_WIRE_PROVIDER_SPEC=
WIRE_BATCH_OPERATOR_ETH_CLIENT_ID=eth-local-1
WIRE_BATCH_OPERATOR_ETH_PROVIDER_ID=eth-local-1
WIRE_BATCH_OPERATOR_ETH_PROVIDER_SPEC=
WIRE_BATCH_OPERATOR_ETH_RPC_URL=http://hardhat:8545
WIRE_BATCH_OPERATOR_ETH_CHAIN_ID=31337
WIRE_BATCH_OPERATOR_ETH_ABI_FILE=/wire/.local/e2e/ethereum-outpost-abi.json
WIRE_BATCH_OPERATOR_ETH_OPP_ADDRESS=
WIRE_BATCH_OPERATOR_ETH_OPP_INBOUND_ADDRESS=
# SOL route operator
WIRE_BATCH_OPERATOR_SOL_WIRE_DEPOT_ACCOUNT=sysio.dpsol
WIRE_BATCH_OPERATOR_SOL_WIRE_ACCOUNT=
WIRE_BATCH_OPERATOR_SOL_WIRE_PROVIDER_ID=
WIRE_BATCH_OPERATOR_SOL_WIRE_PROVIDER_SPEC=
WIRE_BATCH_OPERATOR_SOL_CLIENT_ID=sol-local-1
WIRE_BATCH_OPERATOR_SOL_PROVIDER_ID=sol-local-1
WIRE_BATCH_OPERATOR_SOL_PROVIDER_SPEC=
WIRE_BATCH_OPERATOR_SOL_RPC_URL=http://solana-validator:8899
WIRE_BATCH_OPERATOR_SOL_PROGRAM_ID=
WIRE_BATCH_OPERATOR_SOL_IDL_FILE=/wire/capital-staking/target/idl/liqsol_core.json

10
.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
.env.local
.codex
.local/
.branch-worktrees/
capital-staking/
wire-ethereum/
wire-sysio/
wire-hub-webapp/
wire-cdt/
solana-docker-setup/

124
CHECKPOINT-2026-04-03.md Normal file
View File

@@ -0,0 +1,124 @@
# Checkpoint 2026-04-03
This checkpoint captures the latest live state of the local instant-swap E2E bring-up.
## What Is Working
- Clean end-to-end bootstrap through `./local-e2e-up.sh` after resetting local state.
- `wire-nodeop` comes up cleanly and the Wire batch operator services stay running.
- Real `Eth -> Wire` delivery is working through the live ETH batch operator.
- The Wire depot emits a real outbound `Wire -> Sol` epoch for the instaswap settlement.
- Solana `ingest_epoch` succeeds for that outbound epoch on the fresh chain.
## Current Blocker
The Solana batch operator fails on `process_messages` for the first live
`instaswap_settlement` message.
Observed error:
```text
Transaction simulation failed: Error processing Instruction 1:
custom program error: 0xbc4
```
`0xbc4` is decimal `3012`, which maps to Anchor `AccountNotInitialized`.
## Current Diagnosis
The transport path is not the blocker anymore.
What we proved on the fresh cluster:
- ETH emits `OPPMessage` / `OPPEpoch`
- ETH batch operator ingests that epoch into Wire
- Wire depot creates the `xswap`
- Wire depot emits outbound Solana epoch `0`
- Solana `ingest_epoch` accepts that epoch
The remaining failure is inside Solana settlement execution for
`handle_instaswap_settlement()` in:
- `capital-staking/programs/liqsol-core/src/instructions/opp/yield_hub.rs`
The most likely issue is one missing or wrongly-derived account in the
batch-operator `process_messages` remaining account list, or one account that
the live deploy path did not initialize even though the test harness does.
Primary suspects:
- `extra_account_meta_list`
- `distribution_state`
- `bridge_vault_ata`
- `bucket_token_account`
- `liqsol_mint`
## Reproduction
Fresh bring-up:
```bash
./local-compose.sh down -v
rm -rf .local/wire-node .local/wire-batch-operators .local/e2e
./local-e2e-up.sh
```
Trigger the live ETH instaswap:
```bash
./local-compose.sh exec -T hardhat bash -lc \
'cd /workspace/wire-ethereum && \
HH_SCRIPT_ARGS="instaswap 5.0000 1.0000 B3bVKtqAJxRp4RUbzmUXVUdF4CRtaHfjnNfKzCd9Fy7T" \
npx hardhat run src/scripts/outpost/wire_yield_hub_live.ts --network anvil_local'
```
Inspect the Solana batch operator log:
```bash
tail -n 120 .local/wire-batch-operators/sol/1/log/relay.log
```
Expected current failure:
```text
Submitting solana process_messages for epoch 0 with 1 message(s) and 15 remaining account(s)
batch operator pass failed: -32002 ... custom program error: 0xbc4
```
## Useful Live Checks
Wire chain health:
```bash
./local-compose.sh exec -T wire-nodeop clio -u http://127.0.0.1:8887 get info
```
Wire ETH depot inbound epochs:
```bash
./local-compose.sh exec -T wire-nodeop \
clio -u http://127.0.0.1:8887 get table sysio.dpeth ............2 oppepochin --limit 20
```
Wire xswap rows:
```bash
./local-compose.sh exec -T wire-nodeop \
clio -u http://127.0.0.1:8887 get table sysio.iswap sysio.iswap xswaps --limit 20
```
Wire Sol outbound epochs:
```bash
./local-compose.sh exec -T wire-nodeop \
clio -u http://127.0.0.1:8887 get table sysio.dpsol ............3 oppepochout --limit 20
```
## Suggested Next Step
Reproduce the failing Solana `process_messages` call with the same 15 remaining
accounts in a TypeScript helper or test so the full simulation logs identify the
exact uninitialized account. Once that account is known, patch either:
- the local Solana deploy/bootstrap path, or
- the batch operator account derivation / account ordering.

187
LOCAL-E2E.md Normal file
View File

@@ -0,0 +1,187 @@
# Local Docker-First Stack
This workspace can be run locally on Arch without shell-profile exports.
For the current live checkpoint, known blocker, and exact repo matrix, see
`CHECKPOINT-2026-04-03.md` and `WORKSPACE-MANIFEST.md`.
The setup below uses top-level dependency checkouts:
- `wire-sysio` in Docker, using the maintained Dockerfile under `wire-sysio/etc/docker/Dockerfile`
- `wire-ethereum` in a Node 20 container on a local Hardhat network
- `capital-staking` in a custom Anchor/Solana toolchain container against the local `solana-docker-setup` genesis validator
## What I Could Confirm
- The remote setup document at `swarm.gitgo.app/.../TESTNET-SETUP-README.md` returned `Not found` on April 2, 2026, so this runbook is based on the local repos and `OPP.pdf`.
- `OPP.pdf` is still useful for the protocol shape, but message encoding details are stale because the live code is moving to protobuf-backed payloads.
- `wire-ethereum` still models OPP delivery around sequential epoch envelopes.
- `capital-staking` is the Solana-side outpost/wire harness in this workspace.
- `solana-docker-setup` is now the intended local validator path so epoch timing is configurable through `SOLANA_SLOTS_PER_EPOCH`.
- `wire-sysio` contains the local node/bootstrap path plus the batch-operator relay implementation we are exercising in this workspace.
- The current Solana withdraw flow still has a placeholder instead of real OPP emission:
`capital-staking/programs/liqsol-core/src/instructions/wire_syndication/desyndicate_liqsol.rs`
## One-Time Setup
```bash
cd /home/dtaghavi/Documents/Projects/skunk-net
cp .env.local.example .env.local
```
Then use the checked-in wrapper:
```bash
./local-compose.sh ps
```
Nothing in this flow depends on `~/.bashrc`, and the wrapper injects your live uid/gid at runtime so container-generated files stay owned by your Arch user. Adjust `.env.local` if you want different ports, wallet path, or ledger location.
Important Solana knobs:
- `SOLANA_SLOTS_PER_EPOCH=64` keeps local epoch turnover fast for tests.
- `SOLANA_RUN_SH_VALIDATOR_ARGS` lets you append raw `agave-validator` flags from `.env.local`.
## Build The Containers
```bash
./local-compose.sh build capital-staking-dev wire-sysio-dev
```
If you also need to rebuild the Solana validator image explicitly:
```bash
./local-compose.sh build solana-validator
```
`wire-sysio-dev` is the expensive image because it builds the Wire toolchain and Clang 18 layer. The upstream Wire docs call out a 32 GiB RAM expectation for this build path.
## Solana / Capital Staking
Start the local validator:
```bash
./local-compose.sh up -d solana-validator
```
Check the local epoch cadence:
```bash
./local-compose.sh run --rm --no-deps capital-staking-dev bash -lc '
solana -u "$ANCHOR_PROVIDER_URL" epoch-info
'
```
Reset the local Solana ledger when you want a fresh cluster:
```bash
./local-compose.sh down -v
./local-compose.sh up -d solana-validator
```
Prepare program IDs, build, deploy, and initialize the local cluster:
```bash
./local-compose.sh run --rm capital-staking-dev bash -lc '
solana config set --url "$ANCHOR_PROVIDER_URL" --keypair "$ANCHOR_WALLET" &&
./bash-scripts/prep-anchor-toml.sh &&
npm ci &&
./bash-scripts/reset-local-cluster.sh --wait-for-cluster
'
```
Run the Wire-related Solana suites:
```bash
./local-compose.sh run --rm capital-staking-dev bash -lc '
solana config set --url "$ANCHOR_PROVIDER_URL" --keypair "$ANCHOR_WALLET" &&
npm ci &&
npm run test:wire-syndication &&
npm run test:wire-pretokens &&
npm run test:bar
'
```
Those are the closest existing local tests to the instant-swap / outpost lifecycle in this checkout.
## Ethereum / OPP
For contract-level OPP validation you can either run one-shot tests or keep a local Hardhat node up.
One-shot OPP tests:
```bash
./local-compose.sh run --rm wire-ethereum-dev bash -lc '
cd /workspace/wire-ethereum &&
npm ci &&
npx hardhat test src/test/outpost/OPPSend.ts src/test/outpost/OPPRecv.ts
'
```
Long-lived local JSON-RPC:
```bash
./local-compose.sh up -d hardhat
```
Then run additional tests or scripts from a disposable shell:
```bash
./local-compose.sh run --rm wire-ethereum-dev bash
```
Inside the container:
```bash
cd /workspace/wire-ethereum
npm ci
npx hardhat test src/test/outpost/Depositor.integration.ts
```
## Wire SysIO
Make sure the Wire submodules are present once on the host:
```bash
git -C wire-sysio submodule update --init --recursive
```
Bring up the local Wire node:
```bash
./local-compose.sh up wire-nodeop
```
That service now:
- builds the required `wire-sysio` targets inside Docker
- applies the local BoringSSL/OpenSSL pkg-config shim needed by this checkout
- runs `sys-util chain-configure`
- boots `kiod` and `nodeop` with the canonical 5-part `--signature-provider` format
If you want it detached:
```bash
./local-compose.sh up -d wire-nodeop
```
Check the node:
```bash
curl -s http://127.0.0.1:${WIRE_HTTP_PORT:-8887}/v1/chain/get_info -X POST
```
## Current Protocol Gap
From the code currently in this directory:
- Ethereum OPP is still epoch-based and sequential, but outbound epoch rollover no longer stalls on an unsubmitted prior epoch.
- Solana wire syndication is still mostly local-accounting plus admin completion hooks.
- The Solana PostLaunch withdraw path logs that it should send an OPP message, but does not do it yet.
- I do not see the inbound Wire-side OPP gate you described inside `wire-sysio` itself in this checkout.
That means the fastest path to a real end-to-end instant-swap test is:
1. Stand up the local Solana and Ethereum stacks above.
2. Use the existing wire-related Solana tests and OPP Ethereum tests as the starting harness.
3. Identify where the missing Wire-side depot / batch-operator logic actually lives if it is not in these repos.

377
OPP-COHESIVE-DESIGN.md Normal file
View File

@@ -0,0 +1,377 @@
# Cohesive OPP Design
## Problem
The current repos already show the shape of the system, but not a single transport model that works cleanly for all four directions:
- `wire-ethereum` has a real outbound OPP sender and a partially implemented inbound verifier.
- `capital-staking` has several concrete business flows and several OPP placeholders.
- `wire-sysio` has protobuf ABI support and chain identity types, but no shared OPP depot/runtime in this checkout.
The main failure mode in the current shape is coupling unrelated traffic through global epoch state. That is the wrong abstraction for instant swaps and for the broader outpost/depot model.
The correct abstraction is:
- four independent directed OPP routes:
- `Ethereum -> Wire`
- `Wire -> Ethereum`
- `Solana -> Wire`
- `Wire -> Solana`
- optional sub-lanes inside each route so BAR traffic cannot stall Depositor traffic
- consensus on matching submissions of the same batch data
- request/response correlation by payload ID, not by shared epoch numbering across directions
## Design Goals
- No global OPP epoch across chains or directions.
- Ordering is only required inside a single directed stream.
- Batching exists for operator consensus and gas/tx sizing, not as a business rule.
- Multiple batch operators can submit the same batch independently; the depot accepts the batch once matching weight reaches threshold.
- Reverse-direction flows are independent streams. A `Wire -> Solana` completion is not transport-coupled to the `Solana -> Wire` request batch that caused it.
- Payloads are canonical protobuf messages, even if some origin repos still need a temporary translator while migrating.
- Delivery is idempotent and replay-safe.
## Route And Stream Model
There are four mandatory top-level OPP routes:
| Route | Meaning |
| --- | --- |
| `Ethereum -> Wire` | Ethereum outposts mirror facts into Wire |
| `Wire -> Ethereum` | Wire depot sends commands or acknowledgements to Ethereum outposts |
| `Solana -> Wire` | Solana outposts mirror facts into Wire |
| `Wire -> Solana` | Wire depot sends commands or acknowledgements to Solana outposts |
Each route may contain one or more logical lanes. Lanes are required because the current business surfaces already split into different domains:
- `depositor`
- `pretoken`
- `bar`
- `admin`
The minimum stream identity must therefore be:
```text
stream_key = (
from_chain,
to_chain,
origin_system,
destination_system,
lane
)
```
Where:
- `from_chain` and `to_chain` use `chain_kind_t`
- `origin_system` is the origin contract/program/depot identifier
- `destination_system` is the destination contract/program/depot identifier
- `lane` is a stable logical channel inside the route
This keeps the four chain directions independent while still allowing multiple application lanes per directed pair.
## Transport Units
The transport has three layers:
1. `Assertion`
2. `Message`
3. `Batch`
Definitions:
- An `Assertion` is one business fact or command.
- A `Message` is a deterministic ordered set of assertions derived from one origin transaction or one origin-side decision.
- A `Batch` is a transport seal over a contiguous message range in one stream.
Important rule:
- `Batch` replaces the old global `epoch` concept.
- Batch numbering is per `stream_key`.
- Batches can be sealed by time, size, or explicit flush.
- Failing to deliver batch `N` on one stream must not stall batch creation or delivery on any other stream.
## Protobuf Canonical Format
The canonical transport format should be defined once and shared by all three repos. The simplest shape that fits the current codebase is:
```proto
syntax = "proto3";
package wire.opp.v1;
message StreamKey {
uint32 from_chain = 1;
uint32 to_chain = 2;
bytes origin_system = 3;
bytes destination_system = 4;
uint32 lane = 5;
}
message Assertion {
uint32 assertion_type = 1;
bytes payload = 2;
}
message Uint256 {
bytes be_bytes = 1;
}
message MessageHeader {
StreamKey stream = 1;
uint64 sequence = 2;
bytes previous_message_hash = 3;
uint64 source_timestamp_ms = 4;
bytes source_event_id = 5;
bytes payload_hash = 6;
}
message Message {
MessageHeader header = 1;
repeated Assertion assertions = 2;
}
message BatchHeader {
StreamKey stream = 1;
uint64 batch_number = 2;
bytes previous_batch_hash = 3;
uint64 first_sequence = 4;
uint64 last_sequence = 5;
uint64 sealed_at_ms = 6;
bytes merkle_root = 7;
}
message Batch {
BatchHeader header = 1;
repeated Assertion summary_assertions = 2;
}
message BatchSubmission {
BatchHeader header = 1;
bytes batch_hash = 2;
bytes operator_id = 3;
bytes signature = 4;
}
```
Notes:
- `assertion_type` stays numeric because the current Ethereum code and manager wiring already depend on a numeric registry.
- `payload` becomes a protobuf-encoded business message specific to that assertion type.
- `source_event_id` is the deterministic origin reference used for replay protection and migration from legacy encoders.
- Protobuf has no native `uint256`, so business payload schemas should use a canonical `Uint256` wrapper. The wrapper should be encoded as exactly 32 unsigned big-endian bytes.
## Consensus Model
Consensus happens at the destination depot or endpoint, not at the sender.
For each `(stream_key, batch_number)` the destination keeps:
- every operator submission keyed by `(operator_id, batch_hash)`
- cumulative weight per `batch_hash`
- a single accepted `batch_hash` once threshold is met
Rules:
- A batch is accepted when matching submission weight reaches threshold.
- Conflicting submissions for the same `(stream_key, batch_number)` are retained as evidence.
- Acceptance is per stream only. No other stream is affected.
- Operators may submit late. Late submission is a liveness concern, not a protocol deadlock.
This is the core point the current design needs: consensus is "multiple operators submitted the same batch digest", not "the whole system advanced the next epoch".
## Delivery Model
Once a batch is accepted, any operator may deliver messages from that batch with Merkle proofs.
Execution state is per stream:
- `next_sequence`
- `last_applied_message_hash`
- `next_batch_number`
- optionally one currently accepted batch being drained
The simplest execution rule is:
- delivery chunks must be contiguous within the accepted batch
- the first delivered sequence must equal `next_sequence`
- successful execution increments `next_sequence`
That preserves deterministic ordering while still allowing large batches to be split across multiple destination transactions.
If arbitrary subset delivery is needed later, a per-batch bitmap can be added. It is not required for the first correct implementation.
## Request/Response Correlation
Transport ordering and business correlation are separate concerns.
Any payload that expects a reverse-direction response must include:
- `request_id`
- `origin_message_id` or `source_event_id`
Examples:
- `Solana -> Wire` withdraw request carries `request_id`
- `Wire -> Solana` withdraw completion references the same `request_id`
- `Ethereum -> Wire` unbond request carries `request_id`
- `Wire -> Ethereum` unbond completion or slash references the same `request_id`
This removes the false dependency on shared epoch numbers between opposite directions.
## Current Repo Surfaces Mapped To The Model
### Ethereum -> Wire
Current origin surfaces already emitting OPP payloads:
- `wire-ethereum/contracts/outpost/Depositor.sol`
- `3001` stake
- `3002` unstake
- `3004` liq pretoken purchase
- `3006` yield pretoken purchase
- `wire-ethereum/contracts/outpost/Pretoken.sol`
- `3005` pretoken purchase
- `wire-ethereum/contracts/outpost/BAR.sol`
- `2001` bonded actor
- `2002` unbonded actor
- `2003` bond slashed
Design meaning:
- This route is a facts-to-Wire route.
- Messages represent Ethereum-local state transitions that Wire mirrors.
- The sender can continue producing batches even if a prior batch has not yet been submitted to Wire.
### Wire -> Ethereum
Current transport surface exists in `wire-ethereum/contracts/outpost/OPPInbound.sol`, but it is still globally sequential and not yet attached to real business handlers.
Design meaning:
- This route is a commands/acknowledgements route.
- It needs a real endpoint contract per lane, or one endpoint with per-type handlers.
- State must move from one global queue to per-stream state keyed by `stream_key`.
Immediate commands that fit this route:
- approvals or rejections for cross-chain requests
- BAR role outcomes if Wire is authoritative
- settlement callbacks for future instant swap flows
### Solana -> Wire
Current business surfaces that should emit outbound OPP requests or facts:
- `capital-staking/.../wire_syndication/syndicate_liqsol.rs`
- stake mirror
- `capital-staking/.../wire_syndication/desyndicate_liqsol.rs`
- post-launch withdraw request to Wire is currently a TODO
- `capital-staking/.../wire_pretokens/purchase_pretoken.rs`
- pretoken purchase mirror
- `capital-staking/.../wire_pretokens/purchase_pretokens_from_yield.rs`
- yield purchase mirror
- `capital-staking/.../bar/bond_ops.rs`
- bond request
- unbond request
Design meaning:
- This route is the Solana mirror/request route.
- Solana should emit canonical protobuf messages, or a deterministic translator must derive them from Solana instruction data until native protobuf encoding exists.
### Wire -> Solana
Current destination-side business surfaces already exist:
- `capital-staking/.../wire_config/admin_instructions.rs`
- `complete_withdraw_handler`
- `capital-staking/.../bar/admin_instructions.rs`
- `complete_unbond_role_handler`
- `slash_bond_handler`
- `admin_force_unbond_role_handler`
Design meaning:
- This route is a commands/acknowledgements route.
- These handlers should stop being human-admin-only settlement paths.
- They should become callable by an OPP authority PDA or gateway that verifies accepted Wire batches and enforces idempotency.
## Authority And Idempotency Rules
Every destination endpoint needs the same guarantees:
- only accepted OPP deliveries can execute privileged effects
- the same `request_id` cannot execute twice
- the same `(stream_key, sequence)` cannot execute twice
Practical requirements by repo:
- Ethereum:
- replace global inbound state in `OPPInbound.sol` with per-stream state
- register real assertion handlers for business routes
- Solana:
- add an OPP authority PDA or gateway instruction layer
- move current admin settlement handlers behind that authority
- Wire:
- implement an actual depot contract or plugin state machine for stream state, operator submissions, accepted batches, and handler dispatch
## Migration Strategy
The clean migration path is:
1. Define the shared protobuf schema in one place and vendor it into all repos.
2. Keep the current numeric assertion type registry.
3. Canonicalize every observed origin event into the shared protobuf `Message`.
4. Run consensus on protobuf `Batch` hashes.
5. Dispatch protobuf payloads at the destination.
This allows migration even if some origin contracts still emit legacy byte payloads for a short time, because the operator canonicalization step can deterministically translate them into the protobuf form used for batch hashing and destination execution.
## Immediate Implementation Plan
### Phase 1: Shared Schema
- Add `wire.opp.v1` protobuf definitions to the shared contract/protocol area.
- Add a registry document mapping numeric `assertion_type` values to protobuf payload messages.
### Phase 2: Wire Depot
- Implement per-stream tables:
- `stream_state`
- `batch_submission`
- `accepted_batch`
- `executed_message`
- Implement threshold matching on `(stream_key, batch_number, batch_hash)`.
- Implement contiguous chunk delivery with Merkle proof validation.
### Phase 3: Ethereum Endpoint
- Refactor inbound state from one global queue to `mapping(stream_hash => StreamState)`.
- Keep the sender-side forward progress behavior already added in `OPP.sol`.
- Add real business handlers for `Wire -> Ethereum`.
### Phase 4: Solana Endpoint
- Add an OPP gateway account model and authority PDA.
- Convert current admin settlement instructions into OPP-executable handlers.
- Emit outbound request/fact messages for the existing TODO sites.
### Phase 5: End-To-End Instant Swap Tests
Build the first full-path tests around request/response pairs that prove route independence:
- `Solana -> Wire` withdraw request followed by `Wire -> Solana` completion
- `Ethereum -> Wire` BAR unbond request followed by `Wire -> Ethereum` completion or slash
- one route deliberately delayed while another route continues progressing
## Non-Goals
- No global epoch shared across all routes.
- No requirement that opposite directions share batch numbers.
- No requirement that batch sealing blocks new origin-side messages.
- No special transport rule for instant swaps beyond per-stream ordering and request/response correlation.
## Decision
The system should be built as independent directed OPP streams with per-stream batching and per-stream operator consensus. "Epoch" becomes a local batching mechanism, not a system-wide gating primitive. Reverse-direction actions are correlated by payload IDs, not by transport epoch state.

BIN
OPP.pdf Normal file

Binary file not shown.

28
README.md Normal file
View File

@@ -0,0 +1,28 @@
# skunk-net-e2e
This repo is the cross-repo local E2E harness for the instant-swap / OPP work.
It is intentionally not a monorepo mirror of the underlying projects. The real
code stays in the original repos; this workspace repo tracks:
- Docker compose and bootstrap scripts
- local environment examples
- protocol notes used for this bring-up
- a dated checkpoint with the current blocker and reproduction steps
- the dependency repo matrix and expected branches
## Start Here
1. Read `WORKSPACE-MANIFEST.md`.
2. Clone each dependency repo into the top-level paths listed there.
3. Check out the recorded branches in those repos.
4. Copy `.env.local.example` to `.env.local` if you need overrides.
5. Use `./local-e2e-up.sh` or the individual commands in `LOCAL-E2E.md`.
## Key Files
- `WORKSPACE-MANIFEST.md`: remotes, branches, and current local heads
- `CHECKPOINT-2026-04-03.md`: current live state, blocker, and next steps
- `LOCAL-E2E.md`: bring-up and runtime commands
- `OPP-COHESIVE-DESIGN.md`: directional OPP design notes
- `protocol/opp/v1/opp.proto`: shared protobuf draft used for the cohesive design

66
WORKSPACE-MANIFEST.md Normal file
View File

@@ -0,0 +1,66 @@
# Workspace Manifest
This workspace expects the dependency repos to be cloned into fixed top-level
paths under the workspace root.
## Dependency Repos
| Path | Remote | Working Branch | Current Head |
| --- | --- | --- | --- |
| `wire-sysio` | `https://swarm.gitgo.app/experimental/wire-sysio.git` | `codex/local-kv-batchop` | `0525ce01789c4fc5f8799a1c89f881908ed77b64` |
| `wire-ethereum` | `https://swarm.gitgo.app/experimental/wire-ethereum.git` | `runtime/opp-parity-ethereum` | `cd240e751330356dfe59511e0d97405194cedd2b` |
| `capital-staking` | `https://swarm.gitgo.app/experimental/capital-staking.git` | `runtime/opp-parity-solana` | `b9bd80d9b436c60d8010a1a119f37dab824742f7` |
| `wire-hub-webapp` | `https://swarm.gitgo.app/experimental/wire-hub-webapp.git` | `runtime/instaswap-ui-dev-clean` | `87d9ea1c3fadc169b494f4599ae4e27324539206` |
| `solana-docker-setup` | `https://swarm.gitgo.app/experimental/solana-docker-setup` | `master` | `5c9d912e4e1e4dbd1d84402f9210d7daf09ca420` |
| `wire-cdt` | `https://swarm.gitgo.app/experimental/wire-cdt.git` | `master` | `c81b0b90a7118975dcfdb53ca4da225226aae505` |
## Local Change Summary
`wire-sysio`
- KV migration for the depot / instaswap path.
- batch operator relay implementation for ETH and SOL flows.
- Wire bootstrap / nodeop local harness adjustments.
- Local branch does not have an upstream configured yet.
`wire-ethereum`
- Live local outpost script updated for a real user/admin split and Solana base58 destination parsing.
`capital-staking`
- Docker-validator Anchor target support.
- local deploy now initializes OPP epoch state.
- relay artifact writer added.
- local tranche-state init adjusted for the Docker validator path.
`wire-hub-webapp`
- No local changes in this checkpoint.
## Workspace Files Tracked Here
- `docker-compose.local.yml`
- `local-compose.sh`
- `local-e2e-up.sh`
- `bootstrap-ethereum-local.sh`
- `bootstrap-solana-local.sh`
- `wire-nodeop-local.sh`
- `wire-batch-operator-local.sh`
- `.env.local.example`
- `LOCAL-E2E.md`
- `CHECKPOINT-2026-04-03.md`
- `OPP-COHESIVE-DESIGN.md`
- `protocol/opp/v1/opp.proto`
- `protocol/opp/v1/assertion-registry.md`
## Expected Directory Layout
```text
skunk-net/
README.md
docker-compose.local.yml
local-e2e-up.sh
wire-sysio/
wire-ethereum/
capital-staking/
wire-hub-webapp/
solana-docker-setup/
wire-cdt/
```

127
bootstrap-ethereum-local.sh Executable file
View File

@@ -0,0 +1,127 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WIRE_ETHEREUM_DIR="${WIRE_ETHEREUM_DIR:-$(pwd)}"
WIRE_ETHEREUM_REPO_REL="${WIRE_ETHEREUM_REPO_REL:-$(realpath --relative-to "${ROOT_DIR}" "${WIRE_ETHEREUM_DIR}")}"
LOCAL_DEPLOY_DIR="${WIRE_ETHEREUM_DIR}/.local/deployments"
ARTIFACTS_DIR="${ROOT_DIR}/.local/e2e"
HARDHAT_RPC_URL="${HARDHAT_RPC_URL:-http://127.0.0.1:8545}"
WIRE_BATCH_ETH_RPC_URL="${WIRE_BATCH_ETH_RPC_URL:-http://hardhat:8545}"
HARDHAT_DEPLOYER_KEY="${HARDHAT_DEPLOYER_KEY:-0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80}"
ETH_CLIENT_ID="${ETH_CLIENT_ID:-eth-local-1}"
ETH_PROVIDER_ID="${ETH_PROVIDER_ID:-eth-local-1}"
ETH_ABI_FILE="${ETH_ABI_FILE:-}"
wait_for_rpc() {
for _ in $(seq 1 60); do
if curl -fsS -H 'Content-Type: application/json' \
--data '{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":1}' \
"${HARDHAT_RPC_URL}" >/dev/null 2>&1; then
return 0
fi
sleep 1
done
echo "Hardhat RPC did not become ready at ${HARDHAT_RPC_URL}" >&2
exit 1
}
write_deploy_configs() {
mkdir -p "${LOCAL_DEPLOY_DIR}"
cat >"${LOCAL_DEPLOY_DIR}/liqeth.json" <<EOF
{
"url": "${HARDHAT_RPC_URL}",
"key": "${HARDHAT_DEPLOYER_KEY}",
"addressFile": "${LOCAL_DEPLOY_DIR}/liqeth-addrs.json",
"gasLimitFile": "${LOCAL_DEPLOY_DIR}/liqeth-gas-limits.json"
}
EOF
cat >"${LOCAL_DEPLOY_DIR}/outpost.json" <<EOF
{
"url": "${HARDHAT_RPC_URL}",
"key": "${HARDHAT_DEPLOYER_KEY}",
"useMockAggregator": true,
"addressFile": "${LOCAL_DEPLOY_DIR}/outpost-addrs.json",
"gasLimitFile": "${LOCAL_DEPLOY_DIR}/outpost-gas-limits.json"
}
EOF
}
deploy_local_stack() {
npm ci
write_deploy_configs
npx hardhat run src/scripts/deployLocal.ts --network anvil_local
}
write_artifacts() {
local out_file="${ARTIFACTS_DIR}/ethereum.env"
mkdir -p "${ARTIFACTS_DIR}"
ROOT_DIR="${ROOT_DIR}" \
WIRE_ETHEREUM_DIR="${WIRE_ETHEREUM_DIR}" \
WIRE_ETHEREUM_REPO_REL="${WIRE_ETHEREUM_REPO_REL}" \
ETH_PROVIDER_ID="${ETH_PROVIDER_ID}" \
ETH_CLIENT_ID="${ETH_CLIENT_ID}" \
HARDHAT_DEPLOYER_KEY="${HARDHAT_DEPLOYER_KEY}" \
WIRE_BATCH_ETH_RPC_URL="${WIRE_BATCH_ETH_RPC_URL}" \
ETH_ABI_FILE="${ETH_ABI_FILE}" \
node <<'EOF' >"${out_file}"
const fs = require("fs");
const path = require("path");
const { ethers } = require("ethers");
const rootDir = process.env.ROOT_DIR;
const repoDir = process.env.WIRE_ETHEREUM_DIR;
const repoRel = process.env.WIRE_ETHEREUM_REPO_REL;
const providerId = process.env.ETH_PROVIDER_ID;
const clientId = process.env.ETH_CLIENT_ID;
const privateKey = process.env.HARDHAT_DEPLOYER_KEY;
const rpcUrl = process.env.WIRE_BATCH_ETH_RPC_URL;
const abiFileOverride = process.env.ETH_ABI_FILE;
const addrsPath = path.join(repoDir, ".local", "deployments", "outpost-addrs.json");
const addrs = JSON.parse(fs.readFileSync(addrsPath, "utf8"));
const publicKey = new ethers.SigningKey(privateKey).publicKey;
const abiOutputPath = path.join(rootDir, ".local", "e2e", "ethereum-outpost-abi.json");
let abiFile = abiFileOverride;
if (!abiFile) {
const artifactPaths = [
path.join(repoDir, "artifacts", "contracts", "outpost", "OPP.sol", "OPP.json"),
path.join(repoDir, "artifacts", "contracts", "outpost", "OPPInbound.sol", "OPPInbound.json"),
];
const combinedAbi = artifactPaths.flatMap((artifactPath) => {
const artifact = JSON.parse(fs.readFileSync(artifactPath, "utf8"));
if (!Array.isArray(artifact.abi)) {
throw new Error(`Artifact missing abi array: ${artifactPath}`);
}
return artifact.abi;
});
fs.mkdirSync(path.dirname(abiOutputPath), { recursive: true });
fs.writeFileSync(abiOutputPath, `${JSON.stringify(combinedAbi, null, 2)}\n`);
abiFile = "/wire/.local/e2e/ethereum-outpost-abi.json";
}
console.log(`LOCAL_ETH_CLIENT_ID=${clientId}`);
console.log(`LOCAL_ETH_PROVIDER_ID=${providerId}`);
console.log(`LOCAL_ETH_PROVIDER_SPEC=${providerId},ethereum,ethereum,${publicKey},KEY:${privateKey}`);
console.log(`LOCAL_ETH_RPC_URL=${rpcUrl}`);
console.log(`LOCAL_ETH_OPP_ADDRESS=${addrs.OPP}`);
console.log(`LOCAL_ETH_OPP_INBOUND_ADDRESS=${addrs.OPPInbound}`);
console.log(`LOCAL_ETH_ABI_FILE=${abiFile}`);
console.log(`LOCAL_ETH_REPO_DIR=${repoRel}`);
console.log(`LOCAL_ETH_OUTPOST_ADDRS=${path.join(rootDir, repoRel, ".local", "deployments", "outpost-addrs.json")}`);
EOF
}
main() {
cd "${WIRE_ETHEREUM_DIR}"
wait_for_rpc
deploy_local_stack
write_artifacts
}
main "$@"

184
bootstrap-solana-local.sh Executable file
View File

@@ -0,0 +1,184 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CAPITAL_STAKING_DIR="${CAPITAL_STAKING_DIR:-$(pwd)}"
CAPITAL_STAKING_REPO_REL="${CAPITAL_STAKING_REPO_REL:-$(realpath --relative-to "${ROOT_DIR}" "${CAPITAL_STAKING_DIR}")}"
ARTIFACTS_DIR="${ROOT_DIR}/.local/e2e"
ANCHOR_PROVIDER_URL="${ANCHOR_PROVIDER_URL:-http://solana-validator:8899}"
ANCHOR_WALLET="${ANCHOR_WALLET:-${CAPITAL_STAKING_DIR}/wallets/deploymentWallet/universalDeploymentWallet.json}"
WIRE_BATCH_SOL_RPC_URL="${WIRE_BATCH_SOL_RPC_URL:-http://solana-validator:8899}"
AIRDROP_AMOUNT="${AIRDROP_AMOUNT:-100}"
LOCAL_BOOTSTRAP_ROOT="${LOCAL_BOOTSTRAP_ROOT:-${ROOT_DIR}/.local/solana-bootstrap}"
LOCAL_CARGO_HOME="${LOCAL_CARGO_HOME:-${LOCAL_BOOTSTRAP_ROOT}/cargo-home}"
LOCAL_SOLANA_HOME="${LOCAL_SOLANA_HOME:-${LOCAL_BOOTSTRAP_ROOT}/home}"
LOCAL_RUSTUP_HOME="${LOCAL_RUSTUP_HOME:-${LOCAL_BOOTSTRAP_ROOT}/rustup-home}"
LOCAL_SBF_SDK_PATH="${LOCAL_SBF_SDK_PATH:-${LOCAL_SOLANA_HOME}/sdk/sbf}"
CAPITAL_STAKING_FEATURES="${CAPITAL_STAKING_FEATURES:-development}"
SOL_CLIENT_ID="${SOL_CLIENT_ID:-sol-local-1}"
SOL_PROVIDER_ID="${SOL_PROVIDER_ID:-sol-local-1}"
SOL_PROGRAM_ID="${SOL_PROGRAM_ID:-}"
SOL_IDL_FILE="/wire/${CAPITAL_STAKING_REPO_REL}/target/idl/liqsol_core.json"
prepare_sbf_sdk() {
local default_sbf_sdk="/opt/solana/.local/share/solana/install/active_release/bin/sdk/sbf"
mkdir -p "$(dirname "${LOCAL_SBF_SDK_PATH}")"
if [[ ! -d "${LOCAL_SBF_SDK_PATH}" ]]; then
cp -a "${default_sbf_sdk}" "${LOCAL_SBF_SDK_PATH}"
fi
export SBF_SDK_PATH="${LOCAL_SBF_SDK_PATH}"
}
prepare_rustup_home() {
if [[ ! -d "${LOCAL_RUSTUP_HOME}/toolchains" ]]; then
mkdir -p "${LOCAL_RUSTUP_HOME}"
cp -a /opt/rustup/. "${LOCAL_RUSTUP_HOME}/"
fi
export RUSTUP_HOME="${LOCAL_RUSTUP_HOME}"
}
resolve_sol_program_id() {
if [[ -n "${SOL_PROGRAM_ID}" ]]; then
printf '%s\n' "${SOL_PROGRAM_ID}"
return 0
fi
node <<'EOF'
const fs = require("fs");
const path = require("path");
const { Keypair } = require("@solana/web3.js");
const keypairPath = path.resolve("target/deploy/liqsol_core-keypair.json");
if (fs.existsSync(keypairPath)) {
const secret = Uint8Array.from(JSON.parse(fs.readFileSync(keypairPath, "utf8")));
console.log(Keypair.fromSecretKey(secret).publicKey.toBase58());
process.exit(0);
}
const idlPath = path.resolve("target/idl/liqsol_core.json");
if (fs.existsSync(idlPath)) {
const idl = JSON.parse(fs.readFileSync(idlPath, "utf8"));
if (idl.address) {
console.log(idl.address);
process.exit(0);
}
}
process.exit(1);
EOF
}
deploy_local_cluster() {
export HOME="${LOCAL_SOLANA_HOME}"
export XDG_CACHE_HOME="${HOME}/.cache"
export XDG_CONFIG_HOME="${HOME}/.config"
npm ci
export ANCHOR_PROVIDER_URL
export ANCHOR_WALLET
export AIRDROP_AMOUNT
export CAPITAL_STAKING_FEATURES
export CARGO_HOME="${LOCAL_CARGO_HOME}"
export SOLANA_RPC="${ANCHOR_PROVIDER_URL}"
mkdir -p "${HOME}" "${XDG_CACHE_HOME}" "${XDG_CONFIG_HOME}" "${CARGO_HOME}" "${LOCAL_BOOTSTRAP_ROOT}"
prepare_rustup_home
prepare_sbf_sdk
solana config set --url "${ANCHOR_PROVIDER_URL}" --keypair "${ANCHOR_WALLET}" >/dev/null
bash bash-scripts/local-deploy.sh --wait-for-cluster --features "${CAPITAL_STAKING_FEATURES}"
SOL_PROGRAM_ID="$(resolve_sol_program_id)"
export SOL_PROGRAM_ID
}
set_signature_weight() {
ANCHOR_WALLET="${ANCHOR_WALLET}" \
ANCHOR_PROVIDER_URL="${ANCHOR_PROVIDER_URL}" \
SOL_PROGRAM_ID="${SOL_PROGRAM_ID}" \
node <<'EOF'
const fs = require("fs");
const anchor = require("@coral-xyz/anchor");
const { Connection, Keypair, PublicKey } = require("@solana/web3.js");
const idl = require("./target/idl/liqsol_core.json");
const walletPath = process.env.ANCHOR_WALLET;
const providerUrl = process.env.ANCHOR_PROVIDER_URL;
const programId = new PublicKey(process.env.SOL_PROGRAM_ID || idl.address);
idl.address = programId.toBase58();
const secret = Uint8Array.from(JSON.parse(fs.readFileSync(walletPath, "utf8")));
const wallet = new anchor.Wallet(Keypair.fromSecretKey(secret));
const provider = new anchor.AnchorProvider(new Connection(providerUrl, "confirmed"), wallet, {
commitment: "confirmed",
});
anchor.setProvider(provider);
const program = new anchor.Program(idl, provider);
const [globalConfig] = PublicKey.findProgramAddressSync([Buffer.from("global_config")], programId);
const [controllerState] = PublicKey.findProgramAddressSync([Buffer.from("stake_controller")], programId);
async function main() {
await program.methods
.updateConfigU16({ oppRequiredSignatureWeight: {} }, 1)
.accounts({
globalConfig,
controllerState,
authority: wallet.publicKey,
})
.rpc();
}
main().catch((error) => {
console.error(error);
process.exit(1);
});
EOF
}
write_artifacts() {
local out_file="${ARTIFACTS_DIR}/solana.env"
mkdir -p "${ARTIFACTS_DIR}"
ANCHOR_WALLET="${ANCHOR_WALLET}" \
SOL_PROVIDER_ID="${SOL_PROVIDER_ID}" \
SOL_CLIENT_ID="${SOL_CLIENT_ID}" \
WIRE_BATCH_SOL_RPC_URL="${WIRE_BATCH_SOL_RPC_URL}" \
SOL_PROGRAM_ID="${SOL_PROGRAM_ID}" \
SOL_IDL_FILE="${SOL_IDL_FILE}" \
CAPITAL_STAKING_REPO_REL="${CAPITAL_STAKING_REPO_REL}" \
node <<'EOF' >"${out_file}"
const fs = require("fs");
const bs58 = require("bs58");
const { Keypair } = require("@solana/web3.js");
const walletPath = process.env.ANCHOR_WALLET;
const providerId = process.env.SOL_PROVIDER_ID;
const clientId = process.env.SOL_CLIENT_ID;
const rpcUrl = process.env.WIRE_BATCH_SOL_RPC_URL;
const programId = process.env.SOL_PROGRAM_ID;
const idlFile = process.env.SOL_IDL_FILE;
const repoRel = process.env.CAPITAL_STAKING_REPO_REL;
const secret = Uint8Array.from(JSON.parse(fs.readFileSync(walletPath, "utf8")));
const keypair = Keypair.fromSecretKey(secret);
const secretBase58 = bs58.encode(Buffer.from(keypair.secretKey));
console.log(`LOCAL_SOL_CLIENT_ID=${clientId}`);
console.log(`LOCAL_SOL_PROVIDER_ID=${providerId}`);
console.log(`LOCAL_SOL_PROVIDER_SPEC=${providerId},solana,solana,${keypair.publicKey.toBase58()},KEY:${secretBase58}`);
console.log(`LOCAL_SOL_RPC_URL=${rpcUrl}`);
console.log(`LOCAL_SOL_PROGRAM_ID=${programId}`);
console.log(`LOCAL_SOL_IDL_FILE=${idlFile}`);
console.log(`LOCAL_SOL_REPO_DIR=${repoRel}`);
console.log(`LOCAL_SOL_OPP_REQUIRED_SIGNATURE_WEIGHT=1`);
EOF
}
main() {
cd "${CAPITAL_STAKING_DIR}"
deploy_local_cluster
set_signature_weight
write_artifacts
}
main "$@"

215
docker-compose.local.yml Normal file
View File

@@ -0,0 +1,215 @@
services:
wire-sysio-dev:
build:
context: ${WIRE_SYSIO_CONTEXT:-./wire-sysio}
dockerfile: etc/docker/Dockerfile
target: platform-dev
additional_contexts:
clang-18-scripts: ${WIRE_SYSIO_CONTEXT:-./wire-sysio}/scripts/clang-18
app-root: ${WIRE_SYSIO_CONTEXT:-./wire-sysio}
image: ${WIRE_SYSIO_IMAGE:-wire/sysio:platform-dev-local}
user: "${LOCAL_UID:-1000}:${LOCAL_GID:-1000}"
working_dir: /wire
volumes:
- .:/wire
- ./.local/wire-node:/srv/wire-node
environment:
WIRE_ROOT: /wire/${WIRE_SYSIO_REPO_DIR:-wire-sysio}
WIRE_SYSIO_DIR: /wire/${WIRE_SYSIO_REPO_DIR:-wire-sysio}
command: ["bash", "-lc", "sleep infinity"]
wire-nodeop:
build:
context: ${WIRE_SYSIO_CONTEXT:-./wire-sysio}
dockerfile: etc/docker/Dockerfile
target: platform-dev
additional_contexts:
clang-18-scripts: ${WIRE_SYSIO_CONTEXT:-./wire-sysio}/scripts/clang-18
app-root: ${WIRE_SYSIO_CONTEXT:-./wire-sysio}
image: ${WIRE_SYSIO_IMAGE:-wire/sysio:platform-dev-local}
user: "${LOCAL_UID:-1000}:${LOCAL_GID:-1000}"
working_dir: /wire
init: true
volumes:
- .:/wire
- ./.local/wire-node:/srv/wire-node
environment:
WIRE_ROOT: /wire/${WIRE_SYSIO_REPO_DIR:-wire-sysio}
WIRE_SYSIO_DIR: /wire/${WIRE_SYSIO_REPO_DIR:-wire-sysio}
WIRE_NODE_ROOT: ${WIRE_NODE_ROOT:-/srv/wire-node/chain-001}
WIRE_ARTIFACTS_DIR: /wire/.local/e2e
WIRE_ETH_DEPOT_ACCOUNT: ${WIRE_ETH_DEPOT_ACCOUNT:-sysio.dpeth}
WIRE_SOL_DEPOT_ACCOUNT: ${WIRE_SOL_DEPOT_ACCOUNT:-sysio.dpsol}
WIRE_INSTASWAP_ACCOUNT: ${WIRE_INSTASWAP_ACCOUNT:-sysio.iswap}
WIRE_WYIELD_ACCOUNT: ${WIRE_WYIELD_ACCOUNT:-sysio.wyield}
WIRE_TEST_USER_ACCOUNT: ${WIRE_TEST_USER_ACCOUNT:-userinsta1}
WIRE_BATCH_OPERATOR_ETH_ACCOUNT: ${WIRE_BATCH_OPERATOR_ETH_ACCOUNT:-bopeth111111}
WIRE_BATCH_OPERATOR_SOL_ACCOUNT: ${WIRE_BATCH_OPERATOR_SOL_ACCOUNT:-bopsol111111}
WIRE_BATCH_OPERATOR_ETH_WIRE_PROVIDER_ID: ${WIRE_BATCH_OPERATOR_ETH_WIRE_PROVIDER_ID:-wire-bopeth-1}
WIRE_BATCH_OPERATOR_SOL_WIRE_PROVIDER_ID: ${WIRE_BATCH_OPERATOR_SOL_WIRE_PROVIDER_ID:-wire-bopsol-1}
ports:
- "${WIRE_HTTP_PORT:-8887}:8887"
- "${WIRE_P2P_PORT:-4443}:4443"
command: ["bash", "-lc", "bash /wire/wire-nodeop-local.sh"]
batch-operator-eth-1:
build:
context: ${WIRE_SYSIO_CONTEXT:-./wire-sysio}
dockerfile: etc/docker/Dockerfile
target: platform-dev
additional_contexts:
clang-18-scripts: ${WIRE_SYSIO_CONTEXT:-./wire-sysio}/scripts/clang-18
app-root: ${WIRE_SYSIO_CONTEXT:-./wire-sysio}
image: ${WIRE_SYSIO_IMAGE:-wire/sysio:platform-dev-local}
user: "${LOCAL_UID:-1000}:${LOCAL_GID:-1000}"
working_dir: /wire
init: true
volumes:
- .:/wire
environment:
WIRE_ROOT: /wire/${WIRE_SYSIO_REPO_DIR:-wire-sysio}
WIRE_SYSIO_DIR: /wire/${WIRE_SYSIO_REPO_DIR:-wire-sysio}
WIRE_BATCH_OPERATOR_ROUTE: eth
WIRE_BATCH_OPERATOR_INDEX: "1"
WIRE_BATCH_OPERATOR_ROOT: /wire/.local/wire-batch-operators
WIRE_BATCH_OPERATOR_ARTIFACTS_DIR: /wire/.local/e2e
WIRE_BATCH_OPERATOR_WIRE_RPC_URL: ${WIRE_BATCH_OPERATOR_WIRE_RPC_URL:-http://wire-nodeop:8887}
WIRE_BATCH_OPERATOR_WIRE_DEPOT_ACCOUNT: ${WIRE_BATCH_OPERATOR_ETH_WIRE_DEPOT_ACCOUNT:-sysio.dpeth}
WIRE_BATCH_OPERATOR_WIRE_ACCOUNT: ${WIRE_BATCH_OPERATOR_ETH_WIRE_ACCOUNT:-}
WIRE_BATCH_OPERATOR_WIRE_PROVIDER_ID: ${WIRE_BATCH_OPERATOR_ETH_WIRE_PROVIDER_ID:-}
WIRE_BATCH_OPERATOR_WIRE_PROVIDER_SPEC: ${WIRE_BATCH_OPERATOR_ETH_WIRE_PROVIDER_SPEC:-}
WIRE_BATCH_OPERATOR_SOURCE_CLIENT_ID: ${WIRE_BATCH_OPERATOR_ETH_CLIENT_ID:-eth-local-1}
WIRE_BATCH_OPERATOR_SOURCE_PROVIDER_ID: ${WIRE_BATCH_OPERATOR_ETH_PROVIDER_ID:-eth-local-1}
WIRE_BATCH_OPERATOR_SOURCE_PROVIDER_SPEC: ${WIRE_BATCH_OPERATOR_ETH_PROVIDER_SPEC:-}
WIRE_BATCH_OPERATOR_SOURCE_RPC_URL: ${WIRE_BATCH_OPERATOR_ETH_RPC_URL:-http://hardhat:8545}
WIRE_BATCH_OPERATOR_ETH_CHAIN_ID: ${WIRE_BATCH_OPERATOR_ETH_CHAIN_ID:-31337}
WIRE_BATCH_OPERATOR_ETH_ABI_FILE: ${WIRE_BATCH_OPERATOR_ETH_ABI_FILE:-/wire/.local/e2e/ethereum-outpost-abi.json}
WIRE_BATCH_OPERATOR_ETH_OPP_ADDRESS: ${WIRE_BATCH_OPERATOR_ETH_OPP_ADDRESS:-}
WIRE_BATCH_OPERATOR_ETH_OPP_INBOUND_ADDRESS: ${WIRE_BATCH_OPERATOR_ETH_OPP_INBOUND_ADDRESS:-}
WIRE_BATCH_OPERATOR_POLL_INTERVAL_MS: ${WIRE_BATCH_OPERATOR_POLL_INTERVAL_MS:-5000}
depends_on:
wire-nodeop:
condition: service_started
hardhat:
condition: service_started
command: ["bash", "-lc", "bash /wire/wire-batch-operator-local.sh eth 1"]
batch-operator-sol-1:
build:
context: ${WIRE_SYSIO_CONTEXT:-./wire-sysio}
dockerfile: etc/docker/Dockerfile
target: platform-dev
additional_contexts:
clang-18-scripts: ${WIRE_SYSIO_CONTEXT:-./wire-sysio}/scripts/clang-18
app-root: ${WIRE_SYSIO_CONTEXT:-./wire-sysio}
image: ${WIRE_SYSIO_IMAGE:-wire/sysio:platform-dev-local}
user: "${LOCAL_UID:-1000}:${LOCAL_GID:-1000}"
working_dir: /wire
init: true
volumes:
- .:/wire
environment:
WIRE_ROOT: /wire/${WIRE_SYSIO_REPO_DIR:-wire-sysio}
WIRE_SYSIO_DIR: /wire/${WIRE_SYSIO_REPO_DIR:-wire-sysio}
WIRE_BATCH_OPERATOR_ROUTE: sol
WIRE_BATCH_OPERATOR_INDEX: "1"
WIRE_BATCH_OPERATOR_ROOT: /wire/.local/wire-batch-operators
WIRE_BATCH_OPERATOR_ARTIFACTS_DIR: /wire/.local/e2e
WIRE_BATCH_OPERATOR_WIRE_RPC_URL: ${WIRE_BATCH_OPERATOR_WIRE_RPC_URL:-http://wire-nodeop:8887}
WIRE_BATCH_OPERATOR_WIRE_DEPOT_ACCOUNT: ${WIRE_BATCH_OPERATOR_SOL_WIRE_DEPOT_ACCOUNT:-sysio.dpsol}
WIRE_BATCH_OPERATOR_WIRE_ACCOUNT: ${WIRE_BATCH_OPERATOR_SOL_WIRE_ACCOUNT:-}
WIRE_BATCH_OPERATOR_WIRE_PROVIDER_ID: ${WIRE_BATCH_OPERATOR_SOL_WIRE_PROVIDER_ID:-}
WIRE_BATCH_OPERATOR_WIRE_PROVIDER_SPEC: ${WIRE_BATCH_OPERATOR_SOL_WIRE_PROVIDER_SPEC:-}
WIRE_BATCH_OPERATOR_SOURCE_CLIENT_ID: ${WIRE_BATCH_OPERATOR_SOL_CLIENT_ID:-sol-local-1}
WIRE_BATCH_OPERATOR_SOURCE_PROVIDER_ID: ${WIRE_BATCH_OPERATOR_SOL_PROVIDER_ID:-sol-local-1}
WIRE_BATCH_OPERATOR_SOURCE_PROVIDER_SPEC: ${WIRE_BATCH_OPERATOR_SOL_PROVIDER_SPEC:-}
WIRE_BATCH_OPERATOR_SOURCE_RPC_URL: ${WIRE_BATCH_OPERATOR_SOL_RPC_URL:-http://solana-validator:8899}
WIRE_BATCH_OPERATOR_SOL_PROGRAM_ID: ${WIRE_BATCH_OPERATOR_SOL_PROGRAM_ID:-}
WIRE_BATCH_OPERATOR_SOL_IDL_FILE: ${WIRE_BATCH_OPERATOR_SOL_IDL_FILE:-/wire/.branch-worktrees/capital-staking-opp-parity/target/idl/liqsol_core.json}
WIRE_BATCH_OPERATOR_POLL_INTERVAL_MS: ${WIRE_BATCH_OPERATOR_POLL_INTERVAL_MS:-5000}
depends_on:
wire-nodeop:
condition: service_started
solana-validator:
condition: service_healthy
command: ["bash", "-lc", "bash /wire/wire-batch-operator-local.sh sol 1"]
wire-ethereum-dev:
image: node:20-bookworm
user: "${LOCAL_UID:-1000}:${LOCAL_GID:-1000}"
working_dir: /workspace/${WIRE_ETHEREUM_REPO_DIR:-wire-ethereum}
volumes:
- .:/workspace
command: ["bash", "-lc", "sleep infinity"]
hardhat:
image: node:20-bookworm
user: "${LOCAL_UID:-1000}:${LOCAL_GID:-1000}"
working_dir: /workspace/${WIRE_ETHEREUM_REPO_DIR:-wire-ethereum}
volumes:
- .:/workspace
ports:
- "${HARDHAT_PORT:-8545}:8545"
command: ["bash", "-lc", "npm ci && npx hardhat node --hostname 0.0.0.0"]
capital-staking-dev:
build:
context: ${CAPITAL_STAKING_BUILD_CONTEXT:-./capital-staking}
dockerfile: docker/anchor-toolchain.Dockerfile
image: ${CAPITAL_STAKING_IMAGE:-skunk-net/capital-staking-dev:local}
user: "${LOCAL_UID:-1000}:${LOCAL_GID:-1000}"
working_dir: /workspace/${CAPITAL_STAKING_REPO_DIR:-capital-staking}
volumes:
- .:/workspace
environment:
ANCHOR_PROVIDER_URL: ${ANCHOR_PROVIDER_URL:-http://solana-validator:8899}
ANCHOR_WALLET: ${ANCHOR_WALLET:-/workspace/.branch-worktrees/capital-staking-opp-parity/wallets/deploymentWallet/universalDeploymentWallet.json}
DOCKER_CLUSTER: "1"
RPC_URL: ${RPC_URL:-http://solana-validator:8899}
REQUIRED_VALIDATORS: ${REQUIRED_VALIDATORS:-1}
SOLANA_RPC: ${SOLANA_RPC:-http://solana-validator:8899}
depends_on:
solana-validator:
condition: service_healthy
command: ["bash", "-lc", "sleep infinity"]
solana-validator:
build:
context: ./solana-docker-setup
dockerfile: Dockerfile
image: ${SOLANA_DOCKER_IMAGE:-skunk-net/solana-validator:local}
working_dir: /solana
volumes:
- solana-validator-data:/solana-data
ports:
- "${SOLANA_GOSSIP_PORT:-8001}:8001/udp"
- "${SOLANA_RPC_PORT:-8899}:8899"
- "${SOLANA_WS_PORT:-8900}:8900"
- "${SOLANA_FAUCET_PORT:-9900}:9900"
environment:
IS_GENESIS: "true"
NODE_NAME: genesis
GOSSIP_HOST: solana-validator
GOSSIP_PORT: ${SOLANA_GOSSIP_PORT:-8001}
RPC_PORT: 8899
RPC_BIND_ADDRESS: 0.0.0.0
BIND_ADDRESS: 0.0.0.0
FAUCET_ENABLE: "true"
FAUCET_ADDRESS: 0.0.0.0
FAUCET_PORT: 9900
SLOTS_PER_EPOCH: ${SOLANA_SLOTS_PER_EPOCH:-64}
DATA_DIR: /solana-data/genesis
SOLANA_RUN_SH_VALIDATOR_ARGS: ${SOLANA_RUN_SH_VALIDATOR_ARGS:-}
ulimits:
nofile:
soft: 1000000
hard: 1000000
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:8899/health >/dev/null"]
interval: 5s
timeout: 5s
retries: 60
start_period: 120s
volumes:
solana-validator-data:

23
local-compose.sh Executable file
View File

@@ -0,0 +1,23 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ENV_FILE="${ROOT_DIR}/.env.local"
if [[ ! -f "${ENV_FILE}" ]]; then
if [[ -f "${ROOT_DIR}/.env.local.example" ]]; then
cp "${ROOT_DIR}/.env.local.example" "${ENV_FILE}"
echo "Created ${ENV_FILE} from .env.local.example" >&2
else
echo "Missing ${ENV_FILE} and .env.local.example" >&2
exit 1
fi
fi
exec env \
LOCAL_UID="$(id -u)" \
LOCAL_GID="$(id -g)" \
docker compose \
--env-file "${ENV_FILE}" \
-f "${ROOT_DIR}/docker-compose.local.yml" \
"$@"

25
local-e2e-up.sh Executable file
View File

@@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ENV_FILE="${ROOT_DIR}/.env.local"
if [[ -f "${ENV_FILE}" ]]; then
set -a
# shellcheck disable=SC1090
source "${ENV_FILE}"
set +a
fi
WIRE_ETHEREUM_REPO_DIR="${WIRE_ETHEREUM_REPO_DIR:-wire-ethereum}"
CAPITAL_STAKING_REPO_DIR="${CAPITAL_STAKING_REPO_DIR:-capital-staking}"
"${ROOT_DIR}/local-compose.sh" up -d hardhat solana-validator wire-nodeop
"${ROOT_DIR}/local-compose.sh" exec hardhat bash -lc \
"cd /workspace/${WIRE_ETHEREUM_REPO_DIR} && ROOT_DIR=/workspace WIRE_ETHEREUM_DIR=/workspace/${WIRE_ETHEREUM_REPO_DIR} bash /workspace/bootstrap-ethereum-local.sh"
"${ROOT_DIR}/local-compose.sh" run --rm --no-deps capital-staking-dev bash -lc \
"cd /workspace/${CAPITAL_STAKING_REPO_DIR} && ROOT_DIR=/workspace CAPITAL_STAKING_DIR=/workspace/${CAPITAL_STAKING_REPO_DIR} bash /workspace/bootstrap-solana-local.sh"
"${ROOT_DIR}/local-compose.sh" up -d batch-operator-eth-1 batch-operator-sol-1

View File

@@ -0,0 +1,53 @@
# OPP Assertion Registry
This file is the numeric assertion registry for the shared OPP protobuf schema in `protocol/opp/v1/opp.proto`.
## Rules
- Numeric assertion IDs remain stable once assigned.
- Direction is part of the assignment. The same business meaning in opposite directions gets a different numeric ID.
- Payload bytes are protobuf messages from `wire.opp.v1`.
- Request/response flows correlate through `request_id`, not through shared batch numbers.
## Assigned Ranges
| Range | Route | Notes |
| --- | --- | --- |
| `2000-2099` | `Ethereum -> Wire` BAR | Existing Ethereum BAR assertions |
| `3000-3099` | `Ethereum -> Wire` Depositor/Pretoken | Existing Ethereum depositor and pretoken assertions |
| `4000-4199` | `Solana -> Wire` | Solana facts and requests |
| `5000-5199` | `Wire -> Solana` | Wire settlement and admin commands |
| `6000-6199` | `Wire -> Ethereum` | Wire settlement and admin commands |
## Active Assignments
| ID | Route | Lane | Payload | Current surface |
| --- | --- | --- | --- | --- |
| `2001` | `Ethereum -> Wire` | `bar` | `BondedActorMirror` | `wire-ethereum/contracts/outpost/BAR.sol` |
| `2002` | `Ethereum -> Wire` | `bar` | `UnbondedActorMirror` | `wire-ethereum/contracts/outpost/BAR.sol` |
| `2003` | `Ethereum -> Wire` | `bar` | `BondSlashMirror` | `wire-ethereum/contracts/outpost/BAR.sol` |
| `3001` | `Ethereum -> Wire` | `depositor` | `StakeMirror` | `wire-ethereum/contracts/outpost/Depositor.sol` |
| `3002` | `Ethereum -> Wire` | `depositor` | `UnstakeMirror` | `wire-ethereum/contracts/outpost/Depositor.sol` |
| `3004` | `Ethereum -> Wire` | `pretoken` | `PretokenPurchaseMirror` | `wire-ethereum/contracts/outpost/Depositor.sol` |
| `3005` | `Ethereum -> Wire` | `pretoken` | `PretokenPurchaseMirror` | `wire-ethereum/contracts/outpost/Pretoken.sol` |
| `3006` | `Ethereum -> Wire` | `pretoken` | `PretokenPurchaseMirror` | `wire-ethereum/contracts/outpost/Depositor.sol` |
| `4001` | `Solana -> Wire` | `depositor` | `StakeMirror` | `capital-staking/.../wire_syndication/syndicate_liqsol.rs` |
| `4002` | `Solana -> Wire` | `depositor` | `WithdrawRequest` | `capital-staking/.../wire_syndication/desyndicate_liqsol.rs` |
| `4003` | `Solana -> Wire` | `pretoken` | `PretokenPurchaseMirror` | `capital-staking/.../wire_pretokens/purchase_pretoken.rs` |
| `4004` | `Solana -> Wire` | `pretoken` | `PretokenPurchaseMirror` | `capital-staking/.../wire_pretokens/purchase_pretokens_from_yield.rs` |
| `4101` | `Solana -> Wire` | `bar` | `RoleBondRequest` | `capital-staking/.../bar/bond_ops.rs` |
| `4102` | `Solana -> Wire` | `bar` | `RoleUnbondRequest` | `capital-staking/.../bar/bond_ops.rs` |
| `5001` | `Wire -> Solana` | `admin` | `CompleteWithdrawCommand` | `capital-staking/.../wire_config/admin_instructions.rs` |
| `5002` | `Wire -> Solana` | `admin` | `CompleteUnbondCommand` | `capital-staking/.../bar/admin_instructions.rs` |
| `5003` | `Wire -> Solana` | `admin` | `SlashBondCommand` | `capital-staking/.../bar/admin_instructions.rs` |
| `5004` | `Wire -> Solana` | `admin` | `AdminForceUnbondCommand` | `capital-staking/.../bar/admin_instructions.rs` |
| `5009` | `Wire -> Solana` | `admin` | `RequestRejected` | Reserved for failed Solana request outcomes |
| `6001` | `Wire -> Ethereum` | `admin` | `CompleteUnbondCommand` | Reserved for Ethereum endpoint implementation |
| `6002` | `Wire -> Ethereum` | `admin` | `SlashBondCommand` | Reserved for Ethereum endpoint implementation |
| `6009` | `Wire -> Ethereum` | `admin` | `RequestRejected` | Reserved for Ethereum endpoint implementation |
## Notes
- `3004`, `3005`, and `3006` all currently describe pretoken-related state, but they remain distinct numeric IDs because they originate from different Ethereum business actions.
- `4002` and the `500x` series are the first Solana request/response pair that should be implemented for end-to-end testing.
- `600x` is intentionally small right now because `Wire -> Ethereum` business handlers do not yet exist in this checkout.

215
protocol/opp/v1/opp.proto Normal file
View File

@@ -0,0 +1,215 @@
syntax = "proto3";
package wire.opp.v1;
enum ChainKind {
CHAIN_KIND_UNKNOWN = 0;
CHAIN_KIND_WIRE = 1;
CHAIN_KIND_ETHEREUM = 2;
CHAIN_KIND_SOLANA = 3;
CHAIN_KIND_SUI = 4;
}
enum OppLane {
OPP_LANE_UNSPECIFIED = 0;
OPP_LANE_DEPOSITOR = 1;
OPP_LANE_PRETOKEN = 2;
OPP_LANE_BAR = 3;
OPP_LANE_ADMIN = 4;
OPP_LANE_SWAP = 5;
}
enum Role {
ROLE_UNSPECIFIED = 0;
ROLE_YIELD_OPERATOR = 1;
ROLE_BATCH_OPERATOR = 2;
ROLE_UNDERWRITER = 4;
ROLE_POOL_OPERATOR = 8;
}
enum AssertionType {
ASSERTION_TYPE_UNSPECIFIED = 0;
ASSERTION_TYPE_ETH_BAR_BONDED_ACTOR = 2001;
ASSERTION_TYPE_ETH_BAR_UNBONDED_ACTOR = 2002;
ASSERTION_TYPE_ETH_BAR_BOND_SLASHED = 2003;
ASSERTION_TYPE_ETH_DEPOSITOR_STAKE = 3001;
ASSERTION_TYPE_ETH_DEPOSITOR_UNSTAKE = 3002;
ASSERTION_TYPE_ETH_DEPOSITOR_LIQ_PRETOKEN_PURCHASE = 3004;
ASSERTION_TYPE_ETH_PRETOKEN_PURCHASE = 3005;
ASSERTION_TYPE_ETH_DEPOSITOR_YIELD_PRETOKEN_PURCHASE = 3006;
ASSERTION_TYPE_SOL_STAKE = 4001;
ASSERTION_TYPE_SOL_WITHDRAW_REQUEST = 4002;
ASSERTION_TYPE_SOL_PRETOKEN_PURCHASE = 4003;
ASSERTION_TYPE_SOL_YIELD_PRETOKEN_PURCHASE = 4004;
ASSERTION_TYPE_SOL_BAR_BOND_ROLE = 4101;
ASSERTION_TYPE_SOL_BAR_UNBOND_REQUEST = 4102;
ASSERTION_TYPE_WIRE_SOL_COMPLETE_WITHDRAW = 5001;
ASSERTION_TYPE_WIRE_SOL_COMPLETE_UNBOND = 5002;
ASSERTION_TYPE_WIRE_SOL_SLASH_BOND = 5003;
ASSERTION_TYPE_WIRE_SOL_ADMIN_FORCE_UNBOND = 5004;
ASSERTION_TYPE_WIRE_SOL_REQUEST_REJECTED = 5009;
ASSERTION_TYPE_WIRE_ETH_COMPLETE_UNBOND = 6001;
ASSERTION_TYPE_WIRE_ETH_SLASH_BOND = 6002;
ASSERTION_TYPE_WIRE_ETH_REQUEST_REJECTED = 6009;
}
message StreamKey {
ChainKind from_chain = 1;
ChainKind to_chain = 2;
bytes origin_system = 3;
bytes destination_system = 4;
OppLane lane = 5;
}
message Assertion {
AssertionType assertion_type = 1;
bytes payload = 2;
}
message Uint256 {
// Exactly 32 bytes, big-endian, unsigned.
bytes be_bytes = 1;
}
message MessageHeader {
StreamKey stream = 1;
uint64 sequence = 2;
bytes previous_message_hash = 3;
uint64 source_timestamp_ms = 4;
bytes source_event_id = 5;
bytes payload_hash = 6;
}
message Message {
MessageHeader header = 1;
repeated Assertion assertions = 2;
}
message BatchHeader {
StreamKey stream = 1;
uint64 batch_number = 2;
bytes previous_batch_hash = 3;
uint64 first_sequence = 4;
uint64 last_sequence = 5;
uint64 sealed_at_ms = 6;
bytes merkle_root = 7;
}
message Batch {
BatchHeader header = 1;
repeated Assertion summary_assertions = 2;
}
message BatchSubmission {
BatchHeader header = 1;
bytes batch_hash = 2;
bytes operator_id = 3;
bytes signature = 4;
}
message RequestRef {
bytes request_id = 1;
bytes origin_message_id = 2;
bytes source_event_id = 3;
}
message StakeMirror {
bytes user = 1;
Uint256 principal = 2;
Uint256 shares = 3;
Uint256 index_at_mint = 4;
}
message UnstakeMirror {
bytes user = 1;
Uint256 amount = 2;
Uint256 shares = 3;
Uint256 index_at_burn = 4;
bytes receipt_id = 5;
}
message PretokenPurchaseMirror {
bytes buyer = 1;
Uint256 principal = 2;
Uint256 shares = 3;
Uint256 index_at_mint = 4;
Uint256 pretokens_out = 5;
}
message BondedActorMirror {
bytes actor = 1;
bytes owner = 2;
bytes bond_level_id = 3;
Uint256 token_id = 4;
uint64 bonded_at_unix_seconds = 5;
}
message UnbondedActorMirror {
bytes actor = 1;
bytes bond_level_id = 2;
Uint256 token_id = 3;
uint64 unbonded_at_unix_seconds = 4;
}
message BondSlashMirror {
bytes actor = 1;
bytes bond_level_id = 2;
Uint256 slashed_amount = 3;
}
message WithdrawRequest {
bytes request_id = 1;
bytes user = 2;
Uint256 amount = 3;
}
message RoleBondRequest {
bytes request_id = 1;
bytes user = 2;
Role role = 3;
Uint256 principal = 4;
uint64 warmup_ends_at_unix_seconds = 5;
}
message RoleUnbondRequest {
bytes request_id = 1;
bytes user = 2;
Role role = 3;
}
message CompleteWithdrawCommand {
RequestRef request = 1;
bytes user = 2;
Uint256 amount = 3;
}
message CompleteUnbondCommand {
RequestRef request = 1;
bytes user = 2;
Role role = 3;
Uint256 principal = 4;
}
message SlashBondCommand {
RequestRef request = 1;
bytes user = 2;
Role role = 3;
Uint256 slashed_amount = 4;
}
message AdminForceUnbondCommand {
RequestRef request = 1;
bytes user = 2;
Role role = 3;
}
message RequestRejected {
RequestRef request = 1;
uint32 code = 2;
string reason = 3;
}

View File

@@ -0,0 +1,345 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WIRE_SYSIO_DIR="${WIRE_SYSIO_DIR:-${ROOT_DIR}/wire-sysio}"
BUILD_DIR="${WIRE_SYSIO_BUILD_DIR:-${WIRE_SYSIO_DIR}/build/debug-docker}"
VCPKG_DIR="${BUILD_DIR}/vcpkg_installed/x64-linux"
WIRE_CDT_DIR="${WIRE_CDT_DIR:-${ROOT_DIR}/wire-cdt}"
WIRE_CDT_BUILD_DIR="${WIRE_CDT_BUILD_DIR:-${WIRE_CDT_DIR}/build/debug-docker}"
WIRE_SYSIO_SUBMODULE_FALLBACK_DIR="${WIRE_SYSIO_SUBMODULE_FALLBACK_DIR:-${ROOT_DIR}/wire-sysio}"
WIRE_SYSIO_BUILD_FALLBACK_DIR="${WIRE_SYSIO_BUILD_FALLBACK_DIR:-${ROOT_DIR}/wire-sysio/build/debug-docker}"
ROUTE="${1:-${WIRE_BATCH_OPERATOR_ROUTE:-}}"
INDEX="${2:-${WIRE_BATCH_OPERATOR_INDEX:-1}}"
OP_ROOT="${WIRE_BATCH_OPERATOR_ROOT:-/srv/wire-batch-operators}"
WIRE_BATCH_OPERATOR_ARTIFACTS_DIR="${WIRE_BATCH_OPERATOR_ARTIFACTS_DIR:-${ROOT_DIR}/.local/e2e}"
CONFIG_DIR="${OP_ROOT}/${ROUTE}/${INDEX}/config"
DATA_DIR="${OP_ROOT}/${ROUTE}/${INDEX}/data"
LOG_DIR="${OP_ROOT}/${ROUTE}/${INDEX}/log"
LOG_FILE="${LOG_DIR}/relay.log"
BUILD_LOCK_FILE="${BUILD_DIR}/batch-operator-relay.lock"
CMAKE_COMMON_ARGS=(
-S "${WIRE_SYSIO_DIR}"
-B "${BUILD_DIR}"
-G Ninja
-DCMAKE_BUILD_TYPE=Debug
-DCMAKE_TOOLCHAIN_FILE="${WIRE_SYSIO_DIR}/vcpkg/scripts/buildsystems/vcpkg.cmake"
-DCMAKE_C_COMPILER=/opt/clang/clang-18/bin/clang
-DCMAKE_CXX_COMPILER=/opt/clang/clang-18/bin/clang++
-DCMAKE_INSTALL_PREFIX=/opt/wire
-DCMAKE_PREFIX_PATH="/opt/wire;/opt/clang/clang-18;${WIRE_CDT_BUILD_DIR}"
-DENABLE_TESTS=OFF
-DBUILD_SYSTEM_CONTRACTS=ON
-DBUILD_TEST_CONTRACTS=OFF
-DCDT_ROOT="${WIRE_CDT_BUILD_DIR}"
)
dir_is_empty() {
local path="$1"
[[ -d "${path}" ]] && [[ -z "$(find "${path}" -mindepth 1 -maxdepth 1 -print -quit 2>/dev/null)" ]]
}
link_submodule_from_fallback() {
local relative_path="$1"
local target_path="${WIRE_SYSIO_DIR}/${relative_path}"
local source_path="${WIRE_SYSIO_SUBMODULE_FALLBACK_DIR}/${relative_path}"
local link_source
if [[ ! -e "${source_path}" ]]; then
return
fi
link_source="$(realpath --relative-to "$(dirname "${target_path}")" "${source_path}")"
if [[ -L "${target_path}" ]] && [[ "$(readlink "${target_path}")" == "${link_source}" ]]; then
return
fi
if [[ -L "${target_path}" ]]; then
ln -sfn "${link_source}" "${target_path}"
return
fi
if dir_is_empty "${target_path}"; then
rmdir "${target_path}"
fi
if [[ ! -e "${target_path}" ]]; then
ln -sfn "${link_source}" "${target_path}"
fi
}
hydrate_wire_submodules() {
link_submodule_from_fallback "libraries/appbase"
link_submodule_from_fallback "vcpkg"
}
hydrate_wire_build_cache() {
local target_install_dir="${BUILD_DIR}/vcpkg_installed"
local source_install_dir="${WIRE_SYSIO_BUILD_FALLBACK_DIR}/vcpkg_installed"
local link_source
if [[ ! -d "${source_install_dir}" ]]; then
return
fi
mkdir -p "$(dirname "${target_install_dir}")"
link_source="$(realpath --relative-to "$(dirname "${target_install_dir}")" "${source_install_dir}")"
if [[ -L "${target_install_dir}" ]] && [[ "$(readlink "${target_install_dir}")" == "${link_source}" ]]; then
return
fi
if [[ -L "${target_install_dir}" ]]; then
ln -sfn "${link_source}" "${target_install_dir}"
return
fi
if [[ -d "${target_install_dir}" ]] && [[ ! -f "${BUILD_DIR}/bin/batch-operator-relay" ]]; then
mv "${target_install_dir}" "${target_install_dir}.partial.$(date +%s)"
fi
if [[ ! -e "${target_install_dir}" ]]; then
ln -s "${link_source}" "${target_install_dir}"
fi
}
require_wire_submodules() {
hydrate_wire_submodules
if [[ ! -f "${WIRE_SYSIO_DIR}/vcpkg/bootstrap-vcpkg.sh" ]] || [[ ! -f "${WIRE_SYSIO_DIR}/libraries/appbase/CMakeLists.txt" ]]; then
echo "wire-sysio submodules are missing. Run: git -C ${WIRE_SYSIO_DIR} submodule update --init --recursive" >&2
exit 1
fi
}
write_pkgconfig_shims() {
local pc_dir="$1"
local lib_dir="$2"
mkdir -p "${pc_dir}"
cat >"${pc_dir}/libssl.pc" <<EOF
prefix=${VCPKG_DIR}
exec_prefix=\${prefix}
libdir=${lib_dir}
includedir=${VCPKG_DIR}/include
Name: libssl
Description: BoringSSL compatibility shim for curl configure
Version: 1.0.0
Libs: -L${lib_dir} -lssl
Cflags: -I${VCPKG_DIR}/include
EOF
cat >"${pc_dir}/libcrypto.pc" <<EOF
prefix=${VCPKG_DIR}
exec_prefix=\${prefix}
libdir=${lib_dir}
includedir=${VCPKG_DIR}/include
Name: libcrypto
Description: BoringSSL compatibility shim for curl configure
Version: 1.0.0
Libs: -L${lib_dir} -lbscrypto
Cflags: -I${VCPKG_DIR}/include
EOF
}
configure_wire_sysio() {
mkdir -p "${BUILD_DIR}"
hydrate_wire_build_cache
if [[ ! -x "${WIRE_SYSIO_DIR}/vcpkg/vcpkg" ]]; then
"${WIRE_SYSIO_DIR}/vcpkg/bootstrap-vcpkg.sh"
fi
cmake "${CMAKE_COMMON_ARGS[@]}" >/dev/null 2>&1 || true
write_pkgconfig_shims "${VCPKG_DIR}/lib/pkgconfig" "${VCPKG_DIR}/lib"
write_pkgconfig_shims "${VCPKG_DIR}/debug/lib/pkgconfig" "${VCPKG_DIR}/debug/lib"
cmake \
"${CMAKE_COMMON_ARGS[@]}" \
-DOPENSSL_INCLUDE_DIR="${VCPKG_DIR}/include" \
-DOPENSSL_SSL_LIBRARY="${VCPKG_DIR}/debug/lib/libssl.a" \
-DOPENSSL_CRYPTO_LIBRARY="${VCPKG_DIR}/debug/lib/libbscrypto.a"
}
build_batch_operator() {
cmake --build "${BUILD_DIR}" --target batch-operator-relay -- -j"$(nproc)"
}
require_var() {
local name="$1"
if [[ -z "${!name:-}" ]]; then
echo "Missing required environment variable: ${name}" >&2
exit 1
fi
}
prepare_dirs() {
mkdir -p "${CONFIG_DIR}" "${DATA_DIR}" "${LOG_DIR}"
}
append_log() {
printf '[%s] %s\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" "$*" >>"${LOG_FILE}"
}
configure_and_build_batch_operator() {
mkdir -p "${BUILD_DIR}"
append_log "Waiting for build lock: ${BUILD_LOCK_FILE}"
{
flock 9
append_log "Acquired build lock"
configure_wire_sysio >>"${LOG_FILE}" 2>&1
build_batch_operator >>"${LOG_FILE}" 2>&1
} 9>"${BUILD_LOCK_FILE}"
}
wait_for_artifact() {
local path="$1"
local attempts="${2:-120}"
for _ in $(seq 1 "${attempts}"); do
if [[ -f "${path}" ]]; then
return 0
fi
sleep 1
done
echo "Timed out waiting for artifact: ${path}" >&2
exit 1
}
source_env_file() {
local path="$1"
set -a
# shellcheck disable=SC1090
source "${path}"
set +a
}
load_generated_artifacts() {
local wire_env="${WIRE_BATCH_OPERATOR_ARTIFACTS_DIR}/wire.env"
local source_env
wait_for_artifact "${wire_env}"
source_env_file "${wire_env}"
case "${ROUTE}" in
eth)
source_env="${WIRE_BATCH_OPERATOR_ARTIFACTS_DIR}/ethereum.env"
;;
sol)
source_env="${WIRE_BATCH_OPERATOR_ARTIFACTS_DIR}/solana.env"
;;
*)
echo "Unsupported route '${ROUTE}'. Expected 'eth' or 'sol'." >&2
exit 1
;;
esac
wait_for_artifact "${source_env}"
source_env_file "${source_env}"
if [[ "${ROUTE}" == "eth" ]]; then
[[ -n "${WIRE_BATCH_OPERATOR_WIRE_RPC_URL:-}" ]] || WIRE_BATCH_OPERATOR_WIRE_RPC_URL="${LOCAL_WIRE_RPC_URL:-http://wire-nodeop:8887}"
[[ -n "${WIRE_BATCH_OPERATOR_WIRE_DEPOT_ACCOUNT:-}" ]] || WIRE_BATCH_OPERATOR_WIRE_DEPOT_ACCOUNT="${LOCAL_WIRE_ETH_DEPOT_ACCOUNT:-}"
[[ -n "${WIRE_BATCH_OPERATOR_WIRE_ACCOUNT:-}" ]] || WIRE_BATCH_OPERATOR_WIRE_ACCOUNT="${LOCAL_WIRE_ETH_OPERATOR_ACCOUNT:-}"
[[ -n "${WIRE_BATCH_OPERATOR_WIRE_PROVIDER_ID:-}" ]] || WIRE_BATCH_OPERATOR_WIRE_PROVIDER_ID="${LOCAL_WIRE_ETH_OPERATOR_PROVIDER_ID:-}"
[[ -n "${WIRE_BATCH_OPERATOR_WIRE_PROVIDER_SPEC:-}" ]] || WIRE_BATCH_OPERATOR_WIRE_PROVIDER_SPEC="${LOCAL_WIRE_ETH_OPERATOR_PROVIDER_SPEC:-}"
[[ -n "${WIRE_BATCH_OPERATOR_SOURCE_CLIENT_ID:-}" ]] || WIRE_BATCH_OPERATOR_SOURCE_CLIENT_ID="${LOCAL_ETH_CLIENT_ID:-}"
[[ -n "${WIRE_BATCH_OPERATOR_SOURCE_PROVIDER_ID:-}" ]] || WIRE_BATCH_OPERATOR_SOURCE_PROVIDER_ID="${LOCAL_ETH_PROVIDER_ID:-}"
[[ -n "${WIRE_BATCH_OPERATOR_SOURCE_PROVIDER_SPEC:-}" ]] || WIRE_BATCH_OPERATOR_SOURCE_PROVIDER_SPEC="${LOCAL_ETH_PROVIDER_SPEC:-}"
[[ -n "${WIRE_BATCH_OPERATOR_SOURCE_RPC_URL:-}" ]] || WIRE_BATCH_OPERATOR_SOURCE_RPC_URL="${LOCAL_ETH_RPC_URL:-}"
[[ -n "${WIRE_BATCH_OPERATOR_ETH_ABI_FILE:-}" ]] || WIRE_BATCH_OPERATOR_ETH_ABI_FILE="${LOCAL_ETH_ABI_FILE:-}"
[[ -n "${WIRE_BATCH_OPERATOR_ETH_OPP_ADDRESS:-}" ]] || WIRE_BATCH_OPERATOR_ETH_OPP_ADDRESS="${LOCAL_ETH_OPP_ADDRESS:-}"
[[ -n "${WIRE_BATCH_OPERATOR_ETH_OPP_INBOUND_ADDRESS:-}" ]] || WIRE_BATCH_OPERATOR_ETH_OPP_INBOUND_ADDRESS="${LOCAL_ETH_OPP_INBOUND_ADDRESS:-}"
else
[[ -n "${WIRE_BATCH_OPERATOR_WIRE_RPC_URL:-}" ]] || WIRE_BATCH_OPERATOR_WIRE_RPC_URL="${LOCAL_WIRE_RPC_URL:-http://wire-nodeop:8887}"
[[ -n "${WIRE_BATCH_OPERATOR_WIRE_DEPOT_ACCOUNT:-}" ]] || WIRE_BATCH_OPERATOR_WIRE_DEPOT_ACCOUNT="${LOCAL_WIRE_SOL_DEPOT_ACCOUNT:-}"
[[ -n "${WIRE_BATCH_OPERATOR_WIRE_ACCOUNT:-}" ]] || WIRE_BATCH_OPERATOR_WIRE_ACCOUNT="${LOCAL_WIRE_SOL_OPERATOR_ACCOUNT:-}"
[[ -n "${WIRE_BATCH_OPERATOR_WIRE_PROVIDER_ID:-}" ]] || WIRE_BATCH_OPERATOR_WIRE_PROVIDER_ID="${LOCAL_WIRE_SOL_OPERATOR_PROVIDER_ID:-}"
[[ -n "${WIRE_BATCH_OPERATOR_WIRE_PROVIDER_SPEC:-}" ]] || WIRE_BATCH_OPERATOR_WIRE_PROVIDER_SPEC="${LOCAL_WIRE_SOL_OPERATOR_PROVIDER_SPEC:-}"
[[ -n "${WIRE_BATCH_OPERATOR_SOURCE_CLIENT_ID:-}" ]] || WIRE_BATCH_OPERATOR_SOURCE_CLIENT_ID="${LOCAL_SOL_CLIENT_ID:-}"
[[ -n "${WIRE_BATCH_OPERATOR_SOURCE_PROVIDER_ID:-}" ]] || WIRE_BATCH_OPERATOR_SOURCE_PROVIDER_ID="${LOCAL_SOL_PROVIDER_ID:-}"
[[ -n "${WIRE_BATCH_OPERATOR_SOURCE_PROVIDER_SPEC:-}" ]] || WIRE_BATCH_OPERATOR_SOURCE_PROVIDER_SPEC="${LOCAL_SOL_PROVIDER_SPEC:-}"
[[ -n "${WIRE_BATCH_OPERATOR_SOURCE_RPC_URL:-}" ]] || WIRE_BATCH_OPERATOR_SOURCE_RPC_URL="${LOCAL_SOL_RPC_URL:-}"
[[ -n "${WIRE_BATCH_OPERATOR_SOL_PROGRAM_ID:-}" ]] || WIRE_BATCH_OPERATOR_SOL_PROGRAM_ID="${LOCAL_SOL_PROGRAM_ID:-}"
[[ -n "${WIRE_BATCH_OPERATOR_SOL_IDL_FILE:-}" ]] || WIRE_BATCH_OPERATOR_SOL_IDL_FILE="${LOCAL_SOL_IDL_FILE:-}"
fi
}
build_args() {
local -n out_args=$1
local route="$2"
require_var WIRE_BATCH_OPERATOR_WIRE_ACCOUNT
require_var WIRE_BATCH_OPERATOR_WIRE_PROVIDER_ID
require_var WIRE_BATCH_OPERATOR_WIRE_PROVIDER_SPEC
require_var WIRE_BATCH_OPERATOR_SOURCE_CLIENT_ID
require_var WIRE_BATCH_OPERATOR_SOURCE_PROVIDER_ID
require_var WIRE_BATCH_OPERATOR_SOURCE_PROVIDER_SPEC
require_var WIRE_BATCH_OPERATOR_SOURCE_RPC_URL
out_args=(
--config-dir "${CONFIG_DIR}"
--data-dir "${DATA_DIR}"
--batch-operator-wire-rpc-url "${WIRE_BATCH_OPERATOR_WIRE_RPC_URL:-http://wire-nodeop:8887}"
--batch-operator-wire-depot-account "${WIRE_BATCH_OPERATOR_WIRE_DEPOT_ACCOUNT:-sysio.depot}"
--batch-operator-wire-operator "${WIRE_BATCH_OPERATOR_WIRE_ACCOUNT},${WIRE_BATCH_OPERATOR_WIRE_PROVIDER_ID}"
--batch-operator-poll-interval-ms "${WIRE_BATCH_OPERATOR_POLL_INTERVAL_MS:-5000}"
--signature-provider "${WIRE_BATCH_OPERATOR_WIRE_PROVIDER_SPEC}"
--signature-provider "${WIRE_BATCH_OPERATOR_SOURCE_PROVIDER_SPEC}"
)
if [[ "${route}" == "eth" ]]; then
require_var WIRE_BATCH_OPERATOR_ETH_OPP_ADDRESS
require_var WIRE_BATCH_OPERATOR_ETH_OPP_INBOUND_ADDRESS
require_var WIRE_BATCH_OPERATOR_ETH_ABI_FILE
out_args+=(
--outpost-ethereum-client "${WIRE_BATCH_OPERATOR_SOURCE_CLIENT_ID},${WIRE_BATCH_OPERATOR_SOURCE_PROVIDER_ID},${WIRE_BATCH_OPERATOR_SOURCE_RPC_URL},${WIRE_BATCH_OPERATOR_ETH_CHAIN_ID:-31337}"
--ethereum-abi-file "${WIRE_BATCH_OPERATOR_ETH_ABI_FILE}"
--batch-operator-ethereum-client-id "${WIRE_BATCH_OPERATOR_SOURCE_CLIENT_ID}"
--batch-operator-ethereum-opp-address "${WIRE_BATCH_OPERATOR_ETH_OPP_ADDRESS}"
--batch-operator-ethereum-opp-inbound-address "${WIRE_BATCH_OPERATOR_ETH_OPP_INBOUND_ADDRESS}"
)
elif [[ "${route}" == "sol" ]]; then
require_var WIRE_BATCH_OPERATOR_SOL_PROGRAM_ID
require_var WIRE_BATCH_OPERATOR_SOL_IDL_FILE
out_args+=(
--outpost-solana-client "${WIRE_BATCH_OPERATOR_SOURCE_CLIENT_ID},${WIRE_BATCH_OPERATOR_SOURCE_PROVIDER_ID},${WIRE_BATCH_OPERATOR_SOURCE_RPC_URL}"
--solana-idl-file "${WIRE_BATCH_OPERATOR_SOL_IDL_FILE}"
--batch-operator-solana-client-id "${WIRE_BATCH_OPERATOR_SOURCE_CLIENT_ID}"
--batch-operator-solana-program-id "${WIRE_BATCH_OPERATOR_SOL_PROGRAM_ID}"
)
else
echo "Unsupported route '${route}'. Expected 'eth' or 'sol'." >&2
exit 1
fi
}
main() {
require_wire_submodules
prepare_dirs
load_generated_artifacts
append_log "Loaded generated artifacts for route=${ROUTE} index=${INDEX}"
configure_and_build_batch_operator
local binary="${BUILD_DIR}/bin/batch-operator-relay"
local args=()
build_args args "${ROUTE}"
append_log "Starting batch-operator route=${ROUTE} index=${INDEX}"
append_log "Config dir: ${CONFIG_DIR}"
append_log "Data dir: ${DATA_DIR}"
exec "${binary}" "${args[@]}" >>"${LOG_FILE}" 2>&1
}
main "$@"

567
wire-nodeop-local.sh Normal file
View File

@@ -0,0 +1,567 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WIRE_SYSIO_DIR="${WIRE_SYSIO_DIR:-${ROOT_DIR}/wire-sysio}"
BUILD_DIR="${WIRE_SYSIO_BUILD_DIR:-${WIRE_SYSIO_DIR}/build/debug-docker}"
VCPKG_DIR="${BUILD_DIR}/vcpkg_installed/x64-linux"
WIRE_CDT_DIR="${WIRE_CDT_DIR:-${ROOT_DIR}/wire-cdt}"
WIRE_CDT_BUILD_DIR="${WIRE_CDT_BUILD_DIR:-${WIRE_CDT_DIR}/build/debug-docker}"
TARGET_ROOT="${WIRE_NODE_ROOT:-/srv/wire-node/chain-001}"
CONTRACTS_DIR="${WIRE_SYSIO_CONTRACTS_DIR:-${BUILD_DIR}/contracts}"
WIRE_ARTIFACTS_DIR="${WIRE_ARTIFACTS_DIR:-${ROOT_DIR}/.local/e2e}"
WIRE_SYSIO_SUBMODULE_FALLBACK_DIR="${WIRE_SYSIO_SUBMODULE_FALLBACK_DIR:-${ROOT_DIR}/wire-sysio}"
WIRE_SYSIO_BUILD_FALLBACK_DIR="${WIRE_SYSIO_BUILD_FALLBACK_DIR:-${ROOT_DIR}/wire-sysio/build/debug-docker}"
WIRE_ETH_DEPOT_ACCOUNT="${WIRE_ETH_DEPOT_ACCOUNT:-sysio.dpeth}"
WIRE_SOL_DEPOT_ACCOUNT="${WIRE_SOL_DEPOT_ACCOUNT:-sysio.dpsol}"
WIRE_INSTASWAP_ACCOUNT="${WIRE_INSTASWAP_ACCOUNT:-sysio.iswap}"
WIRE_WYIELD_ACCOUNT="${WIRE_WYIELD_ACCOUNT:-sysio.wyield}"
WIRE_TEST_USER_ACCOUNT="${WIRE_TEST_USER_ACCOUNT:-userinsta1}"
WIRE_BATCH_OPERATOR_ETH_ACCOUNT="${WIRE_BATCH_OPERATOR_ETH_ACCOUNT:-bopeth111111}"
WIRE_BATCH_OPERATOR_SOL_ACCOUNT="${WIRE_BATCH_OPERATOR_SOL_ACCOUNT:-bopsol111111}"
WIRE_BATCH_OPERATOR_ETH_WIRE_PROVIDER_ID="${WIRE_BATCH_OPERATOR_ETH_WIRE_PROVIDER_ID:-wire-bopeth-1}"
WIRE_BATCH_OPERATOR_SOL_WIRE_PROVIDER_ID="${WIRE_BATCH_OPERATOR_SOL_WIRE_PROVIDER_ID:-wire-bopsol-1}"
WIRE_DEPOT_ACCOUNT_RAM_BYTES="${WIRE_DEPOT_ACCOUNT_RAM_BYTES:-8388608}"
WIRE_APP_ACCOUNT_RAM_BYTES="${WIRE_APP_ACCOUNT_RAM_BYTES:-8388608}"
WIRE_OPERATOR_ACCOUNT_RAM_BYTES="${WIRE_OPERATOR_ACCOUNT_RAM_BYTES:-1048576}"
WIRE_USER_ACCOUNT_RAM_BYTES="${WIRE_USER_ACCOUNT_RAM_BYTES:-1048576}"
CMAKE_COMMON_ARGS=(
-S "${WIRE_SYSIO_DIR}"
-B "${BUILD_DIR}"
-G Ninja
-DCMAKE_BUILD_TYPE=Debug
-DCMAKE_TOOLCHAIN_FILE="${WIRE_SYSIO_DIR}/vcpkg/scripts/buildsystems/vcpkg.cmake"
-DCMAKE_C_COMPILER=/opt/clang/clang-18/bin/clang
-DCMAKE_CXX_COMPILER=/opt/clang/clang-18/bin/clang++
-DCMAKE_INSTALL_PREFIX=/opt/wire
-DCMAKE_PREFIX_PATH="/opt/wire;/opt/clang/clang-18;${WIRE_CDT_BUILD_DIR}"
-DENABLE_TESTS=OFF
-DBUILD_SYSTEM_CONTRACTS=ON
-DCDT_ROOT="${WIRE_CDT_BUILD_DIR}"
)
cleanup() {
if [[ -n "${NODEOP_PID:-}" ]]; then
kill "${NODEOP_PID}" >/dev/null 2>&1 || true
wait "${NODEOP_PID}" >/dev/null 2>&1 || true
fi
if [[ -n "${KIOD_PID:-}" ]]; then
kill "${KIOD_PID}" >/dev/null 2>&1 || true
wait "${KIOD_PID}" >/dev/null 2>&1 || true
fi
}
dir_is_empty() {
local path="$1"
[[ -d "${path}" ]] && [[ -z "$(find "${path}" -mindepth 1 -maxdepth 1 -print -quit 2>/dev/null)" ]]
}
link_submodule_from_fallback() {
local relative_path="$1"
local target_path="${WIRE_SYSIO_DIR}/${relative_path}"
local source_path="${WIRE_SYSIO_SUBMODULE_FALLBACK_DIR}/${relative_path}"
local link_source
if [[ ! -e "${source_path}" ]]; then
return
fi
link_source="$(realpath --relative-to "$(dirname "${target_path}")" "${source_path}")"
if [[ -L "${target_path}" ]] && [[ "$(readlink "${target_path}")" == "${link_source}" ]]; then
return
fi
if [[ -L "${target_path}" ]]; then
ln -sfn "${link_source}" "${target_path}"
return
fi
if dir_is_empty "${target_path}"; then
rmdir "${target_path}"
fi
if [[ ! -e "${target_path}" ]]; then
ln -sfn "${link_source}" "${target_path}"
fi
}
hydrate_wire_submodules() {
link_submodule_from_fallback "libraries/appbase"
link_submodule_from_fallback "vcpkg"
}
hydrate_wire_build_cache() {
local target_install_dir="${BUILD_DIR}/vcpkg_installed"
local source_install_dir="${WIRE_SYSIO_BUILD_FALLBACK_DIR}/vcpkg_installed"
local link_source
if [[ ! -d "${source_install_dir}" ]]; then
return
fi
mkdir -p "$(dirname "${target_install_dir}")"
link_source="$(realpath --relative-to "$(dirname "${target_install_dir}")" "${source_install_dir}")"
if [[ -L "${target_install_dir}" ]] && [[ "$(readlink "${target_install_dir}")" == "${link_source}" ]]; then
return
fi
if [[ -L "${target_install_dir}" ]]; then
ln -sfn "${link_source}" "${target_install_dir}"
return
fi
if [[ -d "${target_install_dir}" ]] && [[ ! -f "${BUILD_DIR}/bin/nodeop" ]]; then
mv "${target_install_dir}" "${target_install_dir}.partial.$(date +%s)"
fi
if [[ ! -e "${target_install_dir}" ]]; then
ln -s "${link_source}" "${target_install_dir}"
fi
}
require_wire_submodules() {
hydrate_wire_submodules
if [[ ! -f "${WIRE_SYSIO_DIR}/vcpkg/bootstrap-vcpkg.sh" ]] || [[ ! -f "${WIRE_SYSIO_DIR}/libraries/appbase/CMakeLists.txt" ]]; then
echo "wire-sysio submodules are missing. Run: git -C ${WIRE_SYSIO_DIR} submodule update --init --recursive" >&2
exit 1
fi
}
write_pkgconfig_shims() {
local pc_dir="$1"
local lib_dir="$2"
mkdir -p "${pc_dir}"
cat >"${pc_dir}/libssl.pc" <<EOF
prefix=${VCPKG_DIR}
exec_prefix=\${prefix}
libdir=${lib_dir}
includedir=${VCPKG_DIR}/include
Name: libssl
Description: BoringSSL compatibility shim for curl configure
Version: 1.0.0
Libs: -L${lib_dir} -lssl
Cflags: -I${VCPKG_DIR}/include
EOF
cat >"${pc_dir}/libcrypto.pc" <<EOF
prefix=${VCPKG_DIR}
exec_prefix=\${prefix}
libdir=${lib_dir}
includedir=${VCPKG_DIR}/include
Name: libcrypto
Description: BoringSSL compatibility shim for curl configure
Version: 1.0.0
Libs: -L${lib_dir} -lbscrypto
Cflags: -I${VCPKG_DIR}/include
EOF
}
configure_wire_sysio() {
mkdir -p "${BUILD_DIR}"
hydrate_wire_build_cache
if [[ ! -x "${WIRE_SYSIO_DIR}/vcpkg/vcpkg" ]]; then
"${WIRE_SYSIO_DIR}/vcpkg/bootstrap-vcpkg.sh"
fi
# First configure pass materializes the vcpkg tree used by the shim files.
cmake "${CMAKE_COMMON_ARGS[@]}" >/dev/null 2>&1 || true
write_pkgconfig_shims "${VCPKG_DIR}/lib/pkgconfig" "${VCPKG_DIR}/lib"
write_pkgconfig_shims "${VCPKG_DIR}/debug/lib/pkgconfig" "${VCPKG_DIR}/debug/lib"
cmake \
"${CMAKE_COMMON_ARGS[@]}" \
-DOPENSSL_INCLUDE_DIR="${VCPKG_DIR}/include" \
-DOPENSSL_SSL_LIBRARY="${VCPKG_DIR}/debug/lib/libssl.a" \
-DOPENSSL_CRYPTO_LIBRARY="${VCPKG_DIR}/debug/lib/libbscrypto.a"
}
build_wire_sysio() {
cmake --build \
"${BUILD_DIR}" \
--target contracts_project-build sys-util clio kiod nodeop \
-- -j"$(nproc)"
}
configure_chain() {
export WIRE_ROOT="${WIRE_ROOT:-${WIRE_SYSIO_DIR}}"
export PATH="${BUILD_DIR}/bin:${PATH}"
sys-util chain-configure \
--contracts="${CONTRACTS_DIR}" \
--target="${TARGET_ROOT}" \
--template=aio \
--overwrite
}
extract_secret() {
local pattern="$1"
local file="$2"
sed -n "s/^${pattern}: //p" "${file}"
}
run_clio() {
clio -u http://127.0.0.1:8887 "$@"
}
run_clio_wallet() {
clio wallet "$@"
}
key_file_for_account() {
local account_name="$1"
echo "${TARGET_ROOT}/secrets/${account_name}_key.txt"
}
ensure_local_key() {
local account_name="$1"
local key_file
local output
key_file="$(key_file_for_account "${account_name}")"
if [[ -f "${key_file}" ]]; then
return
fi
output="$(clio create key --to-console)"
printf '%s\n' "${output}" >"${key_file}"
}
import_key_if_needed() {
local key_file="$1"
local private_key
private_key="$(extract_secret "Private key" "${key_file}")"
if [[ -z "${private_key}" ]]; then
echo "Missing private key in ${key_file}" >&2
exit 1
fi
run_clio_wallet import --name default --private-key "${private_key}" >/dev/null 2>&1 || true
}
ensure_account_exists() {
local account_name="$1"
local key_file
local public_key
key_file="$(key_file_for_account "${account_name}")"
public_key="$(extract_secret "Public key" "${key_file}")"
if [[ -z "${public_key}" ]]; then
echo "Missing public key in ${key_file}" >&2
exit 1
fi
if run_clio get account "${account_name}" >/dev/null 2>&1; then
return
fi
run_clio create account sysio "${account_name}" "${public_key}" >/dev/null
}
ensure_contract_deployed() {
local account_name="$1"
local contract_dir="$2"
run_clio set contract "${account_name}" "${contract_dir}" >/dev/null
}
ensure_account_ram() {
local account_name="$1"
local ram_bytes="$2"
run_clio push action \
sysio \
setacctram \
"[\"${account_name}\",${ram_bytes}]" \
-p sysio@active >/dev/null
}
ensure_account_code_permission() {
local account_name="$1"
run_clio set account permission \
"${account_name}" \
active \
--add-code \
-p "${account_name}@active" >/dev/null
}
table_has_rows() {
local account="$1"
local scope="$2"
local table="$3"
! run_clio get table "${account}" "${scope}" "${table}" | grep -Eq '"rows": *\[[[:space:]]*\]'
}
currency_exists() {
local code="$1"
local output
output="$(run_clio get currency stats sysio.token "${code}" 2>/dev/null || true)"
grep -q "\"${code}\"" <<<"${output}"
}
wire_env_path() {
echo "${WIRE_ARTIFACTS_DIR}/wire.env"
}
write_wire_artifacts() {
local eth_key_file
local sol_key_file
local eth_pub
local eth_priv
local sol_pub
local sol_priv
local out_file
mkdir -p "${WIRE_ARTIFACTS_DIR}"
eth_key_file="$(key_file_for_account "${WIRE_BATCH_OPERATOR_ETH_ACCOUNT}")"
sol_key_file="$(key_file_for_account "${WIRE_BATCH_OPERATOR_SOL_ACCOUNT}")"
eth_pub="$(extract_secret "Public key" "${eth_key_file}")"
eth_priv="$(extract_secret "Private key" "${eth_key_file}")"
sol_pub="$(extract_secret "Public key" "${sol_key_file}")"
sol_priv="$(extract_secret "Private key" "${sol_key_file}")"
out_file="$(wire_env_path)"
cat >"${out_file}" <<EOF
LOCAL_WIRE_RPC_URL=http://wire-nodeop:8887
LOCAL_WIRE_ETH_DEPOT_ACCOUNT=${WIRE_ETH_DEPOT_ACCOUNT}
LOCAL_WIRE_SOL_DEPOT_ACCOUNT=${WIRE_SOL_DEPOT_ACCOUNT}
LOCAL_WIRE_INSTASWAP_ACCOUNT=${WIRE_INSTASWAP_ACCOUNT}
LOCAL_WIRE_WYIELD_ACCOUNT=${WIRE_WYIELD_ACCOUNT}
LOCAL_WIRE_TEST_USER_ACCOUNT=${WIRE_TEST_USER_ACCOUNT}
LOCAL_WIRE_ETH_OPERATOR_ACCOUNT=${WIRE_BATCH_OPERATOR_ETH_ACCOUNT}
LOCAL_WIRE_ETH_OPERATOR_PROVIDER_ID=${WIRE_BATCH_OPERATOR_ETH_WIRE_PROVIDER_ID}
LOCAL_WIRE_ETH_OPERATOR_PROVIDER_SPEC=${WIRE_BATCH_OPERATOR_ETH_WIRE_PROVIDER_ID},wire,wire,${eth_pub},KEY:${eth_priv}
LOCAL_WIRE_SOL_OPERATOR_ACCOUNT=${WIRE_BATCH_OPERATOR_SOL_ACCOUNT}
LOCAL_WIRE_SOL_OPERATOR_PROVIDER_ID=${WIRE_BATCH_OPERATOR_SOL_WIRE_PROVIDER_ID}
LOCAL_WIRE_SOL_OPERATOR_PROVIDER_SPEC=${WIRE_BATCH_OPERATOR_SOL_WIRE_PROVIDER_ID},wire,wire,${sol_pub},KEY:${sol_priv}
EOF
}
bootstrap_wire_stack() {
local eth_seed_id="6574682d706f6f6c2d736565642d3031"
local sol_seed_id="736f6c2d706f6f6c2d736565642d3031"
ensure_local_key "${WIRE_ETH_DEPOT_ACCOUNT}"
ensure_local_key "${WIRE_SOL_DEPOT_ACCOUNT}"
ensure_local_key "${WIRE_INSTASWAP_ACCOUNT}"
ensure_local_key "${WIRE_WYIELD_ACCOUNT}"
ensure_local_key "${WIRE_TEST_USER_ACCOUNT}"
ensure_local_key "${WIRE_BATCH_OPERATOR_ETH_ACCOUNT}"
ensure_local_key "${WIRE_BATCH_OPERATOR_SOL_ACCOUNT}"
import_key_if_needed "$(key_file_for_account "${WIRE_ETH_DEPOT_ACCOUNT}")"
import_key_if_needed "$(key_file_for_account "${WIRE_SOL_DEPOT_ACCOUNT}")"
import_key_if_needed "$(key_file_for_account "${WIRE_INSTASWAP_ACCOUNT}")"
import_key_if_needed "$(key_file_for_account "${WIRE_WYIELD_ACCOUNT}")"
import_key_if_needed "$(key_file_for_account "${WIRE_TEST_USER_ACCOUNT}")"
import_key_if_needed "$(key_file_for_account "${WIRE_BATCH_OPERATOR_ETH_ACCOUNT}")"
import_key_if_needed "$(key_file_for_account "${WIRE_BATCH_OPERATOR_SOL_ACCOUNT}")"
ensure_account_exists "${WIRE_ETH_DEPOT_ACCOUNT}"
ensure_account_exists "${WIRE_SOL_DEPOT_ACCOUNT}"
ensure_account_exists "${WIRE_INSTASWAP_ACCOUNT}"
ensure_account_exists "${WIRE_WYIELD_ACCOUNT}"
ensure_account_exists "${WIRE_TEST_USER_ACCOUNT}"
ensure_account_exists "${WIRE_BATCH_OPERATOR_ETH_ACCOUNT}"
ensure_account_exists "${WIRE_BATCH_OPERATOR_SOL_ACCOUNT}"
ensure_account_ram "${WIRE_ETH_DEPOT_ACCOUNT}" "${WIRE_DEPOT_ACCOUNT_RAM_BYTES}"
ensure_account_ram "${WIRE_SOL_DEPOT_ACCOUNT}" "${WIRE_DEPOT_ACCOUNT_RAM_BYTES}"
ensure_account_ram "${WIRE_INSTASWAP_ACCOUNT}" "${WIRE_APP_ACCOUNT_RAM_BYTES}"
ensure_account_ram "${WIRE_WYIELD_ACCOUNT}" "${WIRE_APP_ACCOUNT_RAM_BYTES}"
ensure_account_ram "${WIRE_TEST_USER_ACCOUNT}" "${WIRE_USER_ACCOUNT_RAM_BYTES}"
ensure_account_ram "${WIRE_BATCH_OPERATOR_ETH_ACCOUNT}" "${WIRE_OPERATOR_ACCOUNT_RAM_BYTES}"
ensure_account_ram "${WIRE_BATCH_OPERATOR_SOL_ACCOUNT}" "${WIRE_OPERATOR_ACCOUNT_RAM_BYTES}"
ensure_account_code_permission "${WIRE_ETH_DEPOT_ACCOUNT}"
ensure_account_code_permission "${WIRE_SOL_DEPOT_ACCOUNT}"
ensure_account_code_permission "${WIRE_INSTASWAP_ACCOUNT}"
ensure_account_code_permission "${WIRE_WYIELD_ACCOUNT}"
ensure_contract_deployed "${WIRE_ETH_DEPOT_ACCOUNT}" "${CONTRACTS_DIR}/sysio.depot"
ensure_contract_deployed "${WIRE_SOL_DEPOT_ACCOUNT}" "${CONTRACTS_DIR}/sysio.depot"
ensure_contract_deployed "${WIRE_INSTASWAP_ACCOUNT}" "${CONTRACTS_DIR}/sysio.instaswap"
ensure_contract_deployed "${WIRE_WYIELD_ACCOUNT}" "${CONTRACTS_DIR}/sysio.wyield"
if ! table_has_rows "${WIRE_ETH_DEPOT_ACCOUNT}" "${WIRE_ETH_DEPOT_ACCOUNT}" depotstate; then
run_clio push action \
"${WIRE_ETH_DEPOT_ACCOUNT}" \
init \
"[2,\"sysio.token\"]" \
-p "${WIRE_ETH_DEPOT_ACCOUNT}@active" >/dev/null
fi
if ! table_has_rows "${WIRE_SOL_DEPOT_ACCOUNT}" "${WIRE_SOL_DEPOT_ACCOUNT}" depotstate; then
run_clio push action \
"${WIRE_SOL_DEPOT_ACCOUNT}" \
init \
"[3,\"sysio.token\"]" \
-p "${WIRE_SOL_DEPOT_ACCOUNT}@active" >/dev/null
fi
if ! currency_exists WIRE; then
run_clio push action \
sysio.token \
create \
"[\"${WIRE_INSTASWAP_ACCOUNT}\",\"1000000000.0000 WIRE\"]" \
-p sysio.token@active >/dev/null
fi
if ! currency_exists LIQETH; then
run_clio push action \
sysio.token \
create \
"[\"${WIRE_INSTASWAP_ACCOUNT}\",\"1000000000.0000 LIQETH\"]" \
-p sysio.token@active >/dev/null
fi
if ! currency_exists LIQSOL; then
run_clio push action \
sysio.token \
create \
"[\"${WIRE_INSTASWAP_ACCOUNT}\",\"1000000000.0000 LIQSOL\"]" \
-p sysio.token@active >/dev/null
fi
if ! run_clio get currency balance sysio.token "${WIRE_INSTASWAP_ACCOUNT}" WIRE | grep -q "WIRE"; then
run_clio push action \
sysio.token \
issue \
"[\"${WIRE_INSTASWAP_ACCOUNT}\",\"1000000.0000 WIRE\",\"local pool seed\"]" \
-p "${WIRE_INSTASWAP_ACCOUNT}@active" >/dev/null
fi
if ! table_has_rows "${WIRE_INSTASWAP_ACCOUNT}" "${WIRE_INSTASWAP_ACCOUNT}" config; then
run_clio push action \
"${WIRE_INSTASWAP_ACCOUNT}" \
init \
"[\"sysio.token\",\"${WIRE_ETH_DEPOT_ACCOUNT}\",\"${WIRE_SOL_DEPOT_ACCOUNT}\",\"${WIRE_INSTASWAP_ACCOUNT}\",\"4,WIRE\",\"4,LIQETH\",\"4,LIQSOL\",30]" \
-p "${WIRE_INSTASWAP_ACCOUNT}@active" >/dev/null
fi
if ! table_has_rows "${WIRE_WYIELD_ACCOUNT}" "${WIRE_WYIELD_ACCOUNT}" config; then
run_clio push action \
"${WIRE_WYIELD_ACCOUNT}" \
init \
"[\"sysio.token\",\"${WIRE_INSTASWAP_ACCOUNT}\",\"${WIRE_ETH_DEPOT_ACCOUNT}\",\"${WIRE_SOL_DEPOT_ACCOUNT}\",\"4,WIRE\",\"4,LIQETH\",\"4,LIQSOL\"]" \
-p "${WIRE_WYIELD_ACCOUNT}@active" >/dev/null
fi
run_clio push action \
"${WIRE_INSTASWAP_ACCOUNT}" \
ondeposit \
"[2,\"${eth_seed_id}\",\"${WIRE_INSTASWAP_ACCOUNT}\",\"10000.0000 LIQETH\"]" \
-p "${WIRE_ETH_DEPOT_ACCOUNT}@active" >/dev/null
run_clio push action \
"${WIRE_INSTASWAP_ACCOUNT}" \
ondeposit \
"[3,\"${sol_seed_id}\",\"${WIRE_INSTASWAP_ACCOUNT}\",\"10000.0000 LIQSOL\"]" \
-p "${WIRE_SOL_DEPOT_ACCOUNT}@active" >/dev/null
if ! run_clio get table "${WIRE_ETH_DEPOT_ACCOUNT}" 2 knownops | grep -q "\"wire_account\": \"${WIRE_BATCH_OPERATOR_ETH_ACCOUNT}\""; then
run_clio push action \
"${WIRE_ETH_DEPOT_ACCOUNT}" \
regoperator \
"[\"${WIRE_BATCH_OPERATOR_ETH_ACCOUNT}\",1,\"010101010101010101010101010101010101010101010101010101010101010101\",\"0202020202020202020202020202020202020202020202020202020202020202\",\"1000.0000 WIRE\"]" \
-p "${WIRE_BATCH_OPERATOR_ETH_ACCOUNT}@active" >/dev/null
fi
if ! run_clio get table "${WIRE_SOL_DEPOT_ACCOUNT}" 3 knownops | grep -q "\"wire_account\": \"${WIRE_BATCH_OPERATOR_SOL_ACCOUNT}\""; then
run_clio push action \
"${WIRE_SOL_DEPOT_ACCOUNT}" \
regoperator \
"[\"${WIRE_BATCH_OPERATOR_SOL_ACCOUNT}\",1,\"030303030303030303030303030303030303030303030303030303030303030303\",\"0404040404040404040404040404040404040404040404040404040404040404\",\"1000.0000 WIRE\"]" \
-p "${WIRE_BATCH_OPERATOR_SOL_ACCOUNT}@active" >/dev/null
fi
if ! run_clio get table "${WIRE_ETH_DEPOT_ACCOUNT}" 2 knownops | grep -q "\"wire_account\": \"${WIRE_BATCH_OPERATOR_ETH_ACCOUNT}\",.*\"status\": 1"; then
run_clio push action \
"${WIRE_ETH_DEPOT_ACCOUNT}" \
activateop \
"[\"${WIRE_BATCH_OPERATOR_ETH_ACCOUNT}\"]" \
-p "${WIRE_ETH_DEPOT_ACCOUNT}@active" >/dev/null
fi
if ! run_clio get table "${WIRE_SOL_DEPOT_ACCOUNT}" 3 knownops | grep -q "\"wire_account\": \"${WIRE_BATCH_OPERATOR_SOL_ACCOUNT}\",.*\"status\": 1"; then
run_clio push action \
"${WIRE_SOL_DEPOT_ACCOUNT}" \
activateop \
"[\"${WIRE_BATCH_OPERATOR_SOL_ACCOUNT}\"]" \
-p "${WIRE_SOL_DEPOT_ACCOUNT}@active" >/dev/null
fi
write_wire_artifacts
}
start_nodeop() {
local key_file="${TARGET_ROOT}/secrets/sysio_key.txt"
local wallet_pw
local sys_public_key
local sys_private_key
local sys_bls_public_key
local sys_bls_private_key
local ready=0
wallet_pw="$(cat "${TARGET_ROOT}/secrets/sysio_wallet_pw.txt")"
sys_public_key="$(extract_secret "Public key" "${key_file}")"
sys_private_key="$(extract_secret "Private key" "${key_file}")"
sys_bls_public_key="$(extract_secret "BLS Pub key" "${key_file}")"
sys_bls_private_key="$(extract_secret "BLS Priv key" "${key_file}")"
pkill -9 kiod >/dev/null 2>&1 || true
kiod --wallet-dir "${TARGET_ROOT}/wallet" >"${TARGET_ROOT}/kiod.log" 2>&1 &
KIOD_PID=$!
sleep 1
clio wallet unlock --name default --password "${wallet_pw}" >/dev/null
nodeop \
--config-dir "${TARGET_ROOT}/config" \
--data-dir "${TARGET_ROOT}/data" \
--genesis-json "${TARGET_ROOT}/config/genesis.json" \
--contracts-console \
--signature-provider "wire-${sys_public_key},wire,wire,${sys_public_key},KEY:${sys_private_key}" \
--signature-provider "wire-bls-${sys_bls_public_key},wire,wire_bls,${sys_bls_public_key},KEY:${sys_bls_private_key}" \
>"${TARGET_ROOT}/nodeop.log" 2>&1 &
NODEOP_PID=$!
for _ in $(seq 1 30); do
if clio -u http://127.0.0.1:8887 get info >/dev/null 2>&1; then
ready=1
break
fi
sleep 1
done
if [[ "${ready}" -ne 1 ]]; then
echo "nodeop did not become ready within 30 seconds" >&2
tail -n 80 "${TARGET_ROOT}/nodeop.log" >&2 || true
return 1
fi
bootstrap_wire_stack
wait "${NODEOP_PID}"
}
trap cleanup EXIT
require_wire_submodules
configure_wire_sysio
build_wire_sysio
configure_chain
start_nodeop