Compare commits
64 Commits
712f114763
...
win-servic
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64a07e0ed6 | ||
|
|
f245a6575d | ||
|
|
e3050bc5ff | ||
|
|
d593eedf01 | ||
|
|
2fb5bb3d84 | ||
|
|
86052c9350 | ||
|
|
e5be55e141 | ||
|
|
8f0eb7130b | ||
|
|
94fe04a6a4 | ||
|
|
976c11902c | ||
|
|
c8d2662a36 | ||
|
|
ac5fedddd1 | ||
|
|
0c2d4986a2 | ||
|
|
a3203936d2 | ||
|
|
fb1c0ec130 | ||
|
|
2a21758369 | ||
|
|
1abb5fa006 | ||
|
|
e1b1c857fa | ||
|
|
4216007af3 | ||
|
|
bbf8a8019c | ||
|
|
ac04495480 | ||
|
|
eb25d31361 | ||
|
|
056ff3470b | ||
|
|
c0b08e84cc | ||
|
|
ddd6e7910f | ||
|
|
d9b3694cab | ||
|
|
4ebe7b6fc4 | ||
|
|
8043cdf8d8 | ||
| 2148faa376 | |||
|
|
eb37ee0a0c | ||
|
|
1f07fd6a98 | ||
|
|
e135519c06 | ||
|
|
f015d345f4 | ||
|
|
51674bb39c | ||
|
|
cd07ab7a78 | ||
|
|
cfa6e068eb | ||
|
|
784261f4d8 | ||
|
|
971db0e919 | ||
|
|
e1a8553142 | ||
|
|
ec70561c93 | ||
|
|
3993d3a8cc | ||
|
|
c87456ae2f | ||
|
|
e89983de3a | ||
|
|
f56668d9f6 | ||
|
|
434738bae5 | ||
|
|
915540de32 | ||
|
|
5a5008080a | ||
|
|
3bc423f9b2 | ||
|
|
f2c33a5bf4 | ||
|
|
3e8b26418a | ||
|
|
60ce1cc110 | ||
|
|
2ff4d0961c | ||
|
|
d61dab3285 | ||
|
|
c439c9645d | ||
|
|
c2883704e6 | ||
| 47caec38a6 | |||
|
|
77c3babec7 | ||
|
|
6f03ce4d1d | ||
|
|
c90af9c196 | ||
|
|
a5a9bc73b0 | ||
|
|
099f76166e | ||
|
|
66026e903a | ||
|
|
3360d3c8c7 | ||
|
|
02980468db |
11
.claude/memory/feedback_widget_decomposition.md
Normal file
11
.claude/memory/feedback_widget_decomposition.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
name: Widget decomposition and provider subscriptions
|
||||||
|
description: Prefer splitting screens into multiple focused files/widgets; each widget subscribes to its own relevant providers
|
||||||
|
type: feedback
|
||||||
|
---
|
||||||
|
|
||||||
|
Split screens into multiple smaller widgets across multiple files. Each widget should subscribe only to the providers it needs (`ref.watch` at lowest possible level), rather than having one large screen widget that watches everything and passes data down as parameters.
|
||||||
|
|
||||||
|
**Why:** Reduces unnecessary rebuilds; improves readability; each file has one clear responsibility.
|
||||||
|
|
||||||
|
**How to apply:** When building a new screen, identify which sub-widgets need their own provider subscriptions and extract them into separate files (e.g., `widgets/grant_card.dart` watches enrichment providers itself, rather than the screen doing it and passing resolved strings down).
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -2,3 +2,5 @@ target/
|
|||||||
scripts/__pycache__/
|
scripts/__pycache__/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.cargo/config.toml
|
.cargo/config.toml
|
||||||
|
.vscode/
|
||||||
|
docs/
|
||||||
|
|||||||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -1,2 +0,0 @@
|
|||||||
{
|
|
||||||
}
|
|
||||||
@@ -22,4 +22,4 @@ steps:
|
|||||||
- apt-get update && apt-get install -y pkg-config
|
- apt-get update && apt-get install -y pkg-config
|
||||||
- mise install rust
|
- mise install rust
|
||||||
- mise install protoc
|
- mise install protoc
|
||||||
- mise exec rust -- cargo clippy --all-targets --all-features -- -D warnings
|
- mise exec rust -- cargo clippy --all -- -D warnings
|
||||||
18
.woodpecker/useragent-analyze.yaml
Normal file
18
.woodpecker/useragent-analyze.yaml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
when:
|
||||||
|
- event: pull_request
|
||||||
|
path:
|
||||||
|
include: ['.woodpecker/useragent-*.yaml', 'useragent/**']
|
||||||
|
- event: push
|
||||||
|
branch: main
|
||||||
|
path:
|
||||||
|
include: ['.woodpecker/useragent-*.yaml', 'useragent/**']
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: analyze
|
||||||
|
image: jdxcode/mise:latest
|
||||||
|
commands:
|
||||||
|
- mise install flutter
|
||||||
|
- mise install protoc
|
||||||
|
# Reruns codegen to catch protocol drift
|
||||||
|
- mise codegen
|
||||||
|
- cd useragent/ && flutter analyze
|
||||||
@@ -67,7 +67,7 @@ The server is actor-based using the **kameo** crate. All long-lived state lives
|
|||||||
|
|
||||||
- **`Bootstrapper`** — Manages the one-time bootstrap token written to `~/.arbiter/bootstrap_token` on first run.
|
- **`Bootstrapper`** — Manages the one-time bootstrap token written to `~/.arbiter/bootstrap_token` on first run.
|
||||||
- **`KeyHolder`** — Holds the encrypted root key and manages the Sealed/Unsealed vault state machine. On unseal, decrypts the root key into a `memsafe` hardened memory cell.
|
- **`KeyHolder`** — Holds the encrypted root key and manages the Sealed/Unsealed vault state machine. On unseal, decrypts the root key into a `memsafe` hardened memory cell.
|
||||||
- **`MessageRouter`** — Coordinates streaming messages between user agents and SDK clients.
|
- **`FlowCoordinator`** — Coordinates cross-connection flow between user agents and SDK clients.
|
||||||
- **`EvmActor`** — Handles EVM transaction policy enforcement and signing.
|
- **`EvmActor`** — Handles EVM transaction policy enforcement and signing.
|
||||||
|
|
||||||
Per-connection actors live under `actors/user_agent/` and `actors/client/`, each with `auth` (challenge-response authentication) and `session` (post-auth operations) sub-modules.
|
Per-connection actors live under `actors/user_agent/` and `actors/client/`, each with `auth` (challenge-response authentication) and `session` (post-auth operations) sub-modules.
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ The server is actor-based using the **kameo** crate. All long-lived state lives
|
|||||||
|
|
||||||
- **`Bootstrapper`** — Manages the one-time bootstrap token written to `~/.arbiter/bootstrap_token` on first run.
|
- **`Bootstrapper`** — Manages the one-time bootstrap token written to `~/.arbiter/bootstrap_token` on first run.
|
||||||
- **`KeyHolder`** — Holds the encrypted root key and manages the Sealed/Unsealed vault state machine. On unseal, decrypts the root key into a `memsafe` hardened memory cell.
|
- **`KeyHolder`** — Holds the encrypted root key and manages the Sealed/Unsealed vault state machine. On unseal, decrypts the root key into a `memsafe` hardened memory cell.
|
||||||
- **`MessageRouter`** — Coordinates streaming messages between user agents and SDK clients.
|
- **`FlowCoordinator`** — Coordinates cross-connection flow between user agents and SDK clients.
|
||||||
- **`EvmActor`** — Handles EVM transaction policy enforcement and signing.
|
- **`EvmActor`** — Handles EVM transaction policy enforcement and signing.
|
||||||
|
|
||||||
Per-connection actors live under `actors/user_agent/` and `actors/client/`, each with `auth` (challenge-response authentication) and `session` (post-auth operations) sub-modules.
|
Per-connection actors live under `actors/user_agent/` and `actors/client/`, each with `auth` (challenge-response authentication) and `session` (post-auth operations) sub-modules.
|
||||||
|
|||||||
@@ -6,6 +6,20 @@ This document covers concrete technology choices and dependencies. For the archi
|
|||||||
|
|
||||||
## Client Connection Flow
|
## Client Connection Flow
|
||||||
|
|
||||||
|
### Authentication Result Semantics
|
||||||
|
|
||||||
|
Authentication no longer uses an implicit success-only response shape. Both `client` and `user-agent` return explicit auth status enums over the wire.
|
||||||
|
|
||||||
|
- **Client:** `AuthResult` may return `SUCCESS`, `INVALID_KEY`, `INVALID_SIGNATURE`, `APPROVAL_DENIED`, `NO_USER_AGENTS_ONLINE`, or `INTERNAL`
|
||||||
|
- **User-agent:** `AuthResult` may return `SUCCESS`, `INVALID_KEY`, `INVALID_SIGNATURE`, `BOOTSTRAP_REQUIRED`, `TOKEN_INVALID`, or `INTERNAL`
|
||||||
|
|
||||||
|
This makes transport-level failures and actor/domain-level auth failures distinct:
|
||||||
|
|
||||||
|
- **Transport/protocol failures** are surfaced as stream/status errors
|
||||||
|
- **Authentication failures** are surfaced as successful protocol responses carrying an explicit auth status
|
||||||
|
|
||||||
|
Clients are expected to handle these status codes directly and present the concrete failure reason to the user.
|
||||||
|
|
||||||
### New Client Approval
|
### New Client Approval
|
||||||
|
|
||||||
When a client whose public key is not yet in the database connects, all connected user agents are asked to approve the connection. The first agent to respond determines the outcome; remaining requests are cancelled via a watch channel.
|
When a client whose public key is not yet in the database connects, all connected user agents are asked to approve the connection. The first agent to respond determines the outcome; remaining requests are cancelled via a watch channel.
|
||||||
@@ -68,9 +82,21 @@ The `program_client.nonce` column stores the **next usable nonce** — i.e. it i
|
|||||||
## Communication
|
## Communication
|
||||||
|
|
||||||
- **Protocol:** gRPC with Protocol Buffers
|
- **Protocol:** gRPC with Protocol Buffers
|
||||||
|
- **Request/response matching:** multiplexed over a single bidirectional stream using per-connection request IDs
|
||||||
- **Server identity distribution:** `ServerInfo` protobuf struct containing the TLS public key fingerprint
|
- **Server identity distribution:** `ServerInfo` protobuf struct containing the TLS public key fingerprint
|
||||||
- **Future consideration:** grpc-web lacks bidirectional stream support, so a browser-based wallet may require protojson over WebSocket
|
- **Future consideration:** grpc-web lacks bidirectional stream support, so a browser-based wallet may require protojson over WebSocket
|
||||||
|
|
||||||
|
### Request Multiplexing
|
||||||
|
|
||||||
|
Both `client` and `user-agent` connections support multiple in-flight requests over one gRPC bidi stream.
|
||||||
|
|
||||||
|
- Every request carries a monotonically increasing request ID
|
||||||
|
- Every normal response echoes the request ID it corresponds to
|
||||||
|
- Out-of-band server messages omit the response ID entirely
|
||||||
|
- The server rejects already-seen request IDs at the transport adapter boundary before business logic sees the message
|
||||||
|
|
||||||
|
This keeps request correlation entirely in transport/client connection code while leaving actor and domain handlers unaware of request IDs.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## EVM Policy Engine
|
## EVM Policy Engine
|
||||||
|
|||||||
1308
docs/superpowers/plans/2026-03-28-grant-creation-refactor.md
Normal file
1308
docs/superpowers/plans/2026-03-28-grant-creation-refactor.md
Normal file
File diff suppressed because it is too large
Load Diff
821
docs/superpowers/plans/2026-03-28-grant-grid-view.md
Normal file
821
docs/superpowers/plans/2026-03-28-grant-grid-view.md
Normal file
@@ -0,0 +1,821 @@
|
|||||||
|
# Grant Grid View Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Add an "EVM Grants" dashboard tab that displays all grants as enriched cards (type, chain, wallet address, client name) with per-card revoke support.
|
||||||
|
|
||||||
|
**Architecture:** A new `walletAccessListProvider` fetches wallet accesses with their DB row IDs. The screen (`grants.dart`) watches only `evmGrantsProvider` for top-level state. Each `GrantCard` widget (its own file) watches enrichment providers (`walletAccessListProvider`, `evmProvider`, `sdkClientsProvider`) and the revoke mutation directly — keeping rebuilds scoped to the card. The screen is registered as a dashboard tab in `AdaptiveScaffold`.
|
||||||
|
|
||||||
|
**Tech Stack:** Flutter, Riverpod (`riverpod_annotation` + `build_runner` codegen), `sizer` (adaptive sizing), `auto_route`, Protocol Buffers (Dart), `Palette` design tokens.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Map
|
||||||
|
|
||||||
|
| File | Action | Responsibility |
|
||||||
|
|---|---|---|
|
||||||
|
| `useragent/lib/theme/palette.dart` | Modify | Add `Palette.token` (indigo accent for token-transfer cards) |
|
||||||
|
| `useragent/lib/features/connection/evm/wallet_access.dart` | Modify | Add `listAllWalletAccesses()` function |
|
||||||
|
| `useragent/lib/providers/sdk_clients/wallet_access_list.dart` | Create | `WalletAccessListProvider` — fetches full wallet access list with IDs |
|
||||||
|
| `useragent/lib/screens/dashboard/evm/grants/widgets/grant_card.dart` | Create | `GrantCard` widget — watches enrichment providers + revoke mutation; one card per grant |
|
||||||
|
| `useragent/lib/screens/dashboard/evm/grants/grants.dart` | Create | `EvmGrantsScreen` — watches `evmGrantsProvider`; handles loading/error/empty/data states; renders `GrantCard` list |
|
||||||
|
| `useragent/lib/router.dart` | Modify | Register `EvmGrantsRoute` in dashboard children |
|
||||||
|
| `useragent/lib/screens/dashboard.dart` | Modify | Add Grants entry to `routes` list and `NavigationDestination` list |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: Add `Palette.token`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `useragent/lib/theme/palette.dart`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add the color**
|
||||||
|
|
||||||
|
Replace the contents of `useragent/lib/theme/palette.dart` with:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class Palette {
|
||||||
|
static const ink = Color(0xFF15263C);
|
||||||
|
static const coral = Color(0xFFE26254);
|
||||||
|
static const cream = Color(0xFFFFFAF4);
|
||||||
|
static const line = Color(0x1A15263C);
|
||||||
|
static const token = Color(0xFF5C6BC0);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Verify**
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd useragent && flutter analyze lib/theme/palette.dart
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: no issues.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```sh
|
||||||
|
jj describe -m "feat(theme): add Palette.token for token-transfer grant cards"
|
||||||
|
jj new
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: Add `listAllWalletAccesses` feature function
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `useragent/lib/features/connection/evm/wallet_access.dart`
|
||||||
|
|
||||||
|
`readClientWalletAccess` (existing) filters the list to one client's wallet IDs and returns `Set<int>`. This new function returns the complete unfiltered list with row IDs so the grant cards can resolve wallet_access_id → wallet + client.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Append function**
|
||||||
|
|
||||||
|
Add at the bottom of `useragent/lib/features/connection/evm/wallet_access.dart`:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
Future<List<SdkClientWalletAccess>> listAllWalletAccesses(
|
||||||
|
Connection connection,
|
||||||
|
) async {
|
||||||
|
final response = await connection.ask(
|
||||||
|
UserAgentRequest(listWalletAccess: Empty()),
|
||||||
|
);
|
||||||
|
if (!response.hasListWalletAccessResponse()) {
|
||||||
|
throw Exception(
|
||||||
|
'Expected list wallet access response, got ${response.whichPayload()}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return response.listWalletAccessResponse.accesses.toList(growable: false);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Each returned `SdkClientWalletAccess` has:
|
||||||
|
- `.id` — the `evm_wallet_access` row ID (same value as `wallet_access_id` in a `GrantEntry`)
|
||||||
|
- `.access.walletId` — the EVM wallet DB ID
|
||||||
|
- `.access.sdkClientId` — the SDK client DB ID
|
||||||
|
|
||||||
|
- [ ] **Step 2: Verify**
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd useragent && flutter analyze lib/features/connection/evm/wallet_access.dart
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: no issues.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```sh
|
||||||
|
jj describe -m "feat(evm): add listAllWalletAccesses feature function"
|
||||||
|
jj new
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3: Create `WalletAccessListProvider`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `useragent/lib/providers/sdk_clients/wallet_access_list.dart`
|
||||||
|
- Generated: `useragent/lib/providers/sdk_clients/wallet_access_list.g.dart`
|
||||||
|
|
||||||
|
Mirrors the structure of `EvmGrants` in `providers/evm/evm_grants.dart` — class-based `@riverpod` with a `refresh()` method.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the provider**
|
||||||
|
|
||||||
|
Create `useragent/lib/providers/sdk_clients/wallet_access_list.dart`:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:arbiter/features/connection/evm/wallet_access.dart';
|
||||||
|
import 'package:arbiter/proto/user_agent.pb.dart';
|
||||||
|
import 'package:arbiter/providers/connection/connection_manager.dart';
|
||||||
|
import 'package:mtcore/markettakers.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
part 'wallet_access_list.g.dart';
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
class WalletAccessList extends _$WalletAccessList {
|
||||||
|
@override
|
||||||
|
Future<List<SdkClientWalletAccess>?> build() async {
|
||||||
|
final connection = await ref.watch(connectionManagerProvider.future);
|
||||||
|
if (connection == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await listAllWalletAccesses(connection);
|
||||||
|
} catch (e, st) {
|
||||||
|
talker.handle(e, st);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> refresh() async {
|
||||||
|
final connection = await ref.read(connectionManagerProvider.future);
|
||||||
|
if (connection == null) {
|
||||||
|
state = const AsyncData(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state = const AsyncLoading();
|
||||||
|
state = await AsyncValue.guard(() => listAllWalletAccesses(connection));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run code generation**
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd useragent && dart run build_runner build --delete-conflicting-outputs
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `useragent/lib/providers/sdk_clients/wallet_access_list.g.dart` created. No errors.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Verify**
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd useragent && flutter analyze lib/providers/sdk_clients/
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: no issues.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```sh
|
||||||
|
jj describe -m "feat(providers): add WalletAccessListProvider"
|
||||||
|
jj new
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4: Create `GrantCard` widget
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `useragent/lib/screens/dashboard/evm/grants/widgets/grant_card.dart`
|
||||||
|
|
||||||
|
This widget owns all per-card logic: enrichment lookups, revoke action, and rebuild scope. The screen only passes it a `GrantEntry` — the card fetches everything else itself.
|
||||||
|
|
||||||
|
**Key types:**
|
||||||
|
- `GrantEntry` (from `proto/evm.pb.dart`): `.id`, `.shared.walletAccessId`, `.shared.chainId`, `.specific.whichGrant()`
|
||||||
|
- `SpecificGrant_Grant.etherTransfer` / `.tokenTransfer` — enum values for the oneof
|
||||||
|
- `SdkClientWalletAccess` (from `proto/user_agent.pb.dart`): `.id`, `.access.walletId`, `.access.sdkClientId`
|
||||||
|
- `WalletEntry` (from `proto/evm.pb.dart`): `.id`, `.address` (List<int>)
|
||||||
|
- `SdkClientEntry` (from `proto/user_agent.pb.dart`): `.id`, `.info.name`
|
||||||
|
- `revokeEvmGrantMutation` — `Mutation<void>` (global; all revoke buttons disable together while any revoke is in flight)
|
||||||
|
- `executeRevokeEvmGrant(ref, grantId: int)` — `Future<void>`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the widget**
|
||||||
|
|
||||||
|
Create `useragent/lib/screens/dashboard/evm/grants/widgets/grant_card.dart`:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:arbiter/proto/evm.pb.dart';
|
||||||
|
import 'package:arbiter/proto/user_agent.pb.dart';
|
||||||
|
import 'package:arbiter/providers/evm/evm.dart';
|
||||||
|
import 'package:arbiter/providers/evm/evm_grants.dart';
|
||||||
|
import 'package:arbiter/providers/sdk_clients/list.dart';
|
||||||
|
import 'package:arbiter/providers/sdk_clients/wallet_access_list.dart';
|
||||||
|
import 'package:arbiter/theme/palette.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/experimental/mutation.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:sizer/sizer.dart';
|
||||||
|
|
||||||
|
String _shortAddress(List<int> bytes) {
|
||||||
|
final hex = bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
|
||||||
|
return '0x${hex.substring(0, 6)}...${hex.substring(hex.length - 4)}';
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatError(Object error) {
|
||||||
|
final message = error.toString();
|
||||||
|
if (message.startsWith('Exception: ')) {
|
||||||
|
return message.substring('Exception: '.length);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
class GrantCard extends ConsumerWidget {
|
||||||
|
const GrantCard({super.key, required this.grant});
|
||||||
|
|
||||||
|
final GrantEntry grant;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
// Enrichment lookups — each watch scopes rebuilds to this card only
|
||||||
|
final walletAccesses =
|
||||||
|
ref.watch(walletAccessListProvider).asData?.value ?? const [];
|
||||||
|
final wallets = ref.watch(evmProvider).asData?.value ?? const [];
|
||||||
|
final clients = ref.watch(sdkClientsProvider).asData?.value ?? const [];
|
||||||
|
final revoking = ref.watch(revokeEvmGrantMutation) is MutationPending;
|
||||||
|
|
||||||
|
final isEther =
|
||||||
|
grant.specific.whichGrant() == SpecificGrant_Grant.etherTransfer;
|
||||||
|
final accent = isEther ? Palette.coral : Palette.token;
|
||||||
|
final typeLabel = isEther ? 'Ether' : 'Token';
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final muted = Palette.ink.withValues(alpha: 0.62);
|
||||||
|
|
||||||
|
// Resolve wallet_access_id → wallet address + client name
|
||||||
|
final accessById = <int, SdkClientWalletAccess>{
|
||||||
|
for (final a in walletAccesses) a.id: a,
|
||||||
|
};
|
||||||
|
final walletById = <int, WalletEntry>{
|
||||||
|
for (final w in wallets) w.id: w,
|
||||||
|
};
|
||||||
|
final clientNameById = <int, String>{
|
||||||
|
for (final c in clients) c.id: c.info.name,
|
||||||
|
};
|
||||||
|
|
||||||
|
final accessId = grant.shared.walletAccessId;
|
||||||
|
final access = accessById[accessId];
|
||||||
|
final wallet = access != null ? walletById[access.access.walletId] : null;
|
||||||
|
|
||||||
|
final walletLabel = wallet != null
|
||||||
|
? _shortAddress(wallet.address)
|
||||||
|
: 'Access #$accessId';
|
||||||
|
|
||||||
|
final clientLabel = () {
|
||||||
|
if (access == null) return '';
|
||||||
|
final name = clientNameById[access.access.sdkClientId] ?? '';
|
||||||
|
return name.isEmpty ? 'Client #${access.access.sdkClientId}' : name;
|
||||||
|
}();
|
||||||
|
|
||||||
|
void showError(String message) {
|
||||||
|
if (!context.mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(message), behavior: SnackBarBehavior.floating),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> revoke() async {
|
||||||
|
try {
|
||||||
|
await executeRevokeEvmGrant(ref, grantId: grant.id);
|
||||||
|
} catch (e) {
|
||||||
|
showError(_formatError(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
color: Palette.cream.withValues(alpha: 0.92),
|
||||||
|
border: Border.all(color: Palette.line),
|
||||||
|
),
|
||||||
|
child: IntrinsicHeight(
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
// Accent strip
|
||||||
|
Container(
|
||||||
|
width: 0.8.w,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: accent,
|
||||||
|
borderRadius: const BorderRadius.horizontal(
|
||||||
|
left: Radius.circular(24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Card body
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 1.6.w,
|
||||||
|
vertical: 1.4.h,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Row 1: type badge · chain · spacer · revoke button
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 1.w,
|
||||||
|
vertical: 0.4.h,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: accent.withValues(alpha: 0.15),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
typeLabel,
|
||||||
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
|
color: accent,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 1.w),
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 1.w,
|
||||||
|
vertical: 0.4.h,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Palette.ink.withValues(alpha: 0.06),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'Chain ${grant.shared.chainId}',
|
||||||
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
|
color: muted,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
if (revoking)
|
||||||
|
SizedBox(
|
||||||
|
width: 1.8.h,
|
||||||
|
height: 1.8.h,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: Palette.coral,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: revoke,
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: Palette.coral,
|
||||||
|
side: BorderSide(
|
||||||
|
color: Palette.coral.withValues(alpha: 0.4),
|
||||||
|
),
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 1.w,
|
||||||
|
vertical: 0.6.h,
|
||||||
|
),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
icon: const Icon(Icons.block_rounded, size: 16),
|
||||||
|
label: const Text('Revoke'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: 0.8.h),
|
||||||
|
// Row 2: wallet address · client name
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
walletLabel,
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: Palette.ink,
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 0.8.w),
|
||||||
|
child: Text(
|
||||||
|
'·',
|
||||||
|
style: theme.textTheme.bodySmall
|
||||||
|
?.copyWith(color: muted),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
clientLabel,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: theme.textTheme.bodySmall
|
||||||
|
?.copyWith(color: muted),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Verify**
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd useragent && flutter analyze lib/screens/dashboard/evm/grants/widgets/grant_card.dart
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: no issues.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```sh
|
||||||
|
jj describe -m "feat(grants): add GrantCard widget with self-contained enrichment"
|
||||||
|
jj new
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 5: Create `EvmGrantsScreen`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `useragent/lib/screens/dashboard/evm/grants/grants.dart`
|
||||||
|
|
||||||
|
The screen watches only `evmGrantsProvider` for top-level state (loading / error / no connection / empty / data). When there is data it renders a list of `GrantCard` widgets — each card manages its own enrichment subscriptions.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the screen**
|
||||||
|
|
||||||
|
Create `useragent/lib/screens/dashboard/evm/grants/grants.dart`:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:arbiter/proto/evm.pb.dart';
|
||||||
|
import 'package:arbiter/providers/evm/evm_grants.dart';
|
||||||
|
import 'package:arbiter/providers/sdk_clients/wallet_access_list.dart';
|
||||||
|
import 'package:arbiter/router.gr.dart';
|
||||||
|
import 'package:arbiter/screens/dashboard/evm/grants/widgets/grant_card.dart';
|
||||||
|
import 'package:arbiter/theme/palette.dart';
|
||||||
|
import 'package:arbiter/widgets/page_header.dart';
|
||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:sizer/sizer.dart';
|
||||||
|
|
||||||
|
String _formatError(Object error) {
|
||||||
|
final message = error.toString();
|
||||||
|
if (message.startsWith('Exception: ')) {
|
||||||
|
return message.substring('Exception: '.length);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── State panel ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class _StatePanel extends StatelessWidget {
|
||||||
|
const _StatePanel({
|
||||||
|
required this.icon,
|
||||||
|
required this.title,
|
||||||
|
required this.body,
|
||||||
|
this.actionLabel,
|
||||||
|
this.onAction,
|
||||||
|
this.busy = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
final IconData icon;
|
||||||
|
final String title;
|
||||||
|
final String body;
|
||||||
|
final String? actionLabel;
|
||||||
|
final Future<void> Function()? onAction;
|
||||||
|
final bool busy;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
color: Palette.cream.withValues(alpha: 0.92),
|
||||||
|
border: Border.all(color: Palette.line),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(2.8.h),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (busy)
|
||||||
|
SizedBox(
|
||||||
|
width: 2.8.h,
|
||||||
|
height: 2.8.h,
|
||||||
|
child: const CircularProgressIndicator(strokeWidth: 2.5),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Icon(icon, size: 34, color: Palette.coral),
|
||||||
|
SizedBox(height: 1.8.h),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: theme.textTheme.headlineSmall?.copyWith(
|
||||||
|
color: Palette.ink,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 1.h),
|
||||||
|
Text(
|
||||||
|
body,
|
||||||
|
style: theme.textTheme.bodyLarge?.copyWith(
|
||||||
|
color: Palette.ink.withValues(alpha: 0.72),
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (actionLabel != null && onAction != null) ...[
|
||||||
|
SizedBox(height: 2.h),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: () => onAction!(),
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
label: Text(actionLabel!),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Grant list ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class _GrantList extends StatelessWidget {
|
||||||
|
const _GrantList({required this.grants});
|
||||||
|
|
||||||
|
final List<GrantEntry> grants;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
for (var i = 0; i < grants.length; i++)
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
bottom: i == grants.length - 1 ? 0 : 1.8.h,
|
||||||
|
),
|
||||||
|
child: GrantCard(grant: grants[i]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Screen ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class EvmGrantsScreen extends ConsumerWidget {
|
||||||
|
const EvmGrantsScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
// Screen watches only the grant list for top-level state decisions
|
||||||
|
final grantsAsync = ref.watch(evmGrantsProvider);
|
||||||
|
|
||||||
|
Future<void> refresh() async {
|
||||||
|
await Future.wait([
|
||||||
|
ref.read(evmGrantsProvider.notifier).refresh(),
|
||||||
|
ref.read(walletAccessListProvider.notifier).refresh(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void showMessage(String message) {
|
||||||
|
if (!context.mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(message), behavior: SnackBarBehavior.floating),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> safeRefresh() async {
|
||||||
|
try {
|
||||||
|
await refresh();
|
||||||
|
} catch (e) {
|
||||||
|
showMessage(_formatError(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final grantsState = grantsAsync.asData?.value;
|
||||||
|
final grants = grantsState?.grants;
|
||||||
|
|
||||||
|
final content = switch (grantsAsync) {
|
||||||
|
AsyncLoading() when grantsState == null => const _StatePanel(
|
||||||
|
icon: Icons.hourglass_top,
|
||||||
|
title: 'Loading grants',
|
||||||
|
body: 'Pulling grant registry from Arbiter.',
|
||||||
|
busy: true,
|
||||||
|
),
|
||||||
|
AsyncError(:final error) => _StatePanel(
|
||||||
|
icon: Icons.sync_problem,
|
||||||
|
title: 'Grant registry unavailable',
|
||||||
|
body: _formatError(error),
|
||||||
|
actionLabel: 'Retry',
|
||||||
|
onAction: safeRefresh,
|
||||||
|
),
|
||||||
|
AsyncData(:final value) when value == null => _StatePanel(
|
||||||
|
icon: Icons.portable_wifi_off,
|
||||||
|
title: 'No active server connection',
|
||||||
|
body: 'Reconnect to Arbiter to list EVM grants.',
|
||||||
|
actionLabel: 'Refresh',
|
||||||
|
onAction: safeRefresh,
|
||||||
|
),
|
||||||
|
_ when grants != null && grants.isEmpty => _StatePanel(
|
||||||
|
icon: Icons.policy_outlined,
|
||||||
|
title: 'No grants yet',
|
||||||
|
body: 'Create a grant to allow SDK clients to sign transactions.',
|
||||||
|
actionLabel: 'Create grant',
|
||||||
|
onAction: () => context.router.push(const CreateEvmGrantRoute()),
|
||||||
|
),
|
||||||
|
_ => _GrantList(grants: grants ?? const []),
|
||||||
|
};
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
body: SafeArea(
|
||||||
|
child: RefreshIndicator.adaptive(
|
||||||
|
color: Palette.ink,
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
onRefresh: safeRefresh,
|
||||||
|
child: ListView(
|
||||||
|
physics: const BouncingScrollPhysics(
|
||||||
|
parent: AlwaysScrollableScrollPhysics(),
|
||||||
|
),
|
||||||
|
padding: EdgeInsets.fromLTRB(2.4.w, 2.4.h, 2.4.w, 3.2.h),
|
||||||
|
children: [
|
||||||
|
PageHeader(
|
||||||
|
title: 'EVM Grants',
|
||||||
|
isBusy: grantsAsync.isLoading,
|
||||||
|
actions: [
|
||||||
|
FilledButton.icon(
|
||||||
|
onPressed: () =>
|
||||||
|
context.router.push(const CreateEvmGrantRoute()),
|
||||||
|
icon: const Icon(Icons.add_rounded),
|
||||||
|
label: const Text('Create grant'),
|
||||||
|
),
|
||||||
|
SizedBox(width: 1.w),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: safeRefresh,
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: Palette.ink,
|
||||||
|
side: BorderSide(color: Palette.line),
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 1.4.w,
|
||||||
|
vertical: 1.2.h,
|
||||||
|
),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
icon: const Icon(Icons.refresh, size: 18),
|
||||||
|
label: const Text('Refresh'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: 1.8.h),
|
||||||
|
content,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Verify**
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd useragent && flutter analyze lib/screens/dashboard/evm/grants/
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: no issues.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```sh
|
||||||
|
jj describe -m "feat(grants): add EvmGrantsScreen"
|
||||||
|
jj new
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 6: Wire router and dashboard tab
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `useragent/lib/router.dart`
|
||||||
|
- Modify: `useragent/lib/screens/dashboard.dart`
|
||||||
|
- Regenerated: `useragent/lib/router.gr.dart`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add route to `router.dart`**
|
||||||
|
|
||||||
|
Replace the contents of `useragent/lib/router.dart` with:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
|
||||||
|
import 'router.gr.dart';
|
||||||
|
|
||||||
|
@AutoRouterConfig(generateForDir: ['lib/screens'])
|
||||||
|
class Router extends RootStackRouter {
|
||||||
|
@override
|
||||||
|
List<AutoRoute> get routes => [
|
||||||
|
AutoRoute(page: Bootstrap.page, path: '/bootstrap', initial: true),
|
||||||
|
AutoRoute(page: ServerInfoSetupRoute.page, path: '/server-info'),
|
||||||
|
AutoRoute(page: ServerConnectionRoute.page, path: '/server-connection'),
|
||||||
|
AutoRoute(page: VaultSetupRoute.page, path: '/vault'),
|
||||||
|
AutoRoute(page: ClientDetailsRoute.page, path: '/clients/:clientId'),
|
||||||
|
AutoRoute(page: CreateEvmGrantRoute.page, path: '/evm-grants/create'),
|
||||||
|
|
||||||
|
AutoRoute(
|
||||||
|
page: DashboardRouter.page,
|
||||||
|
path: '/dashboard',
|
||||||
|
children: [
|
||||||
|
AutoRoute(page: EvmRoute.page, path: 'evm'),
|
||||||
|
AutoRoute(page: ClientsRoute.page, path: 'clients'),
|
||||||
|
AutoRoute(page: EvmGrantsRoute.page, path: 'grants'),
|
||||||
|
AutoRoute(page: AboutRoute.page, path: 'about'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Update `dashboard.dart`**
|
||||||
|
|
||||||
|
In `useragent/lib/screens/dashboard.dart`, replace the `routes` constant:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
final routes = [
|
||||||
|
const EvmRoute(),
|
||||||
|
const ClientsRoute(),
|
||||||
|
const EvmGrantsRoute(),
|
||||||
|
const AboutRoute(),
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
And replace the `destinations` list inside `AdaptiveScaffold`:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
destinations: const [
|
||||||
|
NavigationDestination(
|
||||||
|
icon: Icon(Icons.account_balance_wallet_outlined),
|
||||||
|
selectedIcon: Icon(Icons.account_balance_wallet),
|
||||||
|
label: 'Wallets',
|
||||||
|
),
|
||||||
|
NavigationDestination(
|
||||||
|
icon: Icon(Icons.devices_other_outlined),
|
||||||
|
selectedIcon: Icon(Icons.devices_other),
|
||||||
|
label: 'Clients',
|
||||||
|
),
|
||||||
|
NavigationDestination(
|
||||||
|
icon: Icon(Icons.policy_outlined),
|
||||||
|
selectedIcon: Icon(Icons.policy),
|
||||||
|
label: 'Grants',
|
||||||
|
),
|
||||||
|
NavigationDestination(
|
||||||
|
icon: Icon(Icons.info_outline),
|
||||||
|
selectedIcon: Icon(Icons.info),
|
||||||
|
label: 'About',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Regenerate router**
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd useragent && dart run build_runner build --delete-conflicting-outputs
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `lib/router.gr.dart` updated, `EvmGrantsRoute` now available, no errors.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Full project verify**
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd useragent && flutter analyze
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: no issues.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```sh
|
||||||
|
jj describe -m "feat(nav): add Grants dashboard tab"
|
||||||
|
jj new
|
||||||
|
```
|
||||||
170
docs/superpowers/specs/2026-03-28-grant-grid-view-design.md
Normal file
170
docs/superpowers/specs/2026-03-28-grant-grid-view-design.md
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
# Grant Grid View — Design Spec
|
||||||
|
|
||||||
|
**Date:** 2026-03-28
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Add a "Grants" dashboard tab to the Flutter user-agent app that displays all EVM grants as a card-based grid. Each card shows a compact summary (type, chain, wallet address, client name) with a revoke action. The tab integrates into the existing `AdaptiveScaffold` navigation alongside Wallets, Clients, and About.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- New `walletAccessListProvider` for fetching wallet access entries with their DB row IDs
|
||||||
|
- New `EvmGrantsScreen` as a dashboard tab
|
||||||
|
- Grant card widget with enriched display (type, chain, wallet, client)
|
||||||
|
- Revoke action wired to existing `executeRevokeEvmGrant` mutation
|
||||||
|
- Dashboard tab bar and router updated
|
||||||
|
- New token-transfer accent color added to `Palette`
|
||||||
|
|
||||||
|
**Out of scope:** Fixing grant creation (separate task).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Layer
|
||||||
|
|
||||||
|
### `walletAccessListProvider`
|
||||||
|
|
||||||
|
**File:** `useragent/lib/providers/sdk_clients/wallet_access_list.dart`
|
||||||
|
|
||||||
|
- `@riverpod` class, watches `connectionManagerProvider.future`
|
||||||
|
- Returns `List<SdkClientWalletAccess>?` (null when not connected)
|
||||||
|
- Each entry: `.id` (wallet_access_id), `.access.walletId`, `.access.sdkClientId`
|
||||||
|
- Exposes a `refresh()` method following the same pattern as `EvmGrants.refresh()`
|
||||||
|
|
||||||
|
### Enrichment at render time (Approach A)
|
||||||
|
|
||||||
|
The `EvmGrantsScreen` watches four providers:
|
||||||
|
1. `evmGrantsProvider` — the grant list
|
||||||
|
2. `walletAccessListProvider` — to resolve wallet_access_id → (wallet_id, sdk_client_id)
|
||||||
|
3. `evmProvider` — to resolve wallet_id → wallet address
|
||||||
|
4. `sdkClientsProvider` — to resolve sdk_client_id → client name
|
||||||
|
|
||||||
|
All lookups are in-memory Maps built inside the build method; no extra model class needed.
|
||||||
|
|
||||||
|
Fallbacks:
|
||||||
|
- Wallet address not found → `"Access #N"` where N is the wallet_access_id
|
||||||
|
- Client name not found → `"Client #N"` where N is the sdk_client_id
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Route Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
/dashboard
|
||||||
|
/evm ← existing (Wallets tab)
|
||||||
|
/clients ← existing (Clients tab)
|
||||||
|
/grants ← NEW (Grants tab)
|
||||||
|
/about ← existing
|
||||||
|
|
||||||
|
/evm-grants/create ← existing push route (unchanged)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Changes to `router.dart`
|
||||||
|
|
||||||
|
Add inside dashboard children:
|
||||||
|
```dart
|
||||||
|
AutoRoute(page: EvmGrantsRoute.page, path: 'grants'),
|
||||||
|
```
|
||||||
|
|
||||||
|
### Changes to `dashboard.dart`
|
||||||
|
|
||||||
|
Add to `routes` list:
|
||||||
|
```dart
|
||||||
|
const EvmGrantsRoute()
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `NavigationDestination`:
|
||||||
|
```dart
|
||||||
|
NavigationDestination(
|
||||||
|
icon: Icon(Icons.policy_outlined),
|
||||||
|
selectedIcon: Icon(Icons.policy),
|
||||||
|
label: 'Grants',
|
||||||
|
),
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Screen: `EvmGrantsScreen`
|
||||||
|
|
||||||
|
**File:** `useragent/lib/screens/dashboard/evm/grants/grants.dart`
|
||||||
|
|
||||||
|
```
|
||||||
|
Scaffold
|
||||||
|
└─ SafeArea
|
||||||
|
└─ RefreshIndicator.adaptive (refreshes evmGrantsProvider + walletAccessListProvider)
|
||||||
|
└─ ListView (BouncingScrollPhysics + AlwaysScrollableScrollPhysics)
|
||||||
|
├─ PageHeader
|
||||||
|
│ title: 'EVM Grants'
|
||||||
|
│ isBusy: evmGrantsProvider.isLoading
|
||||||
|
│ actions: [CreateGrantButton, RefreshButton]
|
||||||
|
├─ SizedBox(height: 1.8.h)
|
||||||
|
└─ <content>
|
||||||
|
```
|
||||||
|
|
||||||
|
### State handling
|
||||||
|
|
||||||
|
Matches the pattern from `EvmScreen` and `ClientsScreen`:
|
||||||
|
|
||||||
|
| State | Display |
|
||||||
|
|---|---|
|
||||||
|
| Loading (no data yet) | `_StatePanel` with spinner, "Loading grants" |
|
||||||
|
| Error | `_StatePanel` with coral icon, error message, Retry button |
|
||||||
|
| No connection | `_StatePanel`, "No active server connection" |
|
||||||
|
| Empty list | `_StatePanel`, "No grants yet", with Create Grant shortcut |
|
||||||
|
| Data | Column of `_GrantCard` widgets |
|
||||||
|
|
||||||
|
### Header actions
|
||||||
|
|
||||||
|
**CreateGrantButton:** `FilledButton.icon` with `Icons.add_rounded`, pushes `CreateEvmGrantRoute()` via `context.router.push(...)`.
|
||||||
|
|
||||||
|
**RefreshButton:** `OutlinedButton.icon` with `Icons.refresh`, calls `ref.read(evmGrantsProvider.notifier).refresh()`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Grant Card: `_GrantCard`
|
||||||
|
|
||||||
|
**Layout:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Container (rounded 24, Palette.cream bg, Palette.line border)
|
||||||
|
└─ IntrinsicHeight > Row
|
||||||
|
├─ Accent strip (0.8.w wide, full height, rounded left)
|
||||||
|
└─ Padding > Column
|
||||||
|
├─ Row 1: TypeBadge + ChainChip + Spacer + RevokeButton
|
||||||
|
└─ Row 2: WalletText + "·" + ClientText
|
||||||
|
```
|
||||||
|
|
||||||
|
**Accent color by grant type:**
|
||||||
|
- Ether transfer → `Palette.coral`
|
||||||
|
- Token transfer → `Palette.token` (new entry in `Palette` — indigo, e.g. `Color(0xFF5C6BC0)`)
|
||||||
|
|
||||||
|
**TypeBadge:** Small pill container with accent color background at 15% opacity, accent-colored text. Label: `'Ether'` or `'Token'`.
|
||||||
|
|
||||||
|
**ChainChip:** Small container: `'Chain ${grant.shared.chainId}'`, muted ink color.
|
||||||
|
|
||||||
|
**WalletText:** Short hex address (`0xabc...def`) from wallet lookup, `bodySmall`, monospace font family.
|
||||||
|
|
||||||
|
**ClientText:** Client name from `sdkClientsProvider` lookup, or fallback string. `bodySmall`, muted ink.
|
||||||
|
|
||||||
|
**RevokeButton:**
|
||||||
|
- `OutlinedButton` with `Icons.block_rounded` icon, label `'Revoke'`
|
||||||
|
- `foregroundColor: Palette.coral`, `side: BorderSide(color: Palette.coral.withValues(alpha: 0.4))`
|
||||||
|
- Disabled (replaced with `CircularProgressIndicator`) while `revokeEvmGrantMutation` is pending — note: this is a single global mutation, so all revoke buttons disable while any revoke is in flight
|
||||||
|
- On press: calls `executeRevokeEvmGrant(ref, grantId: grant.id)`; shows `SnackBar` on error
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Adaptive Sizing
|
||||||
|
|
||||||
|
All sizing uses `sizer` units (`1.h`, `1.w`, etc.). No hardcoded pixel values.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files to Create / Modify
|
||||||
|
|
||||||
|
| File | Action |
|
||||||
|
|---|---|
|
||||||
|
| `lib/theme/palette.dart` | Modify — add `Palette.token` color |
|
||||||
|
| `lib/providers/sdk_clients/wallet_access_list.dart` | Create |
|
||||||
|
| `lib/screens/dashboard/evm/grants/grants.dart` | Create |
|
||||||
|
| `lib/router.dart` | Modify — add grants route to dashboard children |
|
||||||
|
| `lib/screens/dashboard.dart` | Modify — add tab to routes list and NavigationDestinations |
|
||||||
121
mise.lock
121
mise.lock
@@ -1,19 +1,44 @@
|
|||||||
|
# @generated - this file is auto-generated by `mise lock` https://mise.jdx.dev/dev-tools/mise-lock.html
|
||||||
|
|
||||||
[[tools.ast-grep]]
|
[[tools.ast-grep]]
|
||||||
version = "0.42.0"
|
version = "0.42.0"
|
||||||
backend = "aqua:ast-grep/ast-grep"
|
backend = "aqua:ast-grep/ast-grep"
|
||||||
"platforms.linux-arm64" = { checksum = "sha256:5c830eae8456569e2f7212434ed9c238f58dca412d76045418ed6d394a755836", url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-aarch64-unknown-linux-gnu.zip"}
|
|
||||||
"platforms.linux-x64" = { checksum = "sha256:e825a05603f0bcc4cd9076c4cc8c9abd6d008b7cd07d9aa3cc323ba4b8606651", url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-x86_64-unknown-linux-gnu.zip"}
|
[tools.ast-grep."platforms.linux-arm64"]
|
||||||
"platforms.macos-arm64" = { checksum = "sha256:fc300d5293b1c770a5aece03a8a193b92e71e87cec726c28096990691a582620", url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-aarch64-apple-darwin.zip"}
|
checksum = "sha256:5c830eae8456569e2f7212434ed9c238f58dca412d76045418ed6d394a755836"
|
||||||
"platforms.macos-x64" = { checksum = "sha256:979ffe611327056f4730a1ae71b0209b3b830f58b22c6ed194cda34f55400db2", url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-x86_64-apple-darwin.zip"}
|
url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-aarch64-unknown-linux-gnu.zip"
|
||||||
"platforms.windows-x64" = { checksum = "sha256:55836fa1b2c65dc7d61615a4d9368622a0d2371a76d28b9a165e5a3ab6ae32a4", url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-x86_64-pc-windows-msvc.zip"}
|
|
||||||
|
[tools.ast-grep."platforms.linux-arm64-musl"]
|
||||||
|
checksum = "sha256:5c830eae8456569e2f7212434ed9c238f58dca412d76045418ed6d394a755836"
|
||||||
|
url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-aarch64-unknown-linux-gnu.zip"
|
||||||
|
|
||||||
|
[tools.ast-grep."platforms.linux-x64"]
|
||||||
|
checksum = "sha256:e825a05603f0bcc4cd9076c4cc8c9abd6d008b7cd07d9aa3cc323ba4b8606651"
|
||||||
|
url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-x86_64-unknown-linux-gnu.zip"
|
||||||
|
|
||||||
|
[tools.ast-grep."platforms.linux-x64-musl"]
|
||||||
|
checksum = "sha256:e825a05603f0bcc4cd9076c4cc8c9abd6d008b7cd07d9aa3cc323ba4b8606651"
|
||||||
|
url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-x86_64-unknown-linux-gnu.zip"
|
||||||
|
|
||||||
|
[tools.ast-grep."platforms.macos-arm64"]
|
||||||
|
checksum = "sha256:fc300d5293b1c770a5aece03a8a193b92e71e87cec726c28096990691a582620"
|
||||||
|
url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-aarch64-apple-darwin.zip"
|
||||||
|
|
||||||
|
[tools.ast-grep."platforms.macos-x64"]
|
||||||
|
checksum = "sha256:979ffe611327056f4730a1ae71b0209b3b830f58b22c6ed194cda34f55400db2"
|
||||||
|
url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-x86_64-apple-darwin.zip"
|
||||||
|
|
||||||
|
[tools.ast-grep."platforms.windows-x64"]
|
||||||
|
checksum = "sha256:55836fa1b2c65dc7d61615a4d9368622a0d2371a76d28b9a165e5a3ab6ae32a4"
|
||||||
|
url = "https://github.com/ast-grep/ast-grep/releases/download/0.42.0/app-x86_64-pc-windows-msvc.zip"
|
||||||
|
|
||||||
[[tools."cargo:cargo-audit"]]
|
[[tools."cargo:cargo-audit"]]
|
||||||
version = "0.22.1"
|
version = "0.22.1"
|
||||||
backend = "cargo:cargo-audit"
|
backend = "cargo:cargo-audit"
|
||||||
|
|
||||||
[[tools."cargo:cargo-features"]]
|
[[tools."cargo:cargo-edit"]]
|
||||||
version = "1.0.0"
|
version = "0.13.9"
|
||||||
backend = "cargo:cargo-features"
|
backend = "cargo:cargo-edit"
|
||||||
|
|
||||||
[[tools."cargo:cargo-features-manager"]]
|
[[tools."cargo:cargo-features-manager"]]
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
@@ -28,21 +53,13 @@ version = "0.9.126"
|
|||||||
backend = "cargo:cargo-nextest"
|
backend = "cargo:cargo-nextest"
|
||||||
|
|
||||||
[[tools."cargo:cargo-shear"]]
|
[[tools."cargo:cargo-shear"]]
|
||||||
version = "1.9.1"
|
version = "1.11.2"
|
||||||
backend = "cargo:cargo-shear"
|
backend = "cargo:cargo-shear"
|
||||||
|
|
||||||
[[tools."cargo:cargo-vet"]]
|
[[tools."cargo:cargo-vet"]]
|
||||||
version = "0.10.2"
|
version = "0.10.2"
|
||||||
backend = "cargo:cargo-vet"
|
backend = "cargo:cargo-vet"
|
||||||
|
|
||||||
[[tools."cargo:diesel-cli"]]
|
|
||||||
version = "2.3.6"
|
|
||||||
backend = "cargo:diesel-cli"
|
|
||||||
|
|
||||||
[tools."cargo:diesel-cli".options]
|
|
||||||
default-features = "false"
|
|
||||||
features = "sqlite,sqlite-bundled"
|
|
||||||
|
|
||||||
[[tools."cargo:diesel_cli"]]
|
[[tools."cargo:diesel_cli"]]
|
||||||
version = "2.3.6"
|
version = "2.3.6"
|
||||||
backend = "cargo:diesel_cli"
|
backend = "cargo:diesel_cli"
|
||||||
@@ -51,10 +68,6 @@ backend = "cargo:diesel_cli"
|
|||||||
default-features = "false"
|
default-features = "false"
|
||||||
features = "sqlite,sqlite-bundled"
|
features = "sqlite,sqlite-bundled"
|
||||||
|
|
||||||
[[tools."cargo:rinf_cli"]]
|
|
||||||
version = "8.9.1"
|
|
||||||
backend = "cargo:rinf_cli"
|
|
||||||
|
|
||||||
[[tools.flutter]]
|
[[tools.flutter]]
|
||||||
version = "3.38.9-stable"
|
version = "3.38.9-stable"
|
||||||
backend = "asdf:flutter"
|
backend = "asdf:flutter"
|
||||||
@@ -62,20 +75,66 @@ backend = "asdf:flutter"
|
|||||||
[[tools.protoc]]
|
[[tools.protoc]]
|
||||||
version = "29.6"
|
version = "29.6"
|
||||||
backend = "aqua:protocolbuffers/protobuf/protoc"
|
backend = "aqua:protocolbuffers/protobuf/protoc"
|
||||||
"platforms.linux-arm64" = { checksum = "sha256:2594ff4fcae8cb57310d394d0961b236190ad9c5efbfdf1f597ea471d424fe79", url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-linux-aarch_64.zip"}
|
|
||||||
"platforms.linux-x64" = { checksum = "sha256:48785a926e73ffa3f68e2f22b14e7b849620c7a1d36809ac9249a5495e280323", url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-linux-x86_64.zip"}
|
[tools.protoc."platforms.linux-arm64"]
|
||||||
"platforms.macos-arm64" = { checksum = "sha256:b9576b5fa1a1ef3fe13a8c91d9d8204b46545759bea5ae155cd6ba2ea4cdaeed", url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-osx-aarch_64.zip"}
|
checksum = "sha256:2594ff4fcae8cb57310d394d0961b236190ad9c5efbfdf1f597ea471d424fe79"
|
||||||
"platforms.macos-x64" = { checksum = "sha256:312f04713946921cc0187ef34df80241ddca1bab6f564c636885fd2cc90d3f88", url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-osx-x86_64.zip"}
|
url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-linux-aarch_64.zip"
|
||||||
"platforms.windows-x64" = { checksum = "sha256:1ebd7c87baffb9f1c47169b640872bf5fb1e4408079c691af527be9561d8f6f7", url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-win64.zip"}
|
|
||||||
|
[tools.protoc."platforms.linux-arm64-musl"]
|
||||||
|
checksum = "sha256:2594ff4fcae8cb57310d394d0961b236190ad9c5efbfdf1f597ea471d424fe79"
|
||||||
|
url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-linux-aarch_64.zip"
|
||||||
|
|
||||||
|
[tools.protoc."platforms.linux-x64"]
|
||||||
|
checksum = "sha256:48785a926e73ffa3f68e2f22b14e7b849620c7a1d36809ac9249a5495e280323"
|
||||||
|
url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-linux-x86_64.zip"
|
||||||
|
|
||||||
|
[tools.protoc."platforms.linux-x64-musl"]
|
||||||
|
checksum = "sha256:48785a926e73ffa3f68e2f22b14e7b849620c7a1d36809ac9249a5495e280323"
|
||||||
|
url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-linux-x86_64.zip"
|
||||||
|
|
||||||
|
[tools.protoc."platforms.macos-arm64"]
|
||||||
|
checksum = "sha256:b9576b5fa1a1ef3fe13a8c91d9d8204b46545759bea5ae155cd6ba2ea4cdaeed"
|
||||||
|
url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-osx-aarch_64.zip"
|
||||||
|
|
||||||
|
[tools.protoc."platforms.macos-x64"]
|
||||||
|
checksum = "sha256:312f04713946921cc0187ef34df80241ddca1bab6f564c636885fd2cc90d3f88"
|
||||||
|
url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-osx-x86_64.zip"
|
||||||
|
|
||||||
|
[tools.protoc."platforms.windows-x64"]
|
||||||
|
checksum = "sha256:1ebd7c87baffb9f1c47169b640872bf5fb1e4408079c691af527be9561d8f6f7"
|
||||||
|
url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-win64.zip"
|
||||||
|
|
||||||
[[tools.python]]
|
[[tools.python]]
|
||||||
version = "3.14.3"
|
version = "3.14.3"
|
||||||
backend = "core:python"
|
backend = "core:python"
|
||||||
"platforms.linux-arm64" = { checksum = "sha256:be0f4dc2932f762292b27d46ea7d3e8e66ddf3969a5eb0254a229015ed402625", url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz"}
|
|
||||||
"platforms.linux-x64" = { checksum = "sha256:0a73413f89efd417871876c9accaab28a9d1e3cd6358fbfff171a38ec99302f0", url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz"}
|
[tools.python."platforms.linux-arm64"]
|
||||||
"platforms.macos-arm64" = { checksum = "sha256:4703cdf18b26798fde7b49b6b66149674c25f97127be6a10dbcf29309bdcdcdb", url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-aarch64-apple-darwin-install_only_stripped.tar.gz"}
|
checksum = "sha256:53700338695e402a1a1fe22be4a41fbdacc70e22bb308a48eca8ed67cb7992be"
|
||||||
"platforms.macos-x64" = { checksum = "sha256:76f1cc26e3d262eae8ca546a93e8bded10cf0323613f7e246fea2e10a8115eb7", url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-x86_64-apple-darwin-install_only_stripped.tar.gz"}
|
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz"
|
||||||
"platforms.windows-x64" = { checksum = "sha256:950c5f21a015c1bdd1337f233456df2470fab71e4d794407d27a84cb8b9909a0", url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260303/cpython-3.14.3+20260303-x86_64-pc-windows-msvc-install_only_stripped.tar.gz"}
|
|
||||||
|
[tools.python."platforms.linux-arm64-musl"]
|
||||||
|
checksum = "sha256:53700338695e402a1a1fe22be4a41fbdacc70e22bb308a48eca8ed67cb7992be"
|
||||||
|
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz"
|
||||||
|
|
||||||
|
[tools.python."platforms.linux-x64"]
|
||||||
|
checksum = "sha256:d7a9f970914bb4c88756fe3bdcc186d4feb90e9500e54f1db47dae4dc9687e39"
|
||||||
|
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz"
|
||||||
|
|
||||||
|
[tools.python."platforms.linux-x64-musl"]
|
||||||
|
checksum = "sha256:d7a9f970914bb4c88756fe3bdcc186d4feb90e9500e54f1db47dae4dc9687e39"
|
||||||
|
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz"
|
||||||
|
|
||||||
|
[tools.python."platforms.macos-arm64"]
|
||||||
|
checksum = "sha256:c43aecde4a663aebff99b9b83da0efec506479f1c3f98331442f33d2c43501f9"
|
||||||
|
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-aarch64-apple-darwin-install_only_stripped.tar.gz"
|
||||||
|
|
||||||
|
[tools.python."platforms.macos-x64"]
|
||||||
|
checksum = "sha256:9ab41dbc2f100a2a45d1833b9c11165f51051c558b5213eda9a9731d5948a0c0"
|
||||||
|
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-x86_64-apple-darwin-install_only_stripped.tar.gz"
|
||||||
|
|
||||||
|
[tools.python."platforms.windows-x64"]
|
||||||
|
checksum = "sha256:bbe19034b35b0267176a7442575ae7dc6343480fd4d35598cb7700173d431e09"
|
||||||
|
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260324/cpython-3.14.3+20260324-x86_64-pc-windows-msvc-install_only_stripped.tar.gz"
|
||||||
|
|
||||||
[[tools.rust]]
|
[[tools.rust]]
|
||||||
version = "1.93.0"
|
version = "1.93.0"
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ protoc = "29.6"
|
|||||||
"cargo:cargo-insta" = "1.46.3"
|
"cargo:cargo-insta" = "1.46.3"
|
||||||
python = "3.14.3"
|
python = "3.14.3"
|
||||||
ast-grep = "0.42.0"
|
ast-grep = "0.42.0"
|
||||||
|
"cargo:cargo-edit" = "0.13.9"
|
||||||
|
|
||||||
[tasks.codegen]
|
[tasks.codegen]
|
||||||
sources = ['protobufs/*.proto']
|
sources = ['protobufs/*.proto']
|
||||||
|
|||||||
@@ -3,9 +3,17 @@ syntax = "proto3";
|
|||||||
package arbiter.client;
|
package arbiter.client;
|
||||||
|
|
||||||
import "evm.proto";
|
import "evm.proto";
|
||||||
|
import "google/protobuf/empty.proto";
|
||||||
|
|
||||||
|
message ClientInfo {
|
||||||
|
string name = 1;
|
||||||
|
optional string description = 2;
|
||||||
|
optional string version = 3;
|
||||||
|
}
|
||||||
|
|
||||||
message AuthChallengeRequest {
|
message AuthChallengeRequest {
|
||||||
bytes pubkey = 1;
|
bytes pubkey = 1;
|
||||||
|
ClientInfo client_info = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message AuthChallenge {
|
message AuthChallenge {
|
||||||
@@ -17,30 +25,40 @@ message AuthChallengeSolution {
|
|||||||
bytes signature = 1;
|
bytes signature = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message AuthOk {}
|
enum AuthResult {
|
||||||
|
AUTH_RESULT_UNSPECIFIED = 0;
|
||||||
|
AUTH_RESULT_SUCCESS = 1;
|
||||||
|
AUTH_RESULT_INVALID_KEY = 2;
|
||||||
|
AUTH_RESULT_INVALID_SIGNATURE = 3;
|
||||||
|
AUTH_RESULT_APPROVAL_DENIED = 4;
|
||||||
|
AUTH_RESULT_NO_USER_AGENTS_ONLINE = 5;
|
||||||
|
AUTH_RESULT_INTERNAL = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum VaultState {
|
||||||
|
VAULT_STATE_UNSPECIFIED = 0;
|
||||||
|
VAULT_STATE_UNBOOTSTRAPPED = 1;
|
||||||
|
VAULT_STATE_SEALED = 2;
|
||||||
|
VAULT_STATE_UNSEALED = 3;
|
||||||
|
VAULT_STATE_ERROR = 4;
|
||||||
|
}
|
||||||
|
|
||||||
message ClientRequest {
|
message ClientRequest {
|
||||||
|
int32 request_id = 4;
|
||||||
oneof payload {
|
oneof payload {
|
||||||
AuthChallengeRequest auth_challenge_request = 1;
|
AuthChallengeRequest auth_challenge_request = 1;
|
||||||
AuthChallengeSolution auth_challenge_solution = 2;
|
AuthChallengeSolution auth_challenge_solution = 2;
|
||||||
|
google.protobuf.Empty query_vault_state = 3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message ClientConnectError {
|
|
||||||
enum Code {
|
|
||||||
UNKNOWN = 0;
|
|
||||||
APPROVAL_DENIED = 1;
|
|
||||||
NO_USER_AGENTS_ONLINE = 2;
|
|
||||||
}
|
|
||||||
Code code = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message ClientResponse {
|
message ClientResponse {
|
||||||
|
optional int32 request_id = 7;
|
||||||
oneof payload {
|
oneof payload {
|
||||||
AuthChallenge auth_challenge = 1;
|
AuthChallenge auth_challenge = 1;
|
||||||
AuthOk auth_ok = 2;
|
AuthResult auth_result = 2;
|
||||||
ClientConnectError client_connect_error = 5;
|
|
||||||
arbiter.evm.EvmSignTransactionResponse evm_sign_transaction = 3;
|
arbiter.evm.EvmSignTransactionResponse evm_sign_transaction = 3;
|
||||||
arbiter.evm.EvmAnalyzeTransactionResponse evm_analyze_transaction = 4;
|
arbiter.evm.EvmAnalyzeTransactionResponse evm_analyze_transaction = 4;
|
||||||
|
VaultState vault_state = 6;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ enum EvmError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message WalletEntry {
|
message WalletEntry {
|
||||||
bytes address = 1; // 20-byte Ethereum address
|
int32 id = 1;
|
||||||
|
bytes address = 2; // 20-byte Ethereum address
|
||||||
}
|
}
|
||||||
|
|
||||||
message WalletList {
|
message WalletList {
|
||||||
@@ -46,7 +47,7 @@ message VolumeRateLimit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message SharedSettings {
|
message SharedSettings {
|
||||||
int32 wallet_id = 1;
|
int32 wallet_access_id = 1;
|
||||||
uint64 chain_id = 2;
|
uint64 chain_id = 2;
|
||||||
optional google.protobuf.Timestamp valid_from = 3;
|
optional google.protobuf.Timestamp valid_from = 3;
|
||||||
optional google.protobuf.Timestamp valid_until = 4;
|
optional google.protobuf.Timestamp valid_until = 4;
|
||||||
@@ -139,9 +140,8 @@ message TransactionEvalError {
|
|||||||
|
|
||||||
// --- UserAgent grant management ---
|
// --- UserAgent grant management ---
|
||||||
message EvmGrantCreateRequest {
|
message EvmGrantCreateRequest {
|
||||||
int32 client_id = 1;
|
SharedSettings shared = 1;
|
||||||
SharedSettings shared = 2;
|
SpecificGrant specific = 2;
|
||||||
SpecificGrant specific = 3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message EvmGrantCreateResponse {
|
message EvmGrantCreateResponse {
|
||||||
@@ -165,13 +165,13 @@ message EvmGrantDeleteResponse {
|
|||||||
// Basic grant info returned in grant listings
|
// Basic grant info returned in grant listings
|
||||||
message GrantEntry {
|
message GrantEntry {
|
||||||
int32 id = 1;
|
int32 id = 1;
|
||||||
int32 client_id = 2;
|
int32 wallet_access_id = 2;
|
||||||
SharedSettings shared = 3;
|
SharedSettings shared = 3;
|
||||||
SpecificGrant specific = 4;
|
SpecificGrant specific = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message EvmGrantListRequest {
|
message EvmGrantListRequest {
|
||||||
optional int32 wallet_id = 1;
|
optional int32 wallet_access_id = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message EvmGrantListResponse {
|
message EvmGrantListResponse {
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ syntax = "proto3";
|
|||||||
|
|
||||||
package arbiter.user_agent;
|
package arbiter.user_agent;
|
||||||
|
|
||||||
import "google/protobuf/empty.proto";
|
import "client.proto";
|
||||||
import "evm.proto";
|
import "evm.proto";
|
||||||
|
import "google/protobuf/empty.proto";
|
||||||
|
|
||||||
enum KeyType {
|
enum KeyType {
|
||||||
KEY_TYPE_UNSPECIFIED = 0;
|
KEY_TYPE_UNSPECIFIED = 0;
|
||||||
@@ -12,6 +13,45 @@ enum KeyType {
|
|||||||
KEY_TYPE_RSA = 3;
|
KEY_TYPE_RSA = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- SDK client management ---
|
||||||
|
|
||||||
|
enum SdkClientError {
|
||||||
|
SDK_CLIENT_ERROR_UNSPECIFIED = 0;
|
||||||
|
SDK_CLIENT_ERROR_ALREADY_EXISTS = 1;
|
||||||
|
SDK_CLIENT_ERROR_NOT_FOUND = 2;
|
||||||
|
SDK_CLIENT_ERROR_HAS_RELATED_DATA = 3; // hard-delete blocked by FK (client has grants or transaction logs)
|
||||||
|
SDK_CLIENT_ERROR_INTERNAL = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SdkClientRevokeRequest {
|
||||||
|
int32 client_id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SdkClientEntry {
|
||||||
|
int32 id = 1;
|
||||||
|
bytes pubkey = 2;
|
||||||
|
arbiter.client.ClientInfo info = 3;
|
||||||
|
int32 created_at = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SdkClientList {
|
||||||
|
repeated SdkClientEntry clients = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SdkClientRevokeResponse {
|
||||||
|
oneof result {
|
||||||
|
google.protobuf.Empty ok = 1;
|
||||||
|
SdkClientError error = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message SdkClientListResponse {
|
||||||
|
oneof result {
|
||||||
|
SdkClientList clients = 1;
|
||||||
|
SdkClientError error = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
message AuthChallengeRequest {
|
message AuthChallengeRequest {
|
||||||
bytes pubkey = 1;
|
bytes pubkey = 1;
|
||||||
optional string bootstrap_token = 2;
|
optional string bootstrap_token = 2;
|
||||||
@@ -19,15 +59,23 @@ message AuthChallengeRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message AuthChallenge {
|
message AuthChallenge {
|
||||||
bytes pubkey = 1;
|
|
||||||
int32 nonce = 2;
|
int32 nonce = 2;
|
||||||
|
reserved 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message AuthChallengeSolution {
|
message AuthChallengeSolution {
|
||||||
bytes signature = 1;
|
bytes signature = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message AuthOk {}
|
enum AuthResult {
|
||||||
|
AUTH_RESULT_UNSPECIFIED = 0;
|
||||||
|
AUTH_RESULT_SUCCESS = 1;
|
||||||
|
AUTH_RESULT_INVALID_KEY = 2;
|
||||||
|
AUTH_RESULT_INVALID_SIGNATURE = 3;
|
||||||
|
AUTH_RESULT_BOOTSTRAP_REQUIRED = 4;
|
||||||
|
AUTH_RESULT_TOKEN_INVALID = 5;
|
||||||
|
AUTH_RESULT_INTERNAL = 6;
|
||||||
|
}
|
||||||
|
|
||||||
message UnsealStart {
|
message UnsealStart {
|
||||||
bytes client_pubkey = 1;
|
bytes client_pubkey = 1;
|
||||||
@@ -70,17 +118,44 @@ enum VaultState {
|
|||||||
VAULT_STATE_ERROR = 4;
|
VAULT_STATE_ERROR = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ClientConnectionRequest {
|
message SdkClientConnectionRequest {
|
||||||
|
bytes pubkey = 1;
|
||||||
|
arbiter.client.ClientInfo info = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SdkClientConnectionResponse {
|
||||||
|
bool approved = 1;
|
||||||
|
bytes pubkey = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SdkClientConnectionCancel {
|
||||||
bytes pubkey = 1;
|
bytes pubkey = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ClientConnectionResponse {
|
message WalletAccess {
|
||||||
bool approved = 1;
|
int32 wallet_id = 1;
|
||||||
|
int32 sdk_client_id = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ClientConnectionCancel {}
|
message SdkClientWalletAccess {
|
||||||
|
int32 id = 1;
|
||||||
|
WalletAccess access = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SdkClientGrantWalletAccess {
|
||||||
|
repeated WalletAccess accesses = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SdkClientRevokeWalletAccess {
|
||||||
|
repeated int32 accesses = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListWalletAccessResponse {
|
||||||
|
repeated SdkClientWalletAccess accesses = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message UserAgentRequest {
|
message UserAgentRequest {
|
||||||
|
int32 id = 16;
|
||||||
oneof payload {
|
oneof payload {
|
||||||
AuthChallengeRequest auth_challenge_request = 1;
|
AuthChallengeRequest auth_challenge_request = 1;
|
||||||
AuthChallengeSolution auth_challenge_solution = 2;
|
AuthChallengeSolution auth_challenge_solution = 2;
|
||||||
@@ -92,14 +167,20 @@ message UserAgentRequest {
|
|||||||
arbiter.evm.EvmGrantCreateRequest evm_grant_create = 8;
|
arbiter.evm.EvmGrantCreateRequest evm_grant_create = 8;
|
||||||
arbiter.evm.EvmGrantDeleteRequest evm_grant_delete = 9;
|
arbiter.evm.EvmGrantDeleteRequest evm_grant_delete = 9;
|
||||||
arbiter.evm.EvmGrantListRequest evm_grant_list = 10;
|
arbiter.evm.EvmGrantListRequest evm_grant_list = 10;
|
||||||
ClientConnectionResponse client_connection_response = 11;
|
SdkClientConnectionResponse sdk_client_connection_response = 11;
|
||||||
BootstrapEncryptedKey bootstrap_encrypted_key = 12;
|
SdkClientRevokeRequest sdk_client_revoke = 12;
|
||||||
|
google.protobuf.Empty sdk_client_list = 13;
|
||||||
|
BootstrapEncryptedKey bootstrap_encrypted_key = 14;
|
||||||
|
SdkClientGrantWalletAccess grant_wallet_access = 15;
|
||||||
|
SdkClientRevokeWalletAccess revoke_wallet_access = 17;
|
||||||
|
google.protobuf.Empty list_wallet_access = 18;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
message UserAgentResponse {
|
message UserAgentResponse {
|
||||||
|
optional int32 id = 16;
|
||||||
oneof payload {
|
oneof payload {
|
||||||
AuthChallenge auth_challenge = 1;
|
AuthChallenge auth_challenge = 1;
|
||||||
AuthOk auth_ok = 2;
|
AuthResult auth_result = 2;
|
||||||
UnsealStartResponse unseal_start_response = 3;
|
UnsealStartResponse unseal_start_response = 3;
|
||||||
UnsealResult unseal_result = 4;
|
UnsealResult unseal_result = 4;
|
||||||
VaultState vault_state = 5;
|
VaultState vault_state = 5;
|
||||||
@@ -108,8 +189,11 @@ message UserAgentResponse {
|
|||||||
arbiter.evm.EvmGrantCreateResponse evm_grant_create = 8;
|
arbiter.evm.EvmGrantCreateResponse evm_grant_create = 8;
|
||||||
arbiter.evm.EvmGrantDeleteResponse evm_grant_delete = 9;
|
arbiter.evm.EvmGrantDeleteResponse evm_grant_delete = 9;
|
||||||
arbiter.evm.EvmGrantListResponse evm_grant_list = 10;
|
arbiter.evm.EvmGrantListResponse evm_grant_list = 10;
|
||||||
ClientConnectionRequest client_connection_request = 11;
|
SdkClientConnectionRequest sdk_client_connection_request = 11;
|
||||||
ClientConnectionCancel client_connection_cancel = 12;
|
SdkClientConnectionCancel sdk_client_connection_cancel = 12;
|
||||||
BootstrapResult bootstrap_result = 13;
|
SdkClientRevokeResponse sdk_client_revoke_response = 13;
|
||||||
|
SdkClientListResponse sdk_client_list_response = 14;
|
||||||
|
BootstrapResult bootstrap_result = 15;
|
||||||
|
ListWalletAccessResponse list_wallet_access_response = 17;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
466
server/Cargo.lock
generated
466
server/Cargo.lock
generated
@@ -67,13 +67,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "alloy-chains"
|
name = "alloy-chains"
|
||||||
version = "0.2.31"
|
version = "0.2.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6d9d22005bf31b018f31ef9ecadb5d2c39cf4f6acc8db0456f72c815f3d7f757"
|
checksum = "9247f0a399ef71aeb68f497b2b8fb348014f742b50d3b83b1e00dfe1b7d64b3d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"alloy-primitives",
|
"alloy-primitives",
|
||||||
"num_enum",
|
"num_enum",
|
||||||
"strum",
|
"strum 0.27.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -100,7 +100,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_with",
|
"serde_with",
|
||||||
"thiserror",
|
"thiserror 2.0.18",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -136,7 +136,7 @@ dependencies = [
|
|||||||
"futures",
|
"futures",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror",
|
"thiserror 2.0.18",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -165,7 +165,7 @@ dependencies = [
|
|||||||
"itoa",
|
"itoa",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"winnow",
|
"winnow 0.7.15",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -178,7 +178,7 @@ dependencies = [
|
|||||||
"alloy-rlp",
|
"alloy-rlp",
|
||||||
"crc",
|
"crc",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror",
|
"thiserror 2.0.18",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -203,7 +203,7 @@ dependencies = [
|
|||||||
"alloy-rlp",
|
"alloy-rlp",
|
||||||
"borsh",
|
"borsh",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror",
|
"thiserror 2.0.18",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -239,7 +239,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_with",
|
"serde_with",
|
||||||
"sha2 0.10.9",
|
"sha2 0.10.9",
|
||||||
"thiserror",
|
"thiserror 2.0.18",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -280,7 +280,7 @@ dependencies = [
|
|||||||
"http",
|
"http",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror",
|
"thiserror 2.0.18",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -307,7 +307,7 @@ dependencies = [
|
|||||||
"futures-utils-wasm",
|
"futures-utils-wasm",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror",
|
"thiserror 2.0.18",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -382,7 +382,7 @@ dependencies = [
|
|||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
@@ -471,11 +471,11 @@ dependencies = [
|
|||||||
"alloy-rlp",
|
"alloy-rlp",
|
||||||
"alloy-serde",
|
"alloy-serde",
|
||||||
"alloy-sol-types",
|
"alloy-sol-types",
|
||||||
"itertools 0.14.0",
|
"itertools 0.13.0",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_with",
|
"serde_with",
|
||||||
"thiserror",
|
"thiserror 2.0.18",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -501,7 +501,7 @@ dependencies = [
|
|||||||
"either",
|
"either",
|
||||||
"elliptic-curve",
|
"elliptic-curve",
|
||||||
"k256",
|
"k256",
|
||||||
"thiserror",
|
"thiserror 2.0.18",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -517,7 +517,7 @@ dependencies = [
|
|||||||
"async-trait",
|
"async-trait",
|
||||||
"k256",
|
"k256",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"thiserror",
|
"thiserror 2.0.18",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -578,7 +578,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "a6df77fea9d6a2a75c0ef8d2acbdfd92286cc599983d3175ccdc170d3433d249"
|
checksum = "a6df77fea9d6a2a75c0ef8d2acbdfd92286cc599983d3175ccdc170d3433d249"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"winnow",
|
"winnow 0.7.15",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -608,7 +608,7 @@ dependencies = [
|
|||||||
"parking_lot",
|
"parking_lot",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower",
|
"tower",
|
||||||
"tracing",
|
"tracing",
|
||||||
@@ -624,7 +624,7 @@ checksum = "aa501ad58dd20acddbfebc65b52e60f05ebf97c52fa40d1b35e91f5e2da0ad0e"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"alloy-json-rpc",
|
"alloy-json-rpc",
|
||||||
"alloy-transport",
|
"alloy-transport",
|
||||||
"itertools 0.14.0",
|
"itertools 0.13.0",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tower",
|
"tower",
|
||||||
@@ -644,7 +644,7 @@ dependencies = [
|
|||||||
"nybbles",
|
"nybbles",
|
||||||
"serde",
|
"serde",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"thiserror",
|
"thiserror 2.0.18",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -669,6 +669,56 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstream"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"anstyle-parse",
|
||||||
|
"anstyle-query",
|
||||||
|
"anstyle-wincon",
|
||||||
|
"colorchoice",
|
||||||
|
"is_terminal_polyfill",
|
||||||
|
"utf8parse",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle"
|
||||||
|
version = "1.0.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-parse"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
|
||||||
|
dependencies = [
|
||||||
|
"utf8parse",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-query"
|
||||||
|
version = "1.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-wincon"
|
||||||
|
version = "3.0.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"once_cell_polyfill",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.102"
|
version = "1.0.102"
|
||||||
@@ -678,6 +728,19 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "arbiter-client"
|
name = "arbiter-client"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"alloy",
|
||||||
|
"arbiter-proto",
|
||||||
|
"async-trait",
|
||||||
|
"ed25519-dalek",
|
||||||
|
"http",
|
||||||
|
"rand 0.10.0",
|
||||||
|
"rustls-webpki",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"tokio",
|
||||||
|
"tokio-stream",
|
||||||
|
"tonic",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arbiter-proto"
|
name = "arbiter-proto"
|
||||||
@@ -691,12 +754,14 @@ dependencies = [
|
|||||||
"miette",
|
"miette",
|
||||||
"prost",
|
"prost",
|
||||||
"prost-types",
|
"prost-types",
|
||||||
|
"protoc-bin-vendored",
|
||||||
"rand 0.10.0",
|
"rand 0.10.0",
|
||||||
"rcgen",
|
"rcgen",
|
||||||
"rstest",
|
"rstest",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"thiserror",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-stream",
|
||||||
"tonic",
|
"tonic",
|
||||||
"tonic-prost",
|
"tonic-prost",
|
||||||
"tonic-prost-build",
|
"tonic-prost-build",
|
||||||
@@ -715,11 +780,13 @@ dependencies = [
|
|||||||
"async-trait",
|
"async-trait",
|
||||||
"chacha20poly1305",
|
"chacha20poly1305",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"clap",
|
||||||
"dashmap",
|
"dashmap",
|
||||||
"diesel",
|
"diesel",
|
||||||
"diesel-async",
|
"diesel-async",
|
||||||
"diesel_migrations",
|
"diesel_migrations",
|
||||||
"ed25519-dalek",
|
"ed25519-dalek",
|
||||||
|
"fatality",
|
||||||
"futures",
|
"futures",
|
||||||
"insta",
|
"insta",
|
||||||
"k256",
|
"k256",
|
||||||
@@ -737,14 +804,15 @@ dependencies = [
|
|||||||
"sha2 0.10.9",
|
"sha2 0.10.9",
|
||||||
"smlang",
|
"smlang",
|
||||||
"spki",
|
"spki",
|
||||||
"strum",
|
"strum 0.28.0",
|
||||||
"test-log",
|
"test-log",
|
||||||
"thiserror",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tonic",
|
"tonic",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
"windows-service",
|
||||||
"x25519-dalek",
|
"x25519-dalek",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
@@ -976,7 +1044,7 @@ dependencies = [
|
|||||||
"nom",
|
"nom",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"rusticata-macros",
|
"rusticata-macros",
|
||||||
"thiserror",
|
"thiserror 2.0.18",
|
||||||
"time",
|
"time",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1061,9 +1129,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aws-lc-rs"
|
name = "aws-lc-rs"
|
||||||
version = "1.16.1"
|
version = "1.16.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf"
|
checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-lc-sys",
|
"aws-lc-sys",
|
||||||
"untrusted 0.7.1",
|
"untrusted 0.7.1",
|
||||||
@@ -1072,9 +1140,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aws-lc-sys"
|
name = "aws-lc-sys"
|
||||||
version = "0.38.0"
|
version = "0.39.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e"
|
checksum = "1fa7e52a4c5c547c741610a2c6f123f3881e409b714cd27e6798ef020c514f0a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"cmake",
|
"cmake",
|
||||||
@@ -1269,19 +1337,20 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "borsh"
|
name = "borsh"
|
||||||
version = "1.6.0"
|
version = "1.6.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f"
|
checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"borsh-derive",
|
"borsh-derive",
|
||||||
|
"bytes",
|
||||||
"cfg_aliases",
|
"cfg_aliases",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "borsh-derive"
|
name = "borsh-derive"
|
||||||
version = "1.6.0"
|
version = "1.6.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c"
|
checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"proc-macro-crate",
|
"proc-macro-crate",
|
||||||
@@ -1416,6 +1485,46 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap"
|
||||||
|
version = "4.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351"
|
||||||
|
dependencies = [
|
||||||
|
"clap_builder",
|
||||||
|
"clap_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_builder"
|
||||||
|
version = "4.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
|
||||||
|
dependencies = [
|
||||||
|
"anstream",
|
||||||
|
"anstyle",
|
||||||
|
"clap_lex",
|
||||||
|
"strsim",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_derive"
|
||||||
|
version = "4.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_lex"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cmake"
|
name = "cmake"
|
||||||
version = "0.1.57"
|
version = "0.1.57"
|
||||||
@@ -1425,6 +1534,12 @@ dependencies = [
|
|||||||
"cc",
|
"cc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorchoice"
|
||||||
|
version = "1.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "console"
|
name = "console"
|
||||||
version = "0.15.11"
|
version = "0.15.11"
|
||||||
@@ -1795,15 +1910,16 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "diesel-async"
|
name = "diesel-async"
|
||||||
version = "0.7.4"
|
version = "0.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "13096fb8dae53f2d411c4b523bec85f45552ed3044a2ab4d85fb2092d9cb4f34"
|
checksum = "b95864e58597509106f1fddfe0600de7e589e1fddddd87f54eee0a49fd111bbc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bb8",
|
"bb8",
|
||||||
"diesel",
|
"diesel",
|
||||||
"diesel_migrations",
|
"diesel_migrations",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
"pin-project-lite",
|
||||||
"scoped-futures",
|
"scoped-futures",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
@@ -2036,6 +2152,21 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "expander"
|
||||||
|
version = "2.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e2c470c71d91ecbd179935b24170459e926382eaaa86b590b78814e180d8a8e2"
|
||||||
|
dependencies = [
|
||||||
|
"blake2",
|
||||||
|
"file-guard",
|
||||||
|
"fs-err",
|
||||||
|
"prettyplease",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastrand"
|
name = "fastrand"
|
||||||
version = "2.3.0"
|
version = "2.3.0"
|
||||||
@@ -2064,6 +2195,30 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fatality"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ec6f82451ff7f0568c6181287189126d492b5654e30a788add08027b6363d019"
|
||||||
|
dependencies = [
|
||||||
|
"fatality-proc-macro",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fatality-proc-macro"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eb42427514b063d97ce21d5199f36c0c307d981434a6be32582bc79fe5bd2303"
|
||||||
|
dependencies = [
|
||||||
|
"expander",
|
||||||
|
"indexmap 2.13.0",
|
||||||
|
"proc-macro-crate",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ff"
|
name = "ff"
|
||||||
version = "0.13.1"
|
version = "0.13.1"
|
||||||
@@ -2086,6 +2241,16 @@ version = "0.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24"
|
checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "file-guard"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "21ef72acf95ec3d7dbf61275be556299490a245f017cf084bd23b4f68cf9407c"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "find-msvc-tools"
|
name = "find-msvc-tools"
|
||||||
version = "0.1.9"
|
version = "0.1.9"
|
||||||
@@ -2147,6 +2312,15 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fs-err"
|
||||||
|
version = "2.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fs_extra"
|
name = "fs_extra"
|
||||||
version = "1.3.0"
|
version = "1.3.0"
|
||||||
@@ -2777,6 +2951,12 @@ version = "1.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45"
|
checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is_terminal_polyfill"
|
||||||
|
version = "1.70.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itertools"
|
name = "itertools"
|
||||||
version = "0.10.5"
|
version = "0.10.5"
|
||||||
@@ -2795,20 +2975,11 @@ dependencies = [
|
|||||||
"either",
|
"either",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "itertools"
|
|
||||||
version = "0.14.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
|
|
||||||
dependencies = [
|
|
||||||
"either",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.17"
|
version = "1.0.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jobserver"
|
name = "jobserver"
|
||||||
@@ -3196,9 +3367,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num_enum"
|
name = "num_enum"
|
||||||
version = "0.7.5"
|
version = "0.7.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c"
|
checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"num_enum_derive",
|
"num_enum_derive",
|
||||||
"rustversion",
|
"rustversion",
|
||||||
@@ -3206,9 +3377,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num_enum_derive"
|
name = "num_enum_derive"
|
||||||
version = "0.7.5"
|
version = "0.7.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7"
|
checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -3253,6 +3424,12 @@ version = "1.21.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell_polyfill"
|
||||||
|
version = "1.70.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "opaque-debug"
|
name = "opaque-debug"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@@ -3600,7 +3777,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7"
|
checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
"itertools 0.14.0",
|
"itertools 0.13.0",
|
||||||
"log",
|
"log",
|
||||||
"multimap",
|
"multimap",
|
||||||
"petgraph",
|
"petgraph",
|
||||||
@@ -3621,7 +3798,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b"
|
checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"itertools 0.14.0",
|
"itertools 0.13.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
@@ -3638,10 +3815,74 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pulldown-cmark"
|
name = "protoc-bin-vendored"
|
||||||
version = "0.13.1"
|
version = "3.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "83c41efbf8f90ac44de7f3a868f0867851d261b56291732d0cbf7cceaaeb55a6"
|
checksum = "d1c381df33c98266b5f08186583660090a4ffa0889e76c7e9a5e175f645a67fa"
|
||||||
|
dependencies = [
|
||||||
|
"protoc-bin-vendored-linux-aarch_64",
|
||||||
|
"protoc-bin-vendored-linux-ppcle_64",
|
||||||
|
"protoc-bin-vendored-linux-s390_64",
|
||||||
|
"protoc-bin-vendored-linux-x86_32",
|
||||||
|
"protoc-bin-vendored-linux-x86_64",
|
||||||
|
"protoc-bin-vendored-macos-aarch_64",
|
||||||
|
"protoc-bin-vendored-macos-x86_64",
|
||||||
|
"protoc-bin-vendored-win32",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "protoc-bin-vendored-linux-aarch_64"
|
||||||
|
version = "3.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c350df4d49b5b9e3ca79f7e646fde2377b199e13cfa87320308397e1f37e1a4c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "protoc-bin-vendored-linux-ppcle_64"
|
||||||
|
version = "3.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a55a63e6c7244f19b5c6393f025017eb5d793fd5467823a099740a7a4222440c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "protoc-bin-vendored-linux-s390_64"
|
||||||
|
version = "3.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1dba5565db4288e935d5330a07c264a4ee8e4a5b4a4e6f4e83fad824cc32f3b0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "protoc-bin-vendored-linux-x86_32"
|
||||||
|
version = "3.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8854774b24ee28b7868cd71dccaae8e02a2365e67a4a87a6cd11ee6cdbdf9cf5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "protoc-bin-vendored-linux-x86_64"
|
||||||
|
version = "3.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b38b07546580df720fa464ce124c4b03630a6fb83e05c336fea2a241df7e5d78"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "protoc-bin-vendored-macos-aarch_64"
|
||||||
|
version = "3.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "89278a9926ce312e51f1d999fee8825d324d603213344a9a706daa009f1d8092"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "protoc-bin-vendored-macos-x86_64"
|
||||||
|
version = "3.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "81745feda7ccfb9471d7a4de888f0652e806d5795b61480605d4943176299756"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "protoc-bin-vendored-win32"
|
||||||
|
version = "3.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "95067976aca6421a523e491fce939a3e65249bac4b977adee0ee9771568e8aa3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pulldown-cmark"
|
||||||
|
version = "0.13.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "14104c5a24d9bcf7eb2c24753e0f49fe14555d8bd565ea3d38e4b4303267259d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"memchr",
|
"memchr",
|
||||||
@@ -3677,7 +3918,7 @@ dependencies = [
|
|||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
"rustls",
|
"rustls",
|
||||||
"socket2",
|
"socket2",
|
||||||
"thiserror",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"web-time",
|
"web-time",
|
||||||
@@ -3698,7 +3939,7 @@ dependencies = [
|
|||||||
"rustls",
|
"rustls",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"slab",
|
"slab",
|
||||||
"thiserror",
|
"thiserror 2.0.18",
|
||||||
"tinyvec",
|
"tinyvec",
|
||||||
"tracing",
|
"tracing",
|
||||||
"web-time",
|
"web-time",
|
||||||
@@ -4033,7 +4274,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "a8a1f2315036ef6b1fbacd1972e8ee7688030b0a2121edfc2a6550febd41574d"
|
checksum = "a8a1f2315036ef6b1fbacd1972e8ee7688030b0a2121edfc2a6550febd41574d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hashbrown 0.16.1",
|
"hashbrown 0.16.1",
|
||||||
"thiserror",
|
"thiserror 2.0.18",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4185,9 +4426,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-webpki"
|
name = "rustls-webpki"
|
||||||
version = "0.103.9"
|
version = "0.103.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53"
|
checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-lc-rs",
|
"aws-lc-rs",
|
||||||
"ring",
|
"ring",
|
||||||
@@ -4631,7 +4872,16 @@ version = "0.27.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
|
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"strum_macros",
|
"strum_macros 0.27.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum"
|
||||||
|
version = "0.28.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd"
|
||||||
|
dependencies = [
|
||||||
|
"strum_macros 0.28.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4646,6 +4896,18 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum_macros"
|
||||||
|
version = "0.28.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "subtle"
|
name = "subtle"
|
||||||
version = "2.6.1"
|
version = "2.6.1"
|
||||||
@@ -4787,13 +5049,33 @@ dependencies = [
|
|||||||
"unicode-width 0.2.2",
|
"unicode-width 0.2.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "1.0.69"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl 1.0.69",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "2.0.18"
|
version = "2.0.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl",
|
"thiserror-impl 2.0.18",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "1.0.69"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4955,7 +5237,7 @@ dependencies = [
|
|||||||
"serde_spanned",
|
"serde_spanned",
|
||||||
"toml_datetime 0.7.5+spec-1.1.0",
|
"toml_datetime 0.7.5+spec-1.1.0",
|
||||||
"toml_parser",
|
"toml_parser",
|
||||||
"winnow",
|
"winnow 0.7.15",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4969,32 +5251,32 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_datetime"
|
name = "toml_datetime"
|
||||||
version = "1.0.0+spec-1.1.0"
|
version = "1.0.1+spec-1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e"
|
checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_edit"
|
name = "toml_edit"
|
||||||
version = "0.25.4+spec-1.1.0"
|
version = "0.25.5+spec-1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2"
|
checksum = "8ca1a40644a28bce036923f6a431df0b34236949d111cc07cb6dca830c9ef2e1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap 2.13.0",
|
"indexmap 2.13.0",
|
||||||
"toml_datetime 1.0.0+spec-1.1.0",
|
"toml_datetime 1.0.1+spec-1.1.0",
|
||||||
"toml_parser",
|
"toml_parser",
|
||||||
"winnow",
|
"winnow 1.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_parser"
|
name = "toml_parser"
|
||||||
version = "1.0.9+spec-1.1.0"
|
version = "1.0.10+spec-1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4"
|
checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"winnow",
|
"winnow 1.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5297,6 +5579,12 @@ version = "1.0.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf8parse"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.22.0"
|
version = "1.22.0"
|
||||||
@@ -5503,6 +5791,12 @@ dependencies = [
|
|||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "widestring"
|
||||||
|
version = "1.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi"
|
name = "winapi"
|
||||||
version = "0.3.9"
|
version = "0.3.9"
|
||||||
@@ -5575,6 +5869,17 @@ dependencies = [
|
|||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-service"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "193cae8e647981c35bc947fdd57ba7928b1fa0d4a79305f6dd2dc55221ac35ac"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"widestring",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-strings"
|
name = "windows-strings"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
@@ -5758,6 +6063,15 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wit-bindgen"
|
name = "wit-bindgen"
|
||||||
version = "0.51.0"
|
version = "0.51.0"
|
||||||
@@ -5887,7 +6201,7 @@ dependencies = [
|
|||||||
"nom",
|
"nom",
|
||||||
"oid-registry",
|
"oid-registry",
|
||||||
"rusticata-macros",
|
"rusticata-macros",
|
||||||
"thiserror",
|
"thiserror 2.0.18",
|
||||||
"time",
|
"time",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -5925,18 +6239,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy"
|
name = "zerocopy"
|
||||||
version = "0.8.42"
|
version = "0.8.47"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3"
|
checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zerocopy-derive",
|
"zerocopy-derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy-derive"
|
name = "zerocopy-derive"
|
||||||
version = "0.8.42"
|
version = "0.8.47"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f"
|
checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|||||||
@@ -9,23 +9,23 @@ disallowed-methods = "deny"
|
|||||||
|
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
tonic = { version = "0.14.3", features = [
|
tonic = { version = "0.14.5", features = [
|
||||||
"deflate",
|
"deflate",
|
||||||
"gzip",
|
"gzip",
|
||||||
"tls-connect-info",
|
"tls-connect-info",
|
||||||
"zstd",
|
"zstd",
|
||||||
] }
|
] }
|
||||||
tracing = "0.1.44"
|
tracing = "0.1.44"
|
||||||
tokio = { version = "1.49.0", features = ["full"] }
|
tokio = { version = "1.50.0", features = ["full"] }
|
||||||
ed25519-dalek = { version = "3.0.0-pre.6", features = ["rand_core"] }
|
ed25519-dalek = { version = "3.0.0-pre.6", features = ["rand_core"] }
|
||||||
chrono = { version = "0.4.43", features = ["serde"] }
|
chrono = { version = "0.4.44", features = ["serde"] }
|
||||||
rand = "0.10.0"
|
rand = "0.10.0"
|
||||||
rustls = { version = "0.23.36", features = ["aws-lc-rs"] }
|
rustls = { version = "0.23.37", features = ["aws-lc-rs"] }
|
||||||
smlang = "0.8.0"
|
smlang = "0.8.0"
|
||||||
miette = { version = "7.6.0", features = ["fancy", "serde"] }
|
miette = { version = "7.6.0", features = ["fancy", "serde"] }
|
||||||
thiserror = "2.0.18"
|
thiserror = "2.0.18"
|
||||||
async-trait = "0.1.89"
|
async-trait = "0.1.89"
|
||||||
futures = "0.3.31"
|
futures = "0.3.32"
|
||||||
tokio-stream = { version = "0.1.18", features = ["full"] }
|
tokio-stream = { version = "0.1.18", features = ["full"] }
|
||||||
kameo = "0.19.2"
|
kameo = "0.19.2"
|
||||||
prost-types = { version = "0.14.3", features = ["chrono"] }
|
prost-types = { version = "0.14.3", features = ["chrono"] }
|
||||||
|
|||||||
@@ -5,4 +5,22 @@ edition = "2024"
|
|||||||
repository = "https://git.markettakers.org/MarketTakers/arbiter"
|
repository = "https://git.markettakers.org/MarketTakers/arbiter"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[features]
|
||||||
|
evm = ["dep:alloy"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
arbiter-proto.path = "../arbiter-proto"
|
||||||
|
alloy = { workspace = true, optional = true }
|
||||||
|
tonic.workspace = true
|
||||||
|
tonic.features = ["tls-aws-lc"]
|
||||||
|
tokio.workspace = true
|
||||||
|
tokio-stream.workspace = true
|
||||||
|
ed25519-dalek.workspace = true
|
||||||
|
thiserror.workspace = true
|
||||||
|
http = "1.4.0"
|
||||||
|
rustls-webpki = { version = "0.103.10", features = ["aws-lc-rs"] }
|
||||||
|
async-trait.workspace = true
|
||||||
|
rand.workspace = true
|
||||||
|
|||||||
135
server/crates/arbiter-client/src/auth.rs
Normal file
135
server/crates/arbiter-client/src/auth.rs
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
use arbiter_proto::{
|
||||||
|
ClientMetadata, format_challenge,
|
||||||
|
proto::client::{
|
||||||
|
AuthChallengeRequest, AuthChallengeSolution, AuthResult, ClientInfo as ProtoClientInfo,
|
||||||
|
ClientRequest, client_request::Payload as ClientRequestPayload,
|
||||||
|
client_response::Payload as ClientResponsePayload,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use ed25519_dalek::Signer as _;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
storage::StorageError,
|
||||||
|
transport::{ClientTransport, next_request_id},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum AuthError {
|
||||||
|
#[error("Auth challenge was not returned by server")]
|
||||||
|
MissingAuthChallenge,
|
||||||
|
|
||||||
|
#[error("Client approval denied by User Agent")]
|
||||||
|
ApprovalDenied,
|
||||||
|
|
||||||
|
#[error("No User Agents online to approve client")]
|
||||||
|
NoUserAgentsOnline,
|
||||||
|
|
||||||
|
#[error("Unexpected auth response payload")]
|
||||||
|
UnexpectedAuthResponse,
|
||||||
|
|
||||||
|
#[error("Signing key storage error")]
|
||||||
|
Storage(#[from] StorageError),
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_auth_result(code: i32) -> AuthError {
|
||||||
|
match AuthResult::try_from(code).unwrap_or(AuthResult::Unspecified) {
|
||||||
|
AuthResult::ApprovalDenied => AuthError::ApprovalDenied,
|
||||||
|
AuthResult::NoUserAgentsOnline => AuthError::NoUserAgentsOnline,
|
||||||
|
AuthResult::Unspecified
|
||||||
|
| AuthResult::Success
|
||||||
|
| AuthResult::InvalidKey
|
||||||
|
| AuthResult::InvalidSignature
|
||||||
|
| AuthResult::Internal => AuthError::UnexpectedAuthResponse,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_auth_challenge_request(
|
||||||
|
transport: &mut ClientTransport,
|
||||||
|
metadata: ClientMetadata,
|
||||||
|
key: &ed25519_dalek::SigningKey,
|
||||||
|
) -> std::result::Result<(), AuthError> {
|
||||||
|
transport
|
||||||
|
.send(ClientRequest {
|
||||||
|
request_id: next_request_id(),
|
||||||
|
payload: Some(ClientRequestPayload::AuthChallengeRequest(
|
||||||
|
AuthChallengeRequest {
|
||||||
|
pubkey: key.verifying_key().to_bytes().to_vec(),
|
||||||
|
client_info: Some(ProtoClientInfo {
|
||||||
|
name: metadata.name,
|
||||||
|
description: metadata.description,
|
||||||
|
version: metadata.version,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|_| AuthError::UnexpectedAuthResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn receive_auth_challenge(
|
||||||
|
transport: &mut ClientTransport,
|
||||||
|
) -> std::result::Result<arbiter_proto::proto::client::AuthChallenge, AuthError> {
|
||||||
|
let response = transport
|
||||||
|
.recv()
|
||||||
|
.await
|
||||||
|
.map_err(|_| AuthError::MissingAuthChallenge)?;
|
||||||
|
|
||||||
|
let payload = response.payload.ok_or(AuthError::MissingAuthChallenge)?;
|
||||||
|
match payload {
|
||||||
|
ClientResponsePayload::AuthChallenge(challenge) => Ok(challenge),
|
||||||
|
ClientResponsePayload::AuthResult(result) => Err(map_auth_result(result)),
|
||||||
|
_ => Err(AuthError::UnexpectedAuthResponse),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_auth_challenge_solution(
|
||||||
|
transport: &mut ClientTransport,
|
||||||
|
key: &ed25519_dalek::SigningKey,
|
||||||
|
challenge: arbiter_proto::proto::client::AuthChallenge,
|
||||||
|
) -> std::result::Result<(), AuthError> {
|
||||||
|
let challenge_payload = format_challenge(challenge.nonce, &challenge.pubkey);
|
||||||
|
let signature = key.sign(&challenge_payload).to_bytes().to_vec();
|
||||||
|
|
||||||
|
transport
|
||||||
|
.send(ClientRequest {
|
||||||
|
request_id: next_request_id(),
|
||||||
|
payload: Some(ClientRequestPayload::AuthChallengeSolution(
|
||||||
|
AuthChallengeSolution { signature },
|
||||||
|
)),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|_| AuthError::UnexpectedAuthResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn receive_auth_confirmation(
|
||||||
|
transport: &mut ClientTransport,
|
||||||
|
) -> std::result::Result<(), AuthError> {
|
||||||
|
let response = transport
|
||||||
|
.recv()
|
||||||
|
.await
|
||||||
|
.map_err(|_| AuthError::UnexpectedAuthResponse)?;
|
||||||
|
|
||||||
|
let payload = response
|
||||||
|
.payload
|
||||||
|
.ok_or(AuthError::UnexpectedAuthResponse)?;
|
||||||
|
match payload {
|
||||||
|
ClientResponsePayload::AuthResult(result)
|
||||||
|
if AuthResult::try_from(result).ok() == Some(AuthResult::Success) =>
|
||||||
|
{
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
ClientResponsePayload::AuthResult(result) => Err(map_auth_result(result)),
|
||||||
|
_ => Err(AuthError::UnexpectedAuthResponse),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn authenticate(
|
||||||
|
transport: &mut ClientTransport,
|
||||||
|
metadata: ClientMetadata,
|
||||||
|
key: &ed25519_dalek::SigningKey,
|
||||||
|
) -> std::result::Result<(), AuthError> {
|
||||||
|
send_auth_challenge_request(transport, metadata, key).await?;
|
||||||
|
let challenge = receive_auth_challenge(transport).await?;
|
||||||
|
send_auth_challenge_solution(transport, key, challenge).await?;
|
||||||
|
receive_auth_confirmation(transport).await
|
||||||
|
}
|
||||||
48
server/crates/arbiter-client/src/bin/test_connect.rs
Normal file
48
server/crates/arbiter-client/src/bin/test_connect.rs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
|
||||||
|
use std::io::{self, Write};
|
||||||
|
|
||||||
|
use arbiter_client::ArbiterClient;
|
||||||
|
use arbiter_proto::{ClientMetadata, url::ArbiterUrl};
|
||||||
|
use tonic::ConnectError;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
println!("Testing connection to Arbiter server...");
|
||||||
|
print!("Enter ArbiterUrl: ");
|
||||||
|
let _ = io::stdout().flush();
|
||||||
|
|
||||||
|
let mut input = String::new();
|
||||||
|
if let Err(err) = io::stdin().read_line(&mut input) {
|
||||||
|
eprintln!("Failed to read input: {err}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let input = input.trim();
|
||||||
|
if input.is_empty() {
|
||||||
|
eprintln!("ArbiterUrl cannot be empty");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let url = match ArbiterUrl::try_from(input) {
|
||||||
|
Ok(url) => url,
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("Invalid ArbiterUrl: {err}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("{:#?}", url);
|
||||||
|
|
||||||
|
let metadata = ClientMetadata {
|
||||||
|
name: "arbiter-client test_connect".to_string(),
|
||||||
|
description: Some("Manual connection smoke test".to_string()),
|
||||||
|
version: Some(env!("CARGO_PKG_VERSION").to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
match ArbiterClient::connect(url, metadata).await {
|
||||||
|
Ok(_) => println!("Connected and authenticated successfully."),
|
||||||
|
Err(err) => eprintln!("Failed to connect: {:#?}", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
89
server/crates/arbiter-client/src/client.rs
Normal file
89
server/crates/arbiter-client/src/client.rs
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
use arbiter_proto::{ClientMetadata, proto::arbiter_service_client::ArbiterServiceClient, url::ArbiterUrl};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::{Mutex, mpsc};
|
||||||
|
use tokio_stream::wrappers::ReceiverStream;
|
||||||
|
use tonic::transport::ClientTlsConfig;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
StorageError, auth::{AuthError, authenticate}, storage::{FileSigningKeyStorage, SigningKeyStorage}, transport::{BUFFER_LENGTH, ClientTransport}
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "evm")]
|
||||||
|
use crate::wallets::evm::ArbiterEvmWallet;
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("gRPC error")]
|
||||||
|
Grpc(#[from] tonic::Status),
|
||||||
|
|
||||||
|
#[error("Could not establish connection")]
|
||||||
|
Connection(#[from] tonic::transport::Error),
|
||||||
|
|
||||||
|
#[error("Invalid server URI")]
|
||||||
|
InvalidUri(#[from] http::uri::InvalidUri),
|
||||||
|
|
||||||
|
#[error("Invalid CA certificate")]
|
||||||
|
InvalidCaCert(#[from] webpki::Error),
|
||||||
|
|
||||||
|
#[error("Authentication error")]
|
||||||
|
Authentication(#[from] AuthError),
|
||||||
|
|
||||||
|
#[error("Storage error")]
|
||||||
|
Storage(#[from] StorageError),
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ArbiterClient {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
transport: Arc<Mutex<ClientTransport>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ArbiterClient {
|
||||||
|
pub async fn connect(url: ArbiterUrl, metadata: ClientMetadata) -> Result<Self, Error> {
|
||||||
|
let storage = FileSigningKeyStorage::from_default_location()?;
|
||||||
|
Self::connect_with_storage(url, metadata, &storage).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn connect_with_storage<S: SigningKeyStorage>(
|
||||||
|
url: ArbiterUrl,
|
||||||
|
metadata: ClientMetadata,
|
||||||
|
storage: &S,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
|
let key = storage.load_or_create()?;
|
||||||
|
Self::connect_with_key(url, metadata, key).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn connect_with_key(
|
||||||
|
url: ArbiterUrl,
|
||||||
|
metadata: ClientMetadata,
|
||||||
|
key: ed25519_dalek::SigningKey,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
|
let anchor = webpki::anchor_from_trusted_cert(&url.ca_cert)?.to_owned();
|
||||||
|
let tls = ClientTlsConfig::new().trust_anchor(anchor);
|
||||||
|
|
||||||
|
let channel = tonic::transport::Channel::from_shared(format!("https://{}:{}", url.host, url.port))?
|
||||||
|
.tls_config(tls)?
|
||||||
|
.connect()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut client = ArbiterServiceClient::new(channel);
|
||||||
|
let (tx, rx) = mpsc::channel(BUFFER_LENGTH);
|
||||||
|
let response_stream = client.client(ReceiverStream::new(rx)).await?.into_inner();
|
||||||
|
|
||||||
|
let mut transport = ClientTransport {
|
||||||
|
sender: tx,
|
||||||
|
receiver: response_stream,
|
||||||
|
};
|
||||||
|
|
||||||
|
authenticate(&mut transport, metadata, &key).await?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
transport: Arc::new(Mutex::new(transport)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "evm")]
|
||||||
|
pub async fn evm_wallets(&self) -> Result<Vec<ArbiterEvmWallet>, Error> {
|
||||||
|
todo!("fetch EVM wallet list from server")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,12 @@
|
|||||||
pub fn add(left: u64, right: u64) -> u64 {
|
mod auth;
|
||||||
left + right
|
mod client;
|
||||||
}
|
mod storage;
|
||||||
|
mod transport;
|
||||||
|
pub mod wallets;
|
||||||
|
|
||||||
#[cfg(test)]
|
pub use auth::AuthError;
|
||||||
mod tests {
|
pub use client::{ArbiterClient, Error};
|
||||||
use super::*;
|
pub use storage::{FileSigningKeyStorage, SigningKeyStorage, StorageError};
|
||||||
|
|
||||||
#[test]
|
#[cfg(feature = "evm")]
|
||||||
fn it_works() {
|
pub use wallets::evm::ArbiterEvmWallet;
|
||||||
let result = add(2, 2);
|
|
||||||
assert_eq!(result, 4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
132
server/crates/arbiter-client/src/storage.rs
Normal file
132
server/crates/arbiter-client/src/storage.rs
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
use arbiter_proto::home_path;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum StorageError {
|
||||||
|
#[error("I/O error")]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
|
||||||
|
#[error("Invalid signing key length in storage: expected {expected} bytes, got {actual} bytes")]
|
||||||
|
InvalidKeyLength { expected: usize, actual: usize },
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait SigningKeyStorage {
|
||||||
|
fn load_or_create(&self) -> std::result::Result<ed25519_dalek::SigningKey, StorageError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct FileSigningKeyStorage {
|
||||||
|
path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileSigningKeyStorage {
|
||||||
|
pub const DEFAULT_FILE_NAME: &str = "sdk_client_ed25519.key";
|
||||||
|
|
||||||
|
pub fn new(path: impl Into<PathBuf>) -> Self {
|
||||||
|
Self { path: path.into() }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_default_location() -> std::result::Result<Self, StorageError> {
|
||||||
|
Ok(Self::new(home_path()?.join(Self::DEFAULT_FILE_NAME)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_key(path: &Path) -> std::result::Result<ed25519_dalek::SigningKey, StorageError> {
|
||||||
|
let bytes = std::fs::read(path)?;
|
||||||
|
let raw: [u8; 32] =
|
||||||
|
bytes
|
||||||
|
.try_into()
|
||||||
|
.map_err(|v: Vec<u8>| StorageError::InvalidKeyLength {
|
||||||
|
expected: 32,
|
||||||
|
actual: v.len(),
|
||||||
|
})?;
|
||||||
|
Ok(ed25519_dalek::SigningKey::from_bytes(&raw))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SigningKeyStorage for FileSigningKeyStorage {
|
||||||
|
fn load_or_create(&self) -> std::result::Result<ed25519_dalek::SigningKey, StorageError> {
|
||||||
|
if let Some(parent) = self.path.parent() {
|
||||||
|
std::fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.path.exists() {
|
||||||
|
return Self::read_key(&self.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
||||||
|
let raw_key = key.to_bytes();
|
||||||
|
|
||||||
|
// Use create_new to prevent accidental overwrite if another process creates the key first.
|
||||||
|
match std::fs::OpenOptions::new()
|
||||||
|
.create_new(true)
|
||||||
|
.write(true)
|
||||||
|
.open(&self.path)
|
||||||
|
{
|
||||||
|
Ok(mut file) => {
|
||||||
|
use std::io::Write as _;
|
||||||
|
file.write_all(&raw_key)?;
|
||||||
|
Ok(key)
|
||||||
|
}
|
||||||
|
Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
|
||||||
|
Self::read_key(&self.path)
|
||||||
|
}
|
||||||
|
Err(err) => Err(StorageError::Io(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{FileSigningKeyStorage, SigningKeyStorage, StorageError};
|
||||||
|
|
||||||
|
fn unique_temp_key_path() -> std::path::PathBuf {
|
||||||
|
let nanos = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.expect("clock should be after unix epoch")
|
||||||
|
.as_nanos();
|
||||||
|
std::env::temp_dir().join(format!(
|
||||||
|
"arbiter-client-key-{}-{}.bin",
|
||||||
|
std::process::id(),
|
||||||
|
nanos
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn file_storage_creates_and_reuses_key() {
|
||||||
|
let path = unique_temp_key_path();
|
||||||
|
let storage = FileSigningKeyStorage::new(path.clone());
|
||||||
|
|
||||||
|
let key_a = storage
|
||||||
|
.load_or_create()
|
||||||
|
.expect("first load_or_create should create key");
|
||||||
|
let key_b = storage
|
||||||
|
.load_or_create()
|
||||||
|
.expect("second load_or_create should read same key");
|
||||||
|
|
||||||
|
assert_eq!(key_a.to_bytes(), key_b.to_bytes());
|
||||||
|
assert!(path.exists());
|
||||||
|
|
||||||
|
std::fs::remove_file(path).expect("temp key file should be removable");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn file_storage_rejects_invalid_key_length() {
|
||||||
|
let path = unique_temp_key_path();
|
||||||
|
std::fs::write(&path, [42u8; 31]).expect("should write invalid key file");
|
||||||
|
let storage = FileSigningKeyStorage::new(path.clone());
|
||||||
|
|
||||||
|
let err = storage
|
||||||
|
.load_or_create()
|
||||||
|
.expect_err("storage should reject non-32-byte key file");
|
||||||
|
|
||||||
|
match err {
|
||||||
|
StorageError::InvalidKeyLength { expected, actual } => {
|
||||||
|
assert_eq!(expected, 32);
|
||||||
|
assert_eq!(actual, 31);
|
||||||
|
}
|
||||||
|
other => panic!("unexpected error: {other:?}"),
|
||||||
|
}
|
||||||
|
|
||||||
|
std::fs::remove_file(path).expect("temp key file should be removable");
|
||||||
|
}
|
||||||
|
}
|
||||||
48
server/crates/arbiter-client/src/transport.rs
Normal file
48
server/crates/arbiter-client/src/transport.rs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
use arbiter_proto::proto::{
|
||||||
|
client::{ClientRequest, ClientResponse},
|
||||||
|
};
|
||||||
|
use std::sync::atomic::{AtomicI32, Ordering};
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
|
pub(crate) const BUFFER_LENGTH: usize = 16;
|
||||||
|
static NEXT_REQUEST_ID: AtomicI32 = AtomicI32::new(1);
|
||||||
|
|
||||||
|
pub(crate) fn next_request_id() -> i32 {
|
||||||
|
NEXT_REQUEST_ID.fetch_add(1, Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub(crate) enum ClientSignError {
|
||||||
|
#[error("Transport channel closed")]
|
||||||
|
ChannelClosed,
|
||||||
|
|
||||||
|
#[error("Connection closed by server")]
|
||||||
|
ConnectionClosed,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct ClientTransport {
|
||||||
|
pub(crate) sender: mpsc::Sender<ClientRequest>,
|
||||||
|
pub(crate) receiver: tonic::Streaming<ClientResponse>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClientTransport {
|
||||||
|
pub(crate) async fn send(
|
||||||
|
&mut self,
|
||||||
|
request: ClientRequest,
|
||||||
|
) -> std::result::Result<(), ClientSignError> {
|
||||||
|
self.sender
|
||||||
|
.send(request)
|
||||||
|
.await
|
||||||
|
.map_err(|_| ClientSignError::ChannelClosed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn recv(
|
||||||
|
&mut self,
|
||||||
|
) -> std::result::Result<ClientResponse, ClientSignError> {
|
||||||
|
match self.receiver.message().await {
|
||||||
|
Ok(Some(resp)) => Ok(resp),
|
||||||
|
Ok(None) => Err(ClientSignError::ConnectionClosed),
|
||||||
|
Err(_) => Err(ClientSignError::ConnectionClosed),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
89
server/crates/arbiter-client/src/wallets/evm.rs
Normal file
89
server/crates/arbiter-client/src/wallets/evm.rs
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
use alloy::{
|
||||||
|
consensus::SignableTransaction,
|
||||||
|
network::TxSigner,
|
||||||
|
primitives::{Address, B256, ChainId, Signature},
|
||||||
|
signers::{Error, Result, Signer},
|
||||||
|
};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use crate::transport::ClientTransport;
|
||||||
|
|
||||||
|
pub struct ArbiterEvmWallet {
|
||||||
|
transport: Arc<Mutex<ClientTransport>>,
|
||||||
|
address: Address,
|
||||||
|
chain_id: Option<ChainId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ArbiterEvmWallet {
|
||||||
|
pub(crate) fn new(transport: Arc<Mutex<ClientTransport>>, address: Address) -> Self {
|
||||||
|
Self {
|
||||||
|
transport,
|
||||||
|
address,
|
||||||
|
chain_id: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn address(&self) -> Address {
|
||||||
|
self.address
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_chain_id(mut self, chain_id: ChainId) -> Self {
|
||||||
|
self.chain_id = Some(chain_id);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_chain_id(&self, tx: &mut dyn SignableTransaction<Signature>) -> Result<()> {
|
||||||
|
if let Some(chain_id) = self.chain_id
|
||||||
|
&& !tx.set_chain_id_checked(chain_id)
|
||||||
|
{
|
||||||
|
return Err(Error::TransactionChainIdMismatch {
|
||||||
|
signer: chain_id,
|
||||||
|
tx: tx.chain_id().unwrap(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Signer for ArbiterEvmWallet {
|
||||||
|
async fn sign_hash(&self, _hash: &B256) -> Result<Signature> {
|
||||||
|
Err(Error::other(
|
||||||
|
"hash-only signing is not supported for ArbiterEvmWallet; use transaction signing",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn address(&self) -> Address {
|
||||||
|
self.address
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chain_id(&self) -> Option<ChainId> {
|
||||||
|
self.chain_id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_chain_id(&mut self, chain_id: Option<ChainId>) {
|
||||||
|
self.chain_id = chain_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl TxSigner<Signature> for ArbiterEvmWallet {
|
||||||
|
fn address(&self) -> Address {
|
||||||
|
self.address
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn sign_transaction(
|
||||||
|
&self,
|
||||||
|
tx: &mut dyn SignableTransaction<Signature>,
|
||||||
|
) -> Result<Signature> {
|
||||||
|
let _transport = self.transport.lock().await;
|
||||||
|
self.validate_chain_id(tx)?;
|
||||||
|
|
||||||
|
Err(Error::other(
|
||||||
|
"transaction signing is not supported by current arbiter.client protocol",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
2
server/crates/arbiter-client/src/wallets/mod.rs
Normal file
2
server/crates/arbiter-client/src/wallets/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
#[cfg(feature = "evm")]
|
||||||
|
pub mod evm;
|
||||||
@@ -10,7 +10,7 @@ tonic.workspace = true
|
|||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
tonic-prost = "0.14.3"
|
tonic-prost = "0.14.5"
|
||||||
prost = "0.14.3"
|
prost = "0.14.3"
|
||||||
kameo.workspace = true
|
kameo.workspace = true
|
||||||
url = "2.5.8"
|
url = "2.5.8"
|
||||||
@@ -21,9 +21,11 @@ base64 = "0.22.1"
|
|||||||
prost-types.workspace = true
|
prost-types.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
|
tokio-stream.workspace = true
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tonic-prost-build = "0.14.3"
|
tonic-prost-build = "0.14.5"
|
||||||
|
protoc-bin-vendored = "3"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
rstest.workspace = true
|
rstest.workspace = true
|
||||||
@@ -32,5 +34,3 @@ rcgen.workspace = true
|
|||||||
|
|
||||||
[package.metadata.cargo-shear]
|
[package.metadata.cargo-shear]
|
||||||
ignored = ["tonic-prost", "prost", "kameo"]
|
ignored = ["tonic-prost", "prost", "kameo"]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ pub mod transport;
|
|||||||
pub mod url;
|
pub mod url;
|
||||||
|
|
||||||
use base64::{Engine, prelude::BASE64_STANDARD};
|
use base64::{Engine, prelude::BASE64_STANDARD};
|
||||||
|
use std::{
|
||||||
|
path::PathBuf,
|
||||||
|
sync::{LazyLock, RwLock},
|
||||||
|
};
|
||||||
|
|
||||||
pub mod proto {
|
pub mod proto {
|
||||||
tonic::include_proto!("arbiter");
|
tonic::include_proto!("arbiter");
|
||||||
@@ -19,9 +23,35 @@ pub mod proto {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ClientMetadata {
|
||||||
|
pub name: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub version: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
pub static BOOTSTRAP_PATH: &str = "bootstrap_token";
|
pub static BOOTSTRAP_PATH: &str = "bootstrap_token";
|
||||||
|
pub const DEFAULT_SERVER_PORT: u16 = 50051;
|
||||||
|
static HOME_OVERRIDE: LazyLock<RwLock<Option<PathBuf>>> = LazyLock::new(|| RwLock::new(None));
|
||||||
|
|
||||||
|
pub fn set_home_path_override(path: Option<PathBuf>) -> Result<(), std::io::Error> {
|
||||||
|
let mut lock = HOME_OVERRIDE
|
||||||
|
.write()
|
||||||
|
.map_err(|_| std::io::Error::other("home path override lock poisoned"))?;
|
||||||
|
*lock = path;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn home_path() -> Result<std::path::PathBuf, std::io::Error> {
|
pub fn home_path() -> Result<std::path::PathBuf, std::io::Error> {
|
||||||
|
if let Some(path) = HOME_OVERRIDE
|
||||||
|
.read()
|
||||||
|
.map_err(|_| std::io::Error::other("home path override lock poisoned"))?
|
||||||
|
.clone()
|
||||||
|
{
|
||||||
|
std::fs::create_dir_all(&path)?;
|
||||||
|
return Ok(path);
|
||||||
|
}
|
||||||
|
|
||||||
static ARBITER_HOME: &str = ".arbiter";
|
static ARBITER_HOME: &str = ".arbiter";
|
||||||
let home_dir = std::env::home_dir().ok_or(std::io::Error::new(
|
let home_dir = std::env::home_dir().ok_or(std::io::Error::new(
|
||||||
std::io::ErrorKind::PermissionDenied,
|
std::io::ErrorKind::PermissionDenied,
|
||||||
|
|||||||
@@ -1,29 +1,49 @@
|
|||||||
//! Transport-facing abstractions shared by protocol/session code.
|
//! Transport-facing abstractions shared by protocol/session code.
|
||||||
//!
|
//!
|
||||||
//! This module defines a small duplex interface, [`Bi`], that actors and other
|
//! This module defines a small set of transport traits that actors and other
|
||||||
//! protocol code can depend on without knowing anything about the concrete
|
//! protocol code can depend on without knowing anything about the concrete
|
||||||
//! transport underneath.
|
//! transport underneath.
|
||||||
//!
|
//!
|
||||||
//! [`Bi`] is intentionally minimal and transport-agnostic:
|
//! The abstraction is split into:
|
||||||
//! - [`Bi::recv`] yields inbound messages
|
//! - [`Sender`] for outbound delivery
|
||||||
//! - [`Bi::send`] accepts outbound messages
|
//! - [`Receiver`] for inbound delivery
|
||||||
|
//! - [`Bi`] as the combined duplex form (`Sender + Receiver`)
|
||||||
|
//!
|
||||||
|
//! This split lets code depend only on the half it actually needs. For
|
||||||
|
//! example, some actor/session code only sends out-of-band messages, while
|
||||||
|
//! auth/state-machine code may need full duplex access.
|
||||||
|
//!
|
||||||
|
//! [`Bi`] remains intentionally minimal and transport-agnostic:
|
||||||
|
//! - [`Receiver::recv`] yields inbound messages
|
||||||
|
//! - [`Sender::send`] accepts outbound messages
|
||||||
//!
|
//!
|
||||||
//! Transport-specific adapters, including protobuf or gRPC bridges, live in the
|
//! Transport-specific adapters, including protobuf or gRPC bridges, live in the
|
||||||
//! crates that own those boundaries rather than in `arbiter-proto`.
|
//! crates that own those boundaries rather than in `arbiter-proto`.
|
||||||
//!
|
//!
|
||||||
|
//! [`Bi`] deliberately does not model request/response correlation. Some
|
||||||
|
//! transports may carry multiplexed request/response traffic, some may emit
|
||||||
|
//! out-of-band messages, and some may be one-message-at-a-time state machines.
|
||||||
|
//! Correlation concerns such as request IDs, pending response maps, and
|
||||||
|
//! out-of-band routing belong in the adapter or connection layer built on top
|
||||||
|
//! of [`Bi`], not in this abstraction itself.
|
||||||
|
//!
|
||||||
//! # Generic Ordering Rule
|
//! # Generic Ordering Rule
|
||||||
//!
|
//!
|
||||||
//! This module consistently uses `Inbound` first and `Outbound` second in
|
//! This module consistently uses `Inbound` first and `Outbound` second in
|
||||||
//! generic parameter lists.
|
//! generic parameter lists.
|
||||||
//!
|
//!
|
||||||
//! For [`Bi`], that means `Bi<Inbound, Outbound>`:
|
//! For [`Receiver`], [`Sender`], and [`Bi`], this means:
|
||||||
|
//! - `Receiver<Inbound>`
|
||||||
|
//! - `Sender<Outbound>`
|
||||||
|
//! - `Bi<Inbound, Outbound>`
|
||||||
|
//!
|
||||||
|
//! Concretely, for [`Bi`]:
|
||||||
//! - `recv() -> Option<Inbound>`
|
//! - `recv() -> Option<Inbound>`
|
||||||
//! - `send(Outbound)`
|
//! - `send(Outbound)`
|
||||||
//!
|
//!
|
||||||
//! [`expect_message`] is a small helper for request/response style flows: it
|
//! [`expect_message`] is a small helper for linear protocol steps: it reads one
|
||||||
//! reads one inbound message from a transport and extracts a typed value from
|
//! inbound message from a transport and extracts a typed value from it, failing
|
||||||
//! it, failing if the channel closes or the message shape is not what the
|
//! if the channel closes or the message shape is not what the caller expected.
|
||||||
//! caller expected.
|
|
||||||
//!
|
//!
|
||||||
//! [`DummyTransport`] is a no-op implementation useful for tests and local
|
//! [`DummyTransport`] is a no-op implementation useful for tests and local
|
||||||
//! actor execution where no real stream exists.
|
//! actor execution where no real stream exists.
|
||||||
@@ -63,16 +83,35 @@ where
|
|||||||
extractor(msg).ok_or(Error::UnexpectedMessage)
|
extractor(msg).ok_or(Error::UnexpectedMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait Sender<Outbound>: Send + Sync {
|
||||||
|
async fn send(&mut self, item: Outbound) -> Result<(), Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait Receiver<Inbound>: Send + Sync {
|
||||||
|
async fn recv(&mut self) -> Option<Inbound>;
|
||||||
|
}
|
||||||
|
|
||||||
/// Minimal bidirectional transport abstraction used by protocol code.
|
/// Minimal bidirectional transport abstraction used by protocol code.
|
||||||
///
|
///
|
||||||
/// `Bi<Inbound, Outbound>` models a duplex channel with:
|
/// `Bi<Inbound, Outbound>` is the combined duplex form of [`Sender`] and
|
||||||
|
/// [`Receiver`].
|
||||||
|
///
|
||||||
|
/// It models a channel with:
|
||||||
/// - inbound items of type `Inbound` read via [`Bi::recv`]
|
/// - inbound items of type `Inbound` read via [`Bi::recv`]
|
||||||
/// - outbound items of type `Outbound` written via [`Bi::send`]
|
/// - outbound items of type `Outbound` written via [`Bi::send`]
|
||||||
#[async_trait]
|
///
|
||||||
pub trait Bi<Inbound, Outbound>: Send + Sync + 'static {
|
/// It does not imply request/response sequencing, one-at-a-time exchange, or
|
||||||
async fn send(&mut self, item: Outbound) -> Result<(), Error>;
|
/// any built-in correlation mechanism between inbound and outbound items.
|
||||||
|
pub trait Bi<Inbound, Outbound>: Sender<Outbound> + Receiver<Inbound> + Send + Sync {}
|
||||||
|
|
||||||
async fn recv(&mut self) -> Option<Inbound>;
|
pub trait SplittableBi<Inbound, Outbound>: Bi<Inbound, Outbound> {
|
||||||
|
type Sender: Sender<Outbound>;
|
||||||
|
type Receiver: Receiver<Inbound>;
|
||||||
|
|
||||||
|
fn split(self) -> (Self::Sender, Self::Receiver);
|
||||||
|
fn from_parts(sender: Self::Sender, receiver: Self::Receiver) -> Self;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// No-op [`Bi`] transport for tests and manual actor usage.
|
/// No-op [`Bi`] transport for tests and manual actor usage.
|
||||||
@@ -83,22 +122,16 @@ pub struct DummyTransport<Inbound, Outbound> {
|
|||||||
_marker: PhantomData<(Inbound, Outbound)>,
|
_marker: PhantomData<(Inbound, Outbound)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Inbound, Outbound> DummyTransport<Inbound, Outbound> {
|
impl<Inbound, Outbound> Default for DummyTransport<Inbound, Outbound> {
|
||||||
pub fn new() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
_marker: PhantomData,
|
_marker: PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Inbound, Outbound> Default for DummyTransport<Inbound, Outbound> {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl<Inbound, Outbound> Bi<Inbound, Outbound> for DummyTransport<Inbound, Outbound>
|
impl<Inbound, Outbound> Sender<Outbound> for DummyTransport<Inbound, Outbound>
|
||||||
where
|
where
|
||||||
Inbound: Send + Sync + 'static,
|
Inbound: Send + Sync + 'static,
|
||||||
Outbound: Send + Sync + 'static,
|
Outbound: Send + Sync + 'static,
|
||||||
@@ -106,9 +139,25 @@ where
|
|||||||
async fn send(&mut self, _item: Outbound) -> Result<(), Error> {
|
async fn send(&mut self, _item: Outbound) -> Result<(), Error> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<Inbound, Outbound> Receiver<Inbound> for DummyTransport<Inbound, Outbound>
|
||||||
|
where
|
||||||
|
Inbound: Send + Sync + 'static,
|
||||||
|
Outbound: Send + Sync + 'static,
|
||||||
|
{
|
||||||
async fn recv(&mut self) -> Option<Inbound> {
|
async fn recv(&mut self) -> Option<Inbound> {
|
||||||
std::future::pending::<()>().await;
|
std::future::pending::<()>().await;
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<Inbound, Outbound> Bi<Inbound, Outbound> for DummyTransport<Inbound, Outbound>
|
||||||
|
where
|
||||||
|
Inbound: Send + Sync + 'static,
|
||||||
|
Outbound: Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod grpc;
|
||||||
|
|||||||
106
server/crates/arbiter-proto/src/transport/grpc.rs
Normal file
106
server/crates/arbiter-proto/src/transport/grpc.rs
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use futures::StreamExt;
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
use tokio_stream::wrappers::ReceiverStream;
|
||||||
|
|
||||||
|
use super::{Bi, Receiver, Sender};
|
||||||
|
|
||||||
|
pub struct GrpcSender<Outbound> {
|
||||||
|
tx: mpsc::Sender<Result<Outbound, tonic::Status>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<Outbound> Sender<Result<Outbound, tonic::Status>> for GrpcSender<Outbound>
|
||||||
|
where
|
||||||
|
Outbound: Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
async fn send(&mut self, item: Result<Outbound, tonic::Status>) -> Result<(), super::Error> {
|
||||||
|
self.tx
|
||||||
|
.send(item)
|
||||||
|
.await
|
||||||
|
.map_err(|_| super::Error::ChannelClosed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GrpcReceiver<Inbound> {
|
||||||
|
rx: tonic::Streaming<Inbound>,
|
||||||
|
}
|
||||||
|
#[async_trait]
|
||||||
|
impl<Inbound> Receiver<Result<Inbound, tonic::Status>> for GrpcReceiver<Inbound>
|
||||||
|
where
|
||||||
|
Inbound: Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
async fn recv(&mut self) -> Option<Result<Inbound, tonic::Status>> {
|
||||||
|
self.rx.next().await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GrpcBi<Inbound, Outbound> {
|
||||||
|
sender: GrpcSender<Outbound>,
|
||||||
|
receiver: GrpcReceiver<Inbound>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Inbound, Outbound> GrpcBi<Inbound, Outbound>
|
||||||
|
where
|
||||||
|
Inbound: Send + Sync + 'static,
|
||||||
|
Outbound: Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
pub fn from_bi_stream(
|
||||||
|
receiver: tonic::Streaming<Inbound>,
|
||||||
|
) -> (Self, ReceiverStream<Result<Outbound, tonic::Status>>) {
|
||||||
|
let (tx, rx) = mpsc::channel(10);
|
||||||
|
let sender = GrpcSender { tx };
|
||||||
|
let receiver = GrpcReceiver { rx: receiver };
|
||||||
|
let bi = GrpcBi { sender, receiver };
|
||||||
|
(bi, ReceiverStream::new(rx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<Inbound, Outbound> Sender<Result<Outbound, tonic::Status>> for GrpcBi<Inbound, Outbound>
|
||||||
|
where
|
||||||
|
Inbound: Send + Sync + 'static,
|
||||||
|
Outbound: Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
async fn send(&mut self, item: Result<Outbound, tonic::Status>) -> Result<(), super::Error> {
|
||||||
|
self.sender.send(item).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<Inbound, Outbound> Receiver<Result<Inbound, tonic::Status>> for GrpcBi<Inbound, Outbound>
|
||||||
|
where
|
||||||
|
Inbound: Send + Sync + 'static,
|
||||||
|
Outbound: Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
async fn recv(&mut self) -> Option<Result<Inbound, tonic::Status>> {
|
||||||
|
self.receiver.recv().await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Inbound, Outbound> Bi<Result<Inbound, tonic::Status>, Result<Outbound, tonic::Status>>
|
||||||
|
for GrpcBi<Inbound, Outbound>
|
||||||
|
where
|
||||||
|
Inbound: Send + Sync + 'static,
|
||||||
|
Outbound: Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Inbound, Outbound>
|
||||||
|
super::SplittableBi<Result<Inbound, tonic::Status>, Result<Outbound, tonic::Status>>
|
||||||
|
for GrpcBi<Inbound, Outbound>
|
||||||
|
where
|
||||||
|
Inbound: Send + Sync + 'static,
|
||||||
|
Outbound: Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
type Sender = GrpcSender<Outbound>;
|
||||||
|
type Receiver = GrpcReceiver<Inbound>;
|
||||||
|
|
||||||
|
fn split(self) -> (Self::Sender, Self::Receiver) {
|
||||||
|
(self.sender, self.receiver)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_parts(sender: Self::Sender, receiver: Self::Receiver) -> Self {
|
||||||
|
GrpcBi { sender, receiver }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,8 @@ const ARBITER_URL_SCHEME: &str = "arbiter";
|
|||||||
const CERT_QUERY_KEY: &str = "cert";
|
const CERT_QUERY_KEY: &str = "cert";
|
||||||
const BOOTSTRAP_TOKEN_QUERY_KEY: &str = "bootstrap_token";
|
const BOOTSTRAP_TOKEN_QUERY_KEY: &str = "bootstrap_token";
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub struct ArbiterUrl {
|
pub struct ArbiterUrl {
|
||||||
pub host: String,
|
pub host: String,
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ license = "Apache-2.0"
|
|||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
diesel = { version = "2.3.6", features = ["chrono", "returning_clauses_for_sqlite_3_35", "serde_json", "time", "uuid"] }
|
diesel = { version = "2.3.7", features = ["chrono", "returning_clauses_for_sqlite_3_35", "serde_json", "time", "uuid"] }
|
||||||
diesel-async = { version = "0.7.4", features = [
|
diesel-async = { version = "0.8.0", features = [
|
||||||
"bb8",
|
"bb8",
|
||||||
"migrations",
|
"migrations",
|
||||||
"sqlite",
|
"sqlite",
|
||||||
@@ -27,6 +27,7 @@ rustls.workspace = true
|
|||||||
smlang.workspace = true
|
smlang.workspace = true
|
||||||
miette.workspace = true
|
miette.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
|
fatality = "0.1.1"
|
||||||
diesel_migrations = { version = "2.3.1", features = ["sqlite"] }
|
diesel_migrations = { version = "2.3.1", features = ["sqlite"] }
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
secrecy = "0.10.3"
|
secrecy = "0.10.3"
|
||||||
@@ -43,7 +44,7 @@ x25519-dalek.workspace = true
|
|||||||
chacha20poly1305 = { version = "0.10.1", features = ["std"] }
|
chacha20poly1305 = { version = "0.10.1", features = ["std"] }
|
||||||
argon2 = { version = "0.5.3", features = ["zeroize"] }
|
argon2 = { version = "0.5.3", features = ["zeroize"] }
|
||||||
restructed = "0.2.2"
|
restructed = "0.2.2"
|
||||||
strum = { version = "0.27.2", features = ["derive"] }
|
strum = { version = "0.28.0", features = ["derive"] }
|
||||||
pem = "3.0.6"
|
pem = "3.0.6"
|
||||||
k256.workspace = true
|
k256.workspace = true
|
||||||
rsa.workspace = true
|
rsa.workspace = true
|
||||||
@@ -52,7 +53,11 @@ spki.workspace = true
|
|||||||
alloy.workspace = true
|
alloy.workspace = true
|
||||||
prost-types.workspace = true
|
prost-types.workspace = true
|
||||||
arbiter-tokens-registry.path = "../arbiter-tokens-registry"
|
arbiter-tokens-registry.path = "../arbiter-tokens-registry"
|
||||||
|
clap = { version = "4.6", features = ["derive"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
insta = "1.46.3"
|
insta = "1.46.3"
|
||||||
test-log = { version = "0.2", default-features = false, features = ["trace"] }
|
test-log = { version = "0.2", default-features = false, features = ["trace"] }
|
||||||
|
|
||||||
|
[target.'cfg(windows)'.dependencies]
|
||||||
|
windows-service = "0.8"
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ create table if not exists arbiter_settings (
|
|||||||
tls_id integer references tls_history (id) on delete RESTRICT
|
tls_id integer references tls_history (id) on delete RESTRICT
|
||||||
) STRICT;
|
) STRICT;
|
||||||
|
|
||||||
insert into arbiter_settings (id) values (1) on conflict do nothing; -- ensure singleton row exists
|
insert into arbiter_settings (id) values (1) on conflict do nothing;
|
||||||
|
-- ensure singleton row exists
|
||||||
|
|
||||||
create table if not exists useragent_client (
|
create table if not exists useragent_client (
|
||||||
id integer not null primary key,
|
id integer not null primary key,
|
||||||
@@ -50,15 +51,40 @@ create table if not exists useragent_client (
|
|||||||
created_at integer not null default(unixepoch ('now')),
|
created_at integer not null default(unixepoch ('now')),
|
||||||
updated_at integer not null default(unixepoch ('now'))
|
updated_at integer not null default(unixepoch ('now'))
|
||||||
) STRICT;
|
) STRICT;
|
||||||
|
create unique index if not exists uniq_useragent_client_public_key on useragent_client (public_key, key_type);
|
||||||
|
|
||||||
|
create table if not exists client_metadata (
|
||||||
|
id integer not null primary key,
|
||||||
|
name text not null, -- human-readable name for the client
|
||||||
|
description text, -- optional description for the client
|
||||||
|
version text, -- client version for tracking and debugging
|
||||||
|
created_at integer not null default(unixepoch ('now'))
|
||||||
|
) STRICT;
|
||||||
|
|
||||||
|
-- created to track history of changes
|
||||||
|
create table if not exists client_metadata_history (
|
||||||
|
id integer not null primary key,
|
||||||
|
metadata_id integer not null references client_metadata (id) on delete cascade,
|
||||||
|
client_id integer not null references program_client (id) on delete cascade,
|
||||||
|
created_at integer not null default(unixepoch ('now'))
|
||||||
|
) STRICT;
|
||||||
|
|
||||||
|
create unique index if not exists uniq_metadata_binding_client on client_metadata_history (client_id);
|
||||||
|
|
||||||
create table if not exists program_client (
|
create table if not exists program_client (
|
||||||
id integer not null primary key,
|
id integer not null primary key,
|
||||||
nonce integer not null default(1), -- used for auth challenge
|
nonce integer not null default(1), -- used for auth challenge
|
||||||
public_key blob not null,
|
public_key blob not null,
|
||||||
|
metadata_id integer not null references client_metadata (id) on delete cascade,
|
||||||
created_at integer not null default(unixepoch ('now')),
|
created_at integer not null default(unixepoch ('now')),
|
||||||
updated_at integer not null default(unixepoch ('now'))
|
updated_at integer not null default(unixepoch ('now'))
|
||||||
) STRICT;
|
) STRICT;
|
||||||
|
|
||||||
|
create unique index if not exists program_client_public_key_unique
|
||||||
|
on program_client (public_key);
|
||||||
|
|
||||||
|
create unique index if not exists uniq_program_client_public_key on program_client (public_key);
|
||||||
|
|
||||||
create table if not exists evm_wallet (
|
create table if not exists evm_wallet (
|
||||||
id integer not null primary key,
|
id integer not null primary key,
|
||||||
address blob not null, -- 20-byte Ethereum address
|
address blob not null, -- 20-byte Ethereum address
|
||||||
@@ -67,8 +93,18 @@ create table if not exists evm_wallet (
|
|||||||
) STRICT;
|
) STRICT;
|
||||||
|
|
||||||
create unique index if not exists uniq_evm_wallet_address on evm_wallet (address);
|
create unique index if not exists uniq_evm_wallet_address on evm_wallet (address);
|
||||||
|
|
||||||
create unique index if not exists uniq_evm_wallet_aead on evm_wallet (aead_encrypted_id);
|
create unique index if not exists uniq_evm_wallet_aead on evm_wallet (aead_encrypted_id);
|
||||||
|
|
||||||
|
create table if not exists evm_wallet_access (
|
||||||
|
id integer not null primary key,
|
||||||
|
wallet_id integer not null references evm_wallet (id) on delete cascade,
|
||||||
|
client_id integer not null references program_client (id) on delete cascade,
|
||||||
|
created_at integer not null default(unixepoch ('now'))
|
||||||
|
) STRICT;
|
||||||
|
|
||||||
|
create unique index if not exists uniq_wallet_access on evm_wallet_access (wallet_id, client_id);
|
||||||
|
|
||||||
create table if not exists evm_ether_transfer_limit (
|
create table if not exists evm_ether_transfer_limit (
|
||||||
id integer not null primary key,
|
id integer not null primary key,
|
||||||
window_secs integer not null, -- window duration in seconds
|
window_secs integer not null, -- window duration in seconds
|
||||||
@@ -78,8 +114,7 @@ create table if not exists evm_ether_transfer_limit (
|
|||||||
-- Shared grant properties: client scope, timeframe, fee caps, and rate limit
|
-- Shared grant properties: client scope, timeframe, fee caps, and rate limit
|
||||||
create table if not exists evm_basic_grant (
|
create table if not exists evm_basic_grant (
|
||||||
id integer not null primary key,
|
id integer not null primary key,
|
||||||
wallet_id integer not null references evm_wallet(id) on delete restrict,
|
wallet_access_id integer not null references evm_wallet_access (id) on delete restrict,
|
||||||
client_id integer not null references program_client(id) on delete restrict,
|
|
||||||
chain_id integer not null, -- EIP-155 chain ID
|
chain_id integer not null, -- EIP-155 chain ID
|
||||||
valid_from integer, -- unix timestamp (seconds), null = no lower bound
|
valid_from integer, -- unix timestamp (seconds), null = no lower bound
|
||||||
valid_until integer, -- unix timestamp (seconds), null = no upper bound
|
valid_until integer, -- unix timestamp (seconds), null = no upper bound
|
||||||
@@ -88,28 +123,27 @@ create table if not exists evm_basic_grant (
|
|||||||
rate_limit_count integer, -- max transactions in window, null = unlimited
|
rate_limit_count integer, -- max transactions in window, null = unlimited
|
||||||
rate_limit_window_secs integer, -- window duration in seconds, null = unlimited
|
rate_limit_window_secs integer, -- window duration in seconds, null = unlimited
|
||||||
revoked_at integer, -- unix timestamp when revoked, null = still active
|
revoked_at integer, -- unix timestamp when revoked, null = still active
|
||||||
created_at integer not null default(unixepoch('now'))
|
created_at integer not null default(unixepoch ('now'))
|
||||||
) STRICT;
|
) STRICT;
|
||||||
|
|
||||||
-- Shared transaction log for all EVM grants, used for rate limit tracking and auditing
|
-- Shared transaction log for all EVM grants, used for rate limit tracking and auditing
|
||||||
create table if not exists evm_transaction_log (
|
create table if not exists evm_transaction_log (
|
||||||
id integer not null primary key,
|
id integer not null primary key,
|
||||||
grant_id integer not null references evm_basic_grant(id) on delete restrict,
|
wallet_access_id integer not null references evm_wallet_access (id) on delete restrict,
|
||||||
client_id integer not null references program_client(id) on delete restrict,
|
grant_id integer not null references evm_basic_grant (id) on delete restrict,
|
||||||
wallet_id integer not null references evm_wallet(id) on delete restrict,
|
|
||||||
chain_id integer not null,
|
chain_id integer not null,
|
||||||
eth_value blob not null, -- always present on any EVM tx
|
eth_value blob not null, -- always present on any EVM tx
|
||||||
signed_at integer not null default(unixepoch('now'))
|
signed_at integer not null default(unixepoch ('now'))
|
||||||
) STRICT;
|
) STRICT;
|
||||||
|
|
||||||
create index if not exists idx_evm_basic_grant_wallet_chain on evm_basic_grant(client_id, wallet_id, chain_id);
|
create index if not exists idx_evm_basic_grant_access_chain on evm_basic_grant (wallet_access_id, chain_id);
|
||||||
|
|
||||||
-- ===============================
|
-- ===============================
|
||||||
-- ERC20 token transfer grant
|
-- ERC20 token transfer grant
|
||||||
-- ===============================
|
-- ===============================
|
||||||
create table if not exists evm_token_transfer_grant (
|
create table if not exists evm_token_transfer_grant (
|
||||||
id integer not null primary key,
|
id integer not null primary key,
|
||||||
basic_grant_id integer not null unique references evm_basic_grant(id) on delete cascade,
|
basic_grant_id integer not null unique references evm_basic_grant (id) on delete cascade,
|
||||||
token_contract blob not null, -- 20-byte ERC20 contract address
|
token_contract blob not null, -- 20-byte ERC20 contract address
|
||||||
receiver blob -- 20-byte recipient address or null if every recipient allowed
|
receiver blob -- 20-byte recipient address or null if every recipient allowed
|
||||||
) STRICT;
|
) STRICT;
|
||||||
@@ -117,7 +151,7 @@ create table if not exists evm_token_transfer_grant (
|
|||||||
-- Per-window volume limits for token transfer grants
|
-- Per-window volume limits for token transfer grants
|
||||||
create table if not exists evm_token_transfer_volume_limit (
|
create table if not exists evm_token_transfer_volume_limit (
|
||||||
id integer not null primary key,
|
id integer not null primary key,
|
||||||
grant_id integer not null references evm_token_transfer_grant(id) on delete cascade,
|
grant_id integer not null references evm_token_transfer_grant (id) on delete cascade,
|
||||||
window_secs integer not null, -- window duration in seconds
|
window_secs integer not null, -- window duration in seconds
|
||||||
max_volume blob not null -- big-endian 32-byte U256
|
max_volume blob not null -- big-endian 32-byte U256
|
||||||
) STRICT;
|
) STRICT;
|
||||||
@@ -125,35 +159,35 @@ create table if not exists evm_token_transfer_volume_limit (
|
|||||||
-- Log table for token transfer grant usage
|
-- Log table for token transfer grant usage
|
||||||
create table if not exists evm_token_transfer_log (
|
create table if not exists evm_token_transfer_log (
|
||||||
id integer not null primary key,
|
id integer not null primary key,
|
||||||
grant_id integer not null references evm_token_transfer_grant(id) on delete restrict,
|
grant_id integer not null references evm_token_transfer_grant (id) on delete restrict,
|
||||||
log_id integer not null references evm_transaction_log(id) on delete restrict,
|
log_id integer not null references evm_transaction_log (id) on delete restrict,
|
||||||
chain_id integer not null, -- EIP-155 chain ID
|
chain_id integer not null, -- EIP-155 chain ID
|
||||||
token_contract blob not null, -- 20-byte ERC20 contract address
|
token_contract blob not null, -- 20-byte ERC20 contract address
|
||||||
recipient_address blob not null, -- 20-byte recipient address
|
recipient_address blob not null, -- 20-byte recipient address
|
||||||
value blob not null, -- big-endian 32-byte U256
|
value blob not null, -- big-endian 32-byte U256
|
||||||
created_at integer not null default(unixepoch('now'))
|
created_at integer not null default(unixepoch ('now'))
|
||||||
) STRICT;
|
) STRICT;
|
||||||
|
|
||||||
create index if not exists idx_token_transfer_log_grant on evm_token_transfer_log(grant_id);
|
create index if not exists idx_token_transfer_log_grant on evm_token_transfer_log (grant_id);
|
||||||
create index if not exists idx_token_transfer_log_log_id on evm_token_transfer_log(log_id);
|
|
||||||
create index if not exists idx_token_transfer_log_chain on evm_token_transfer_log(chain_id);
|
|
||||||
|
|
||||||
|
create index if not exists idx_token_transfer_log_log_id on evm_token_transfer_log (log_id);
|
||||||
|
|
||||||
|
create index if not exists idx_token_transfer_log_chain on evm_token_transfer_log (chain_id);
|
||||||
|
|
||||||
-- ===============================
|
-- ===============================
|
||||||
-- Ether transfer grant (uses base log)
|
-- Ether transfer grant (uses base log)
|
||||||
-- ===============================
|
-- ===============================
|
||||||
create table if not exists evm_ether_transfer_grant (
|
create table if not exists evm_ether_transfer_grant (
|
||||||
id integer not null primary key,
|
id integer not null primary key,
|
||||||
basic_grant_id integer not null unique references evm_basic_grant(id) on delete cascade,
|
basic_grant_id integer not null unique references evm_basic_grant (id) on delete cascade,
|
||||||
limit_id integer not null references evm_ether_transfer_limit(id) on delete restrict
|
limit_id integer not null references evm_ether_transfer_limit (id) on delete restrict
|
||||||
) STRICT;
|
) STRICT;
|
||||||
|
|
||||||
-- Specific recipient addresses for an ether transfer grant
|
-- Specific recipient addresses for an ether transfer grant
|
||||||
create table if not exists evm_ether_transfer_grant_target (
|
create table if not exists evm_ether_transfer_grant_target (
|
||||||
id integer not null primary key,
|
id integer not null primary key,
|
||||||
grant_id integer not null references evm_ether_transfer_grant(id) on delete cascade,
|
grant_id integer not null references evm_ether_transfer_grant (id) on delete cascade,
|
||||||
address blob not null -- 20-byte recipient address
|
address blob not null -- 20-byte recipient address
|
||||||
) STRICT;
|
) STRICT;
|
||||||
|
|
||||||
create unique index if not exists uniq_ether_transfer_target on evm_ether_transfer_grant_target(grant_id, address);
|
create unique index if not exists uniq_ether_transfer_target on evm_ether_transfer_grant_target (grant_id, address);
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +1,30 @@
|
|||||||
use arbiter_proto::{format_challenge, transport::expect_message};
|
use arbiter_proto::{
|
||||||
|
ClientMetadata, format_challenge, transport::{Bi, expect_message}
|
||||||
|
};
|
||||||
|
use chrono::Utc;
|
||||||
use diesel::{
|
use diesel::{
|
||||||
ExpressionMethods as _, OptionalExtension as _, QueryDsl as _, dsl::insert_into, update,
|
ExpressionMethods as _, OptionalExtension as _, QueryDsl as _, SelectableHelper as _,
|
||||||
|
dsl::insert_into, update,
|
||||||
};
|
};
|
||||||
use diesel_async::RunQueryDsl as _;
|
use diesel_async::RunQueryDsl as _;
|
||||||
use ed25519_dalek::VerifyingKey;
|
use ed25519_dalek::{Signature, VerifyingKey};
|
||||||
use kameo::error::SendError;
|
use kameo::error::SendError;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
actors::{
|
actors::{
|
||||||
client::{ClientConnection, ConnectErrorCode, Request, Response},
|
client::{ClientConnection, ClientProfile},
|
||||||
router::{self, RequestClientApproval},
|
flow_coordinator::{self, RequestClientApproval},
|
||||||
|
},
|
||||||
|
db::{
|
||||||
|
self,
|
||||||
|
models::{ProgramClientMetadata, SqliteTimestamp},
|
||||||
|
schema::program_client,
|
||||||
},
|
},
|
||||||
db::{self, schema::program_client},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::session::ClientSession;
|
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
|
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("Unexpected message payload")]
|
|
||||||
UnexpectedMessagePayload,
|
|
||||||
#[error("Invalid client public key length")]
|
|
||||||
InvalidClientPubkeyLength,
|
|
||||||
#[error("Invalid client public key encoding")]
|
|
||||||
InvalidAuthPubkeyEncoding,
|
|
||||||
#[error("Database pool unavailable")]
|
#[error("Database pool unavailable")]
|
||||||
DatabasePoolUnavailable,
|
DatabasePoolUnavailable,
|
||||||
#[error("Database operation failed")]
|
#[error("Database operation failed")]
|
||||||
@@ -33,8 +33,6 @@ pub enum Error {
|
|||||||
InvalidChallengeSolution,
|
InvalidChallengeSolution,
|
||||||
#[error("Client approval request failed")]
|
#[error("Client approval request failed")]
|
||||||
ApproveError(#[from] ApproveError),
|
ApproveError(#[from] ApproveError),
|
||||||
#[error("Internal error")]
|
|
||||||
InternalError,
|
|
||||||
#[error("Transport error")]
|
#[error("Transport error")]
|
||||||
Transport,
|
Transport,
|
||||||
}
|
}
|
||||||
@@ -46,12 +44,37 @@ pub enum ApproveError {
|
|||||||
#[error("Client connection denied by user agents")]
|
#[error("Client connection denied by user agents")]
|
||||||
Denied,
|
Denied,
|
||||||
#[error("Upstream error: {0}")]
|
#[error("Upstream error: {0}")]
|
||||||
Upstream(router::ApprovalError),
|
Upstream(flow_coordinator::ApprovalError),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Inbound {
|
||||||
|
AuthChallengeRequest {
|
||||||
|
pubkey: VerifyingKey,
|
||||||
|
metadata: ClientMetadata,
|
||||||
|
},
|
||||||
|
AuthChallengeSolution {
|
||||||
|
signature: Signature,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Outbound {
|
||||||
|
AuthChallenge { pubkey: VerifyingKey, nonce: i32 },
|
||||||
|
AuthSuccess,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ClientInfo {
|
||||||
|
pub id: i32,
|
||||||
|
pub current_nonce: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Atomically reads and increments the nonce for a known client.
|
/// Atomically reads and increments the nonce for a known client.
|
||||||
/// Returns `None` if the pubkey is not registered.
|
/// Returns `None` if the pubkey is not registered.
|
||||||
async fn get_nonce(db: &db::DatabasePool, pubkey: &VerifyingKey) -> Result<Option<i32>, Error> {
|
async fn get_client_and_nonce(
|
||||||
|
db: &db::DatabasePool,
|
||||||
|
pubkey: &VerifyingKey,
|
||||||
|
) -> Result<Option<ClientInfo>, Error> {
|
||||||
let pubkey_bytes = pubkey.as_bytes().to_vec();
|
let pubkey_bytes = pubkey.as_bytes().to_vec();
|
||||||
|
|
||||||
let mut conn = db.get().await.map_err(|e| {
|
let mut conn = db.get().await.map_err(|e| {
|
||||||
@@ -62,10 +85,10 @@ async fn get_nonce(db: &db::DatabasePool, pubkey: &VerifyingKey) -> Result<Optio
|
|||||||
conn.exclusive_transaction(|conn| {
|
conn.exclusive_transaction(|conn| {
|
||||||
let pubkey_bytes = pubkey_bytes.clone();
|
let pubkey_bytes = pubkey_bytes.clone();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let Some(current_nonce) = program_client::table
|
let Some((client_id, current_nonce)) = program_client::table
|
||||||
.filter(program_client::public_key.eq(&pubkey_bytes))
|
.filter(program_client::public_key.eq(&pubkey_bytes))
|
||||||
.select(program_client::nonce)
|
.select((program_client::id, program_client::nonce))
|
||||||
.first::<i32>(conn)
|
.first::<(i32, i32)>(conn)
|
||||||
.await
|
.await
|
||||||
.optional()?
|
.optional()?
|
||||||
else {
|
else {
|
||||||
@@ -78,7 +101,10 @@ async fn get_nonce(db: &db::DatabasePool, pubkey: &VerifyingKey) -> Result<Optio
|
|||||||
.execute(conn)
|
.execute(conn)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(Some(current_nonce))
|
Ok(Some(ClientInfo {
|
||||||
|
id: client_id,
|
||||||
|
current_nonce,
|
||||||
|
}))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@@ -90,13 +116,11 @@ async fn get_nonce(db: &db::DatabasePool, pubkey: &VerifyingKey) -> Result<Optio
|
|||||||
|
|
||||||
async fn approve_new_client(
|
async fn approve_new_client(
|
||||||
actors: &crate::actors::GlobalActors,
|
actors: &crate::actors::GlobalActors,
|
||||||
pubkey: VerifyingKey,
|
profile: ClientProfile,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let result = actors
|
let result = actors
|
||||||
.router
|
.flow_coordinator
|
||||||
.ask(RequestClientApproval {
|
.ask(RequestClientApproval { client: profile })
|
||||||
client_pubkey: pubkey,
|
|
||||||
})
|
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
@@ -107,61 +131,144 @@ async fn approve_new_client(
|
|||||||
Err(Error::ApproveError(ApproveError::Upstream(e)))
|
Err(Error::ApproveError(ApproveError::Upstream(e)))
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(error = ?e, "Approval request to router failed");
|
error!(error = ?e, "Approval request to flow coordinator failed");
|
||||||
Err(Error::ApproveError(ApproveError::Internal))
|
Err(Error::ApproveError(ApproveError::Internal))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn insert_client(db: &db::DatabasePool, pubkey: &VerifyingKey) -> Result<(), Error> {
|
async fn insert_client(
|
||||||
let now = std::time::SystemTime::now()
|
db: &db::DatabasePool,
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
pubkey: &VerifyingKey,
|
||||||
.unwrap_or_default()
|
metadata: &ClientMetadata,
|
||||||
.as_secs() as i32;
|
) -> Result<i32, Error> {
|
||||||
|
use crate::db::schema::{client_metadata, program_client};
|
||||||
|
let mut conn = db.get().await.map_err(|e| {
|
||||||
|
error!(error = ?e, "Database pool error");
|
||||||
|
Error::DatabasePoolUnavailable
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let metadata_id = insert_into(client_metadata::table)
|
||||||
|
.values((
|
||||||
|
client_metadata::name.eq(&metadata.name),
|
||||||
|
client_metadata::description.eq(&metadata.description),
|
||||||
|
client_metadata::version.eq(&metadata.version),
|
||||||
|
))
|
||||||
|
.returning(client_metadata::id)
|
||||||
|
.get_result::<i32>(&mut conn)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
error!(error = ?e, "Failed to insert client metadata");
|
||||||
|
Error::DatabaseOperationFailed
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let client_id = insert_into(program_client::table)
|
||||||
|
.values((
|
||||||
|
program_client::public_key.eq(pubkey.as_bytes().to_vec()),
|
||||||
|
program_client::metadata_id.eq(metadata_id),
|
||||||
|
program_client::nonce.eq(1), // pre-incremented; challenge uses 0
|
||||||
|
))
|
||||||
|
.on_conflict_do_nothing()
|
||||||
|
.returning(program_client::id)
|
||||||
|
.get_result::<i32>(&mut conn)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
error!(error = ?e, "Failed to insert client metadata");
|
||||||
|
Error::DatabaseOperationFailed
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(client_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn sync_client_metadata(
|
||||||
|
db: &db::DatabasePool,
|
||||||
|
client_id: i32,
|
||||||
|
metadata: &ClientMetadata,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
use crate::db::schema::{client_metadata, client_metadata_history};
|
||||||
|
|
||||||
|
let now = SqliteTimestamp(Utc::now());
|
||||||
|
|
||||||
let mut conn = db.get().await.map_err(|e| {
|
let mut conn = db.get().await.map_err(|e| {
|
||||||
error!(error = ?e, "Database pool error");
|
error!(error = ?e, "Database pool error");
|
||||||
Error::DatabasePoolUnavailable
|
Error::DatabasePoolUnavailable
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
insert_into(program_client::table)
|
conn.exclusive_transaction(|conn| {
|
||||||
|
let metadata = metadata.clone();
|
||||||
|
Box::pin(async move {
|
||||||
|
let (current_metadata_id, current): (i32, ProgramClientMetadata) =
|
||||||
|
program_client::table
|
||||||
|
.find(client_id)
|
||||||
|
.inner_join(client_metadata::table)
|
||||||
|
.select((
|
||||||
|
program_client::metadata_id,
|
||||||
|
ProgramClientMetadata::as_select(),
|
||||||
|
))
|
||||||
|
.first(conn)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let unchanged = current.name == metadata.name
|
||||||
|
&& current.description == metadata.description
|
||||||
|
&& current.version == metadata.version;
|
||||||
|
if unchanged {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
insert_into(client_metadata_history::table)
|
||||||
.values((
|
.values((
|
||||||
program_client::public_key.eq(pubkey.as_bytes().to_vec()),
|
client_metadata_history::metadata_id.eq(current_metadata_id),
|
||||||
program_client::nonce.eq(1), // pre-incremented; challenge uses 0
|
client_metadata_history::client_id.eq(client_id),
|
||||||
program_client::created_at.eq(now),
|
))
|
||||||
|
.execute(conn)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let metadata_id = insert_into(client_metadata::table)
|
||||||
|
.values((
|
||||||
|
client_metadata::name.eq(&metadata.name),
|
||||||
|
client_metadata::description.eq(&metadata.description),
|
||||||
|
client_metadata::version.eq(&metadata.version),
|
||||||
|
))
|
||||||
|
.returning(client_metadata::id)
|
||||||
|
.get_result::<i32>(conn)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
update(program_client::table.find(client_id))
|
||||||
|
.set((
|
||||||
|
program_client::metadata_id.eq(metadata_id),
|
||||||
program_client::updated_at.eq(now),
|
program_client::updated_at.eq(now),
|
||||||
))
|
))
|
||||||
.execute(&mut conn)
|
.execute(conn)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok::<(), diesel::result::Error>(())
|
||||||
|
})
|
||||||
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
error!(error = ?e, "Failed to insert new client");
|
error!(error = ?e, "Database error");
|
||||||
Error::DatabaseOperationFailed
|
Error::DatabaseOperationFailed
|
||||||
})?;
|
})
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn challenge_client(
|
async fn challenge_client<T>(
|
||||||
props: &mut ClientConnection,
|
transport: &mut T,
|
||||||
pubkey: VerifyingKey,
|
pubkey: VerifyingKey,
|
||||||
nonce: i32,
|
nonce: i32,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error>
|
||||||
let challenge_pubkey = pubkey.as_bytes().to_vec();
|
where
|
||||||
|
T: Bi<Inbound, Result<Outbound, Error>> + ?Sized,
|
||||||
props
|
{
|
||||||
.transport
|
transport
|
||||||
.send(Ok(Response::AuthChallenge {
|
.send(Ok(Outbound::AuthChallenge { pubkey, nonce }))
|
||||||
pubkey: challenge_pubkey.clone(),
|
|
||||||
nonce,
|
|
||||||
}))
|
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
error!(error = ?e, "Failed to send auth challenge");
|
error!(error = ?e, "Failed to send auth challenge");
|
||||||
Error::Transport
|
Error::Transport
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let signature = expect_message(&mut *props.transport, |req: Request| match req {
|
let signature = expect_message(transport, |req: Inbound| match req {
|
||||||
Request::AuthChallengeSolution { signature } => Some(signature),
|
Inbound::AuthChallengeSolution { signature } => Some(signature),
|
||||||
_ => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@@ -170,13 +277,9 @@ async fn challenge_client(
|
|||||||
Error::Transport
|
Error::Transport
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let formatted = format_challenge(nonce, &challenge_pubkey);
|
let formatted = format_challenge(nonce, pubkey.as_bytes());
|
||||||
let sig = signature.as_slice().try_into().map_err(|_| {
|
|
||||||
error!("Invalid signature length");
|
|
||||||
Error::InvalidChallengeSolution
|
|
||||||
})?;
|
|
||||||
|
|
||||||
pubkey.verify_strict(&formatted, &sig).map_err(|_| {
|
pubkey.verify_strict(&formatted, &signature).map_err(|_| {
|
||||||
error!("Challenge solution verification failed");
|
error!("Challenge solution verification failed");
|
||||||
Error::InvalidChallengeSolution
|
Error::InvalidChallengeSolution
|
||||||
})?;
|
})?;
|
||||||
@@ -184,54 +287,47 @@ async fn challenge_client(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn connect_error_code(err: &Error) -> ConnectErrorCode {
|
pub async fn authenticate<T>(
|
||||||
match err {
|
props: &mut ClientConnection,
|
||||||
Error::ApproveError(ApproveError::Denied) => ConnectErrorCode::ApprovalDenied,
|
transport: &mut T,
|
||||||
Error::ApproveError(ApproveError::Upstream(
|
) -> Result<VerifyingKey, Error>
|
||||||
router::ApprovalError::NoUserAgentsConnected,
|
where
|
||||||
)) => ConnectErrorCode::NoUserAgentsOnline,
|
T: Bi<Inbound, Result<Outbound, Error>> + Send + ?Sized,
|
||||||
_ => ConnectErrorCode::Unknown,
|
{
|
||||||
}
|
let Some(Inbound::AuthChallengeRequest { pubkey, metadata }) = transport.recv().await else {
|
||||||
}
|
|
||||||
|
|
||||||
async fn authenticate(props: &mut ClientConnection) -> Result<VerifyingKey, Error> {
|
|
||||||
let Some(Request::AuthChallengeRequest {
|
|
||||||
pubkey: challenge_pubkey,
|
|
||||||
}) = props.transport.recv().await
|
|
||||||
else {
|
|
||||||
return Err(Error::Transport);
|
return Err(Error::Transport);
|
||||||
};
|
};
|
||||||
|
|
||||||
let pubkey_bytes = challenge_pubkey
|
let info = match get_client_and_nonce(&props.db, &pubkey).await? {
|
||||||
.as_array()
|
|
||||||
.ok_or(Error::InvalidClientPubkeyLength)?;
|
|
||||||
let pubkey =
|
|
||||||
VerifyingKey::from_bytes(pubkey_bytes).map_err(|_| Error::InvalidAuthPubkeyEncoding)?;
|
|
||||||
|
|
||||||
let nonce = match get_nonce(&props.db, &pubkey).await? {
|
|
||||||
Some(nonce) => nonce,
|
Some(nonce) => nonce,
|
||||||
None => {
|
None => {
|
||||||
approve_new_client(&props.actors, pubkey).await?;
|
approve_new_client(
|
||||||
insert_client(&props.db, &pubkey).await?;
|
&props.actors,
|
||||||
0
|
ClientProfile {
|
||||||
|
pubkey,
|
||||||
|
metadata: metadata.clone(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let client_id = insert_client(&props.db, &pubkey, &metadata).await?;
|
||||||
|
ClientInfo {
|
||||||
|
id: client_id,
|
||||||
|
current_nonce: 0,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
challenge_client(props, pubkey, nonce).await?;
|
sync_client_metadata(&props.db, info.id, &metadata).await?;
|
||||||
|
|
||||||
|
challenge_client(transport, pubkey, info.current_nonce).await?;
|
||||||
|
|
||||||
|
transport
|
||||||
|
.send(Ok(Outbound::AuthSuccess))
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
error!(error = ?e, "Failed to send auth success");
|
||||||
|
Error::Transport
|
||||||
|
})?;
|
||||||
|
|
||||||
Ok(pubkey)
|
Ok(pubkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn authenticate_and_create(mut props: ClientConnection) -> Result<ClientSession, Error> {
|
|
||||||
match authenticate(&mut props).await {
|
|
||||||
Ok(_pubkey) => Ok(ClientSession::new(props)),
|
|
||||||
Err(err) => {
|
|
||||||
let code = connect_error_code(&err);
|
|
||||||
let _ = props
|
|
||||||
.transport
|
|
||||||
.send(Ok(Response::ClientConnectError { code }))
|
|
||||||
.await;
|
|
||||||
Err(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,74 +1,43 @@
|
|||||||
use arbiter_proto::transport::Bi;
|
use arbiter_proto::{ClientMetadata, transport::Bi};
|
||||||
use kameo::actor::Spawn;
|
use kameo::actor::Spawn;
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
actors::{GlobalActors, client::session::ClientSession},
|
actors::{GlobalActors, client::{ session::ClientSession}},
|
||||||
db,
|
db,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum ClientError {
|
pub struct ClientProfile {
|
||||||
#[error("Expected message with payload")]
|
pub pubkey: ed25519_dalek::VerifyingKey,
|
||||||
MissingRequestPayload,
|
pub metadata: ClientMetadata,
|
||||||
#[error("Unexpected request payload")]
|
|
||||||
UnexpectedRequestPayload,
|
|
||||||
#[error("State machine error")]
|
|
||||||
StateTransitionFailed,
|
|
||||||
#[error("Connection registration failed")]
|
|
||||||
ConnectionRegistrationFailed,
|
|
||||||
#[error(transparent)]
|
|
||||||
Auth(#[from] auth::Error),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum ConnectErrorCode {
|
|
||||||
Unknown,
|
|
||||||
ApprovalDenied,
|
|
||||||
NoUserAgentsOnline,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub enum Request {
|
|
||||||
AuthChallengeRequest { pubkey: Vec<u8> },
|
|
||||||
AuthChallengeSolution { signature: Vec<u8> },
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub enum Response {
|
|
||||||
AuthChallenge { pubkey: Vec<u8>, nonce: i32 },
|
|
||||||
AuthOk,
|
|
||||||
ClientConnectError { code: ConnectErrorCode },
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type Transport = Box<dyn Bi<Request, Result<Response, ClientError>> + Send>;
|
|
||||||
|
|
||||||
pub struct ClientConnection {
|
pub struct ClientConnection {
|
||||||
pub(crate) db: db::DatabasePool,
|
pub(crate) db: db::DatabasePool,
|
||||||
pub(crate) transport: Transport,
|
|
||||||
pub(crate) actors: GlobalActors,
|
pub(crate) actors: GlobalActors,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ClientConnection {
|
impl ClientConnection {
|
||||||
pub fn new(db: db::DatabasePool, transport: Transport, actors: GlobalActors) -> Self {
|
pub fn new(db: db::DatabasePool, actors: GlobalActors) -> Self {
|
||||||
Self {
|
Self { db, actors }
|
||||||
db,
|
|
||||||
transport,
|
|
||||||
actors,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod session;
|
pub mod session;
|
||||||
|
|
||||||
pub async fn connect_client(props: ClientConnection) {
|
pub async fn connect_client<T>(mut props: ClientConnection, transport: &mut T)
|
||||||
match auth::authenticate_and_create(props).await {
|
where
|
||||||
Ok(session) => {
|
T: Bi<auth::Inbound, Result<auth::Outbound, auth::Error>> + Send + ?Sized,
|
||||||
ClientSession::spawn(session);
|
{
|
||||||
|
match auth::authenticate(&mut props, transport).await {
|
||||||
|
Ok(_pubkey) => {
|
||||||
|
ClientSession::spawn(ClientSession::new(props));
|
||||||
info!("Client authenticated, session started");
|
info!("Client authenticated, session started");
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
let _ = transport.send(Err(err.clone())).await;
|
||||||
error!(?err, "Authentication failed, closing connection");
|
error!(?err, "Authentication failed, closing connection");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
use kameo::Actor;
|
use kameo::{Actor, messages};
|
||||||
use tokio::select;
|
use tracing::error;
|
||||||
use tracing::{error, info};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
actors::{
|
actors::{
|
||||||
GlobalActors,
|
GlobalActors, client::ClientConnection, flow_coordinator::RegisterClient,
|
||||||
client::{ClientConnection, ClientError, Request, Response},
|
keyholder::KeyHolderState,
|
||||||
router::RegisterClient,
|
|
||||||
},
|
},
|
||||||
db,
|
db,
|
||||||
};
|
};
|
||||||
@@ -19,19 +17,30 @@ impl ClientSession {
|
|||||||
pub(crate) fn new(props: ClientConnection) -> Self {
|
pub(crate) fn new(props: ClientConnection) -> Self {
|
||||||
Self { props }
|
Self { props }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn process_transport_inbound(&mut self, req: Request) -> Output {
|
|
||||||
let _ = req;
|
|
||||||
Err(ClientError::UnexpectedRequestPayload)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Output = Result<Response, ClientError>;
|
#[messages]
|
||||||
|
impl ClientSession {
|
||||||
|
#[message]
|
||||||
|
pub(crate) async fn handle_query_vault_state(&mut self) -> Result<KeyHolderState, Error> {
|
||||||
|
use crate::actors::keyholder::GetState;
|
||||||
|
|
||||||
|
let vault_state = match self.props.actors.key_holder.ask(GetState {}).await {
|
||||||
|
Ok(state) => state,
|
||||||
|
Err(err) => {
|
||||||
|
error!(?err, actor = "client", "keyholder.query.failed");
|
||||||
|
return Err(Error::Internal);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(vault_state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Actor for ClientSession {
|
impl Actor for ClientSession {
|
||||||
type Args = Self;
|
type Args = Self;
|
||||||
|
|
||||||
type Error = ClientError;
|
type Error = Error;
|
||||||
|
|
||||||
async fn on_start(
|
async fn on_start(
|
||||||
args: Self::Args,
|
args: Self::Args,
|
||||||
@@ -39,55 +48,25 @@ impl Actor for ClientSession {
|
|||||||
) -> Result<Self, Self::Error> {
|
) -> Result<Self, Self::Error> {
|
||||||
args.props
|
args.props
|
||||||
.actors
|
.actors
|
||||||
.router
|
.flow_coordinator
|
||||||
.ask(RegisterClient { actor: this })
|
.ask(RegisterClient { actor: this })
|
||||||
.await
|
.await
|
||||||
.map_err(|_| ClientError::ConnectionRegistrationFailed)?;
|
.map_err(|_| Error::ConnectionRegistrationFailed)?;
|
||||||
Ok(args)
|
Ok(args)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn next(
|
|
||||||
&mut self,
|
|
||||||
_actor_ref: kameo::prelude::WeakActorRef<Self>,
|
|
||||||
mailbox_rx: &mut kameo::prelude::MailboxReceiver<Self>,
|
|
||||||
) -> Option<kameo::mailbox::Signal<Self>> {
|
|
||||||
loop {
|
|
||||||
select! {
|
|
||||||
signal = mailbox_rx.recv() => {
|
|
||||||
return signal;
|
|
||||||
}
|
|
||||||
msg = self.props.transport.recv() => {
|
|
||||||
match msg {
|
|
||||||
Some(request) => {
|
|
||||||
match self.process_transport_inbound(request).await {
|
|
||||||
Ok(resp) => {
|
|
||||||
if self.props.transport.send(Ok(resp)).await.is_err() {
|
|
||||||
error!(actor = "client", reason = "channel closed", "send.failed");
|
|
||||||
return Some(kameo::mailbox::Signal::Stop);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
let _ = self.props.transport.send(Err(err)).await;
|
|
||||||
return Some(kameo::mailbox::Signal::Stop);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
info!(actor = "client", "transport.closed");
|
|
||||||
return Some(kameo::mailbox::Signal::Stop);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ClientSession {
|
impl ClientSession {
|
||||||
pub fn new_test(db: db::DatabasePool, actors: GlobalActors) -> Self {
|
pub fn new_test(db: db::DatabasePool, actors: GlobalActors) -> Self {
|
||||||
use arbiter_proto::transport::DummyTransport;
|
let props = ClientConnection::new(db, actors);
|
||||||
let transport: super::Transport = Box::new(DummyTransport::new());
|
|
||||||
let props = ClientConnection::new(db, transport, actors);
|
|
||||||
Self { props }
|
Self { props }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("Connection registration failed")]
|
||||||
|
ConnectionRegistrationFailed,
|
||||||
|
#[error("Internal error")]
|
||||||
|
Internal,
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ use rand::{SeedableRng, rng, rngs::StdRng};
|
|||||||
use crate::{
|
use crate::{
|
||||||
actors::keyholder::{CreateNew, Decrypt, KeyHolder},
|
actors::keyholder::{CreateNew, Decrypt, KeyHolder},
|
||||||
db::{
|
db::{
|
||||||
self, DatabasePool,
|
self, DatabaseError, DatabasePool,
|
||||||
models::{self, SqliteTimestamp},
|
models::{self, SqliteTimestamp},
|
||||||
schema,
|
schema,
|
||||||
},
|
},
|
||||||
evm::{
|
evm::{
|
||||||
self, ListGrantsError, RunKind,
|
self, RunKind,
|
||||||
policies::{
|
policies::{
|
||||||
FullGrant, Grant, SharedGrantSettings, SpecificGrant, SpecificMeaning,
|
FullGrant, Grant, SharedGrantSettings, SpecificGrant, SpecificMeaning,
|
||||||
ether_transfer::EtherTransfer, token_transfers::TokenTransfer,
|
ether_transfer::EtherTransfer, token_transfers::TokenTransfer,
|
||||||
@@ -33,11 +33,7 @@ pub enum SignTransactionError {
|
|||||||
|
|
||||||
#[error("Database error: {0}")]
|
#[error("Database error: {0}")]
|
||||||
#[diagnostic(code(arbiter::evm::sign::database))]
|
#[diagnostic(code(arbiter::evm::sign::database))]
|
||||||
Database(#[from] diesel::result::Error),
|
Database(#[from] DatabaseError),
|
||||||
|
|
||||||
#[error("Database pool error: {0}")]
|
|
||||||
#[diagnostic(code(arbiter::evm::sign::pool))]
|
|
||||||
Pool(#[from] db::PoolError),
|
|
||||||
|
|
||||||
#[error("Keyholder error: {0}")]
|
#[error("Keyholder error: {0}")]
|
||||||
#[diagnostic(code(arbiter::evm::sign::keyholder))]
|
#[diagnostic(code(arbiter::evm::sign::keyholder))]
|
||||||
@@ -68,15 +64,7 @@ pub enum Error {
|
|||||||
|
|
||||||
#[error("Database error: {0}")]
|
#[error("Database error: {0}")]
|
||||||
#[diagnostic(code(arbiter::evm::database))]
|
#[diagnostic(code(arbiter::evm::database))]
|
||||||
Database(#[from] diesel::result::Error),
|
Database(#[from] DatabaseError),
|
||||||
|
|
||||||
#[error("Database pool error: {0}")]
|
|
||||||
#[diagnostic(code(arbiter::evm::database_pool))]
|
|
||||||
DatabasePool(#[from] db::PoolError),
|
|
||||||
|
|
||||||
#[error("Grant creation error: {0}")]
|
|
||||||
#[diagnostic(code(arbiter::evm::creation))]
|
|
||||||
Creation(#[from] evm::CreationError),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Actor)]
|
#[derive(Actor)]
|
||||||
@@ -105,7 +93,7 @@ impl EvmActor {
|
|||||||
#[messages]
|
#[messages]
|
||||||
impl EvmActor {
|
impl EvmActor {
|
||||||
#[message]
|
#[message]
|
||||||
pub async fn generate(&mut self) -> Result<Address, Error> {
|
pub async fn generate(&mut self) -> Result<(i32, Address), Error> {
|
||||||
let (mut key_cell, address) = safe_signer::generate(&mut self.rng);
|
let (mut key_cell, address) = safe_signer::generate(&mut self.rng);
|
||||||
|
|
||||||
let plaintext = key_cell.read_inline(|reader| SafeCell::new(reader.to_vec()));
|
let plaintext = key_cell.read_inline(|reader| SafeCell::new(reader.to_vec()));
|
||||||
@@ -116,29 +104,32 @@ impl EvmActor {
|
|||||||
.await
|
.await
|
||||||
.map_err(|_| Error::KeyholderSend)?;
|
.map_err(|_| Error::KeyholderSend)?;
|
||||||
|
|
||||||
let mut conn = self.db.get().await?;
|
let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
|
||||||
insert_into(schema::evm_wallet::table)
|
let wallet_id = insert_into(schema::evm_wallet::table)
|
||||||
.values(&models::NewEvmWallet {
|
.values(&models::NewEvmWallet {
|
||||||
address: address.as_slice().to_vec(),
|
address: address.as_slice().to_vec(),
|
||||||
aead_encrypted_id: aead_id,
|
aead_encrypted_id: aead_id,
|
||||||
})
|
})
|
||||||
.execute(&mut conn)
|
.returning(schema::evm_wallet::id)
|
||||||
.await?;
|
.get_result(&mut conn)
|
||||||
|
.await
|
||||||
|
.map_err(DatabaseError::from)?;
|
||||||
|
|
||||||
Ok(address)
|
Ok((wallet_id, address))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[message]
|
#[message]
|
||||||
pub async fn list_wallets(&self) -> Result<Vec<Address>, Error> {
|
pub async fn list_wallets(&self) -> Result<Vec<(i32, Address)>, Error> {
|
||||||
let mut conn = self.db.get().await?;
|
let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
|
||||||
let rows: Vec<models::EvmWallet> = schema::evm_wallet::table
|
let rows: Vec<models::EvmWallet> = schema::evm_wallet::table
|
||||||
.select(models::EvmWallet::as_select())
|
.select(models::EvmWallet::as_select())
|
||||||
.load(&mut conn)
|
.load(&mut conn)
|
||||||
.await?;
|
.await
|
||||||
|
.map_err(DatabaseError::from)?;
|
||||||
|
|
||||||
Ok(rows
|
Ok(rows
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|w| Address::from_slice(&w.address))
|
.map(|w| (w.id, Address::from_slice(&w.address)))
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -148,31 +139,24 @@ impl EvmActor {
|
|||||||
#[message]
|
#[message]
|
||||||
pub async fn useragent_create_grant(
|
pub async fn useragent_create_grant(
|
||||||
&mut self,
|
&mut self,
|
||||||
client_id: i32,
|
|
||||||
basic: SharedGrantSettings,
|
basic: SharedGrantSettings,
|
||||||
grant: SpecificGrant,
|
grant: SpecificGrant,
|
||||||
) -> Result<i32, evm::CreationError> {
|
) -> Result<i32, DatabaseError> {
|
||||||
match grant {
|
match grant {
|
||||||
SpecificGrant::EtherTransfer(settings) => {
|
SpecificGrant::EtherTransfer(settings) => {
|
||||||
self.engine
|
self.engine
|
||||||
.create_grant::<EtherTransfer>(
|
.create_grant::<EtherTransfer>(FullGrant {
|
||||||
client_id,
|
|
||||||
FullGrant {
|
|
||||||
basic,
|
basic,
|
||||||
specific: settings,
|
specific: settings,
|
||||||
},
|
})
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
SpecificGrant::TokenTransfer(settings) => {
|
SpecificGrant::TokenTransfer(settings) => {
|
||||||
self.engine
|
self.engine
|
||||||
.create_grant::<TokenTransfer>(
|
.create_grant::<TokenTransfer>(FullGrant {
|
||||||
client_id,
|
|
||||||
FullGrant {
|
|
||||||
basic,
|
basic,
|
||||||
specific: settings,
|
specific: settings,
|
||||||
},
|
})
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -180,22 +164,23 @@ impl EvmActor {
|
|||||||
|
|
||||||
#[message]
|
#[message]
|
||||||
pub async fn useragent_delete_grant(&mut self, grant_id: i32) -> Result<(), Error> {
|
pub async fn useragent_delete_grant(&mut self, grant_id: i32) -> Result<(), Error> {
|
||||||
let mut conn = self.db.get().await?;
|
let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
|
||||||
diesel::update(schema::evm_basic_grant::table)
|
diesel::update(schema::evm_basic_grant::table)
|
||||||
.filter(schema::evm_basic_grant::id.eq(grant_id))
|
.filter(schema::evm_basic_grant::id.eq(grant_id))
|
||||||
.set(schema::evm_basic_grant::revoked_at.eq(SqliteTimestamp::now()))
|
.set(schema::evm_basic_grant::revoked_at.eq(SqliteTimestamp::now()))
|
||||||
.execute(&mut conn)
|
.execute(&mut conn)
|
||||||
.await?;
|
.await
|
||||||
|
.map_err(DatabaseError::from)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[message]
|
#[message]
|
||||||
pub async fn useragent_list_grants(&mut self) -> Result<Vec<Grant<SpecificGrant>>, Error> {
|
pub async fn useragent_list_grants(&mut self) -> Result<Vec<Grant<SpecificGrant>>, Error> {
|
||||||
match self.engine.list_all_grants().await {
|
Ok(self
|
||||||
Ok(grants) => Ok(grants),
|
.engine
|
||||||
Err(ListGrantsError::Database(db)) => Err(Error::Database(db)),
|
.list_all_grants()
|
||||||
Err(ListGrantsError::Pool(pool)) => Err(Error::DatabasePool(pool)),
|
.await
|
||||||
}
|
.map_err(DatabaseError::from)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[message]
|
#[message]
|
||||||
@@ -205,24 +190,29 @@ impl EvmActor {
|
|||||||
wallet_address: Address,
|
wallet_address: Address,
|
||||||
transaction: TxEip1559,
|
transaction: TxEip1559,
|
||||||
) -> Result<SpecificMeaning, SignTransactionError> {
|
) -> Result<SpecificMeaning, SignTransactionError> {
|
||||||
let mut conn = self.db.get().await?;
|
let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
|
||||||
let wallet = schema::evm_wallet::table
|
let wallet = schema::evm_wallet::table
|
||||||
.select(models::EvmWallet::as_select())
|
.select(models::EvmWallet::as_select())
|
||||||
.filter(schema::evm_wallet::address.eq(wallet_address.as_slice()))
|
.filter(schema::evm_wallet::address.eq(wallet_address.as_slice()))
|
||||||
.first(&mut conn)
|
.first(&mut conn)
|
||||||
.await
|
.await
|
||||||
.optional()?
|
.optional()
|
||||||
|
.map_err(DatabaseError::from)?
|
||||||
|
.ok_or(SignTransactionError::WalletNotFound)?;
|
||||||
|
let wallet_access = schema::evm_wallet_access::table
|
||||||
|
.select(models::EvmWalletAccess::as_select())
|
||||||
|
.filter(schema::evm_wallet_access::wallet_id.eq(wallet.id))
|
||||||
|
.filter(schema::evm_wallet_access::client_id.eq(client_id))
|
||||||
|
.first(&mut conn)
|
||||||
|
.await
|
||||||
|
.optional()
|
||||||
|
.map_err(DatabaseError::from)?
|
||||||
.ok_or(SignTransactionError::WalletNotFound)?;
|
.ok_or(SignTransactionError::WalletNotFound)?;
|
||||||
drop(conn);
|
drop(conn);
|
||||||
|
|
||||||
let meaning = self
|
let meaning = self
|
||||||
.engine
|
.engine
|
||||||
.evaluate_transaction(
|
.evaluate_transaction(wallet_access, transaction.clone(), RunKind::Execution)
|
||||||
wallet.id,
|
|
||||||
client_id,
|
|
||||||
transaction.clone(),
|
|
||||||
RunKind::Execution,
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(meaning)
|
Ok(meaning)
|
||||||
@@ -235,13 +225,23 @@ impl EvmActor {
|
|||||||
wallet_address: Address,
|
wallet_address: Address,
|
||||||
mut transaction: TxEip1559,
|
mut transaction: TxEip1559,
|
||||||
) -> Result<Signature, SignTransactionError> {
|
) -> Result<Signature, SignTransactionError> {
|
||||||
let mut conn = self.db.get().await?;
|
let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
|
||||||
let wallet = schema::evm_wallet::table
|
let wallet = schema::evm_wallet::table
|
||||||
.select(models::EvmWallet::as_select())
|
.select(models::EvmWallet::as_select())
|
||||||
.filter(schema::evm_wallet::address.eq(wallet_address.as_slice()))
|
.filter(schema::evm_wallet::address.eq(wallet_address.as_slice()))
|
||||||
.first(&mut conn)
|
.first(&mut conn)
|
||||||
.await
|
.await
|
||||||
.optional()?
|
.optional()
|
||||||
|
.map_err(DatabaseError::from)?
|
||||||
|
.ok_or(SignTransactionError::WalletNotFound)?;
|
||||||
|
let wallet_access = schema::evm_wallet_access::table
|
||||||
|
.select(models::EvmWalletAccess::as_select())
|
||||||
|
.filter(schema::evm_wallet_access::wallet_id.eq(wallet.id))
|
||||||
|
.filter(schema::evm_wallet_access::client_id.eq(client_id))
|
||||||
|
.first(&mut conn)
|
||||||
|
.await
|
||||||
|
.optional()
|
||||||
|
.map_err(DatabaseError::from)?
|
||||||
.ok_or(SignTransactionError::WalletNotFound)?;
|
.ok_or(SignTransactionError::WalletNotFound)?;
|
||||||
drop(conn);
|
drop(conn);
|
||||||
|
|
||||||
@@ -256,12 +256,7 @@ impl EvmActor {
|
|||||||
let signer = safe_signer::SafeSigner::from_cell(raw_key)?;
|
let signer = safe_signer::SafeSigner::from_cell(raw_key)?;
|
||||||
|
|
||||||
self.engine
|
self.engine
|
||||||
.evaluate_transaction(
|
.evaluate_transaction(wallet_access, transaction.clone(), RunKind::Execution)
|
||||||
wallet.id,
|
|
||||||
client_id,
|
|
||||||
transaction.clone(),
|
|
||||||
RunKind::Execution,
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
use alloy::network::TxSignerSync as _;
|
use alloy::network::TxSignerSync as _;
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
use std::ops::ControlFlow;
|
||||||
|
|
||||||
|
use kameo::{
|
||||||
|
Actor, messages,
|
||||||
|
prelude::{ActorId, ActorRef, ActorStopReason, Context, WeakActorRef},
|
||||||
|
reply::ReplySender,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::actors::{
|
||||||
|
client::ClientProfile,
|
||||||
|
flow_coordinator::ApprovalError,
|
||||||
|
user_agent::{UserAgentSession, session::BeginNewClientApproval},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct Args {
|
||||||
|
pub client: ClientProfile,
|
||||||
|
pub user_agents: Vec<ActorRef<UserAgentSession>>,
|
||||||
|
pub reply: ReplySender<Result<bool, ApprovalError>>
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ClientApprovalController {
|
||||||
|
/// Number of UAs that have not yet responded (approval or denial) or died.
|
||||||
|
pending: usize,
|
||||||
|
/// Number of approvals received so far.
|
||||||
|
approved: usize,
|
||||||
|
reply: Option<ReplySender<Result<bool, ApprovalError>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClientApprovalController {
|
||||||
|
fn send_reply(&mut self, result: Result<bool, ApprovalError>) {
|
||||||
|
if let Some(reply) = self.reply.take() {
|
||||||
|
reply.send(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Actor for ClientApprovalController {
|
||||||
|
type Args = Args;
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
async fn on_start(
|
||||||
|
Args { client, mut user_agents, reply }: Self::Args,
|
||||||
|
actor_ref: ActorRef<Self>,
|
||||||
|
) -> Result<Self, Self::Error> {
|
||||||
|
let this = Self {
|
||||||
|
pending: user_agents.len(),
|
||||||
|
approved: 0,
|
||||||
|
reply: Some(reply),
|
||||||
|
};
|
||||||
|
|
||||||
|
for user_agent in user_agents.drain(..) {
|
||||||
|
actor_ref.link(&user_agent).await;
|
||||||
|
let _ = user_agent
|
||||||
|
.tell(BeginNewClientApproval {
|
||||||
|
client: client.clone(),
|
||||||
|
controller: actor_ref.clone(),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn on_link_died(
|
||||||
|
&mut self,
|
||||||
|
_: WeakActorRef<Self>,
|
||||||
|
_: ActorId,
|
||||||
|
_: ActorStopReason,
|
||||||
|
) -> Result<ControlFlow<ActorStopReason>, Self::Error> {
|
||||||
|
// A linked UA died before responding — counts as a non-approval.
|
||||||
|
self.pending = self.pending.saturating_sub(1);
|
||||||
|
if self.pending == 0 {
|
||||||
|
// At least one UA didn't approve: deny.
|
||||||
|
self.send_reply(Ok(false));
|
||||||
|
return Ok(ControlFlow::Break(ActorStopReason::Normal));
|
||||||
|
}
|
||||||
|
Ok(ControlFlow::Continue(()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[messages]
|
||||||
|
impl ClientApprovalController {
|
||||||
|
#[message(ctx)]
|
||||||
|
pub async fn client_approval_answer(&mut self, approved: bool, ctx: &mut Context<Self, ()>) {
|
||||||
|
if !approved {
|
||||||
|
// Denial wins immediately regardless of other pending responses.
|
||||||
|
self.send_reply(Ok(false));
|
||||||
|
ctx.stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.approved += 1;
|
||||||
|
self.pending = self.pending.saturating_sub(1);
|
||||||
|
|
||||||
|
if self.pending == 0 {
|
||||||
|
// Every connected UA approved.
|
||||||
|
self.send_reply(Ok(true));
|
||||||
|
ctx.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
118
server/crates/arbiter-server/src/actors/flow_coordinator/mod.rs
Normal file
118
server/crates/arbiter-server/src/actors/flow_coordinator/mod.rs
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
use std::{collections::HashMap, ops::ControlFlow};
|
||||||
|
|
||||||
|
use kameo::{
|
||||||
|
Actor,
|
||||||
|
actor::{ActorId, ActorRef, Spawn},
|
||||||
|
messages,
|
||||||
|
prelude::{ActorStopReason, Context, WeakActorRef},
|
||||||
|
reply::DelegatedReply,
|
||||||
|
};
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
use crate::actors::{
|
||||||
|
client::{ClientProfile, session::ClientSession},
|
||||||
|
flow_coordinator::client_connect_approval::ClientApprovalController,
|
||||||
|
user_agent::session::UserAgentSession,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod client_connect_approval;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct FlowCoordinator {
|
||||||
|
pub user_agents: HashMap<ActorId, ActorRef<UserAgentSession>>,
|
||||||
|
pub clients: HashMap<ActorId, ActorRef<ClientSession>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Actor for FlowCoordinator {
|
||||||
|
type Args = Self;
|
||||||
|
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
async fn on_start(args: Self::Args, _: ActorRef<Self>) -> Result<Self, Self::Error> {
|
||||||
|
Ok(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn on_link_died(
|
||||||
|
&mut self,
|
||||||
|
_: WeakActorRef<Self>,
|
||||||
|
id: ActorId,
|
||||||
|
_: ActorStopReason,
|
||||||
|
) -> Result<ControlFlow<ActorStopReason>, Self::Error> {
|
||||||
|
if self.user_agents.remove(&id).is_some() {
|
||||||
|
info!(
|
||||||
|
?id,
|
||||||
|
actor = "FlowCoordinator",
|
||||||
|
event = "useragent.disconnected"
|
||||||
|
);
|
||||||
|
} else if self.clients.remove(&id).is_some() {
|
||||||
|
info!(
|
||||||
|
?id,
|
||||||
|
actor = "FlowCoordinator",
|
||||||
|
event = "client.disconnected"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
info!(
|
||||||
|
?id,
|
||||||
|
actor = "FlowCoordinator",
|
||||||
|
event = "unknown.actor.disconnected"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(ControlFlow::Continue(()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub enum ApprovalError {
|
||||||
|
#[error("No user agents connected")]
|
||||||
|
NoUserAgentsConnected,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[messages]
|
||||||
|
impl FlowCoordinator {
|
||||||
|
#[message(ctx)]
|
||||||
|
pub async fn register_user_agent(
|
||||||
|
&mut self,
|
||||||
|
actor: ActorRef<UserAgentSession>,
|
||||||
|
ctx: &mut Context<Self, ()>,
|
||||||
|
) {
|
||||||
|
info!(id = %actor.id(), actor = "FlowCoordinator", event = "useragent.connected");
|
||||||
|
ctx.actor_ref().link(&actor).await;
|
||||||
|
self.user_agents.insert(actor.id(), actor);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[message(ctx)]
|
||||||
|
pub async fn register_client(
|
||||||
|
&mut self,
|
||||||
|
actor: ActorRef<ClientSession>,
|
||||||
|
ctx: &mut Context<Self, ()>,
|
||||||
|
) {
|
||||||
|
info!(id = %actor.id(), actor = "FlowCoordinator", event = "client.connected");
|
||||||
|
ctx.actor_ref().link(&actor).await;
|
||||||
|
self.clients.insert(actor.id(), actor);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[message(ctx)]
|
||||||
|
pub async fn request_client_approval(
|
||||||
|
&mut self,
|
||||||
|
client: ClientProfile,
|
||||||
|
ctx: &mut Context<Self, DelegatedReply<Result<bool, ApprovalError>>>,
|
||||||
|
) -> DelegatedReply<Result<bool, ApprovalError>> {
|
||||||
|
let (reply, Some(reply_sender)) = ctx.reply_sender() else {
|
||||||
|
unreachable!("Expected `request_client_approval` to have callback channel");
|
||||||
|
};
|
||||||
|
|
||||||
|
let refs: Vec<_> = self.user_agents.values().cloned().collect();
|
||||||
|
if refs.is_empty() {
|
||||||
|
reply_sender.send(Err(ApprovalError::NoUserAgentsConnected));
|
||||||
|
return reply;
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientApprovalController::spawn(client_connect_approval::Args {
|
||||||
|
client,
|
||||||
|
user_agents: refs,
|
||||||
|
reply: reply_sender,
|
||||||
|
});
|
||||||
|
|
||||||
|
reply
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ use encryption::v1::{self, KeyCell, Nonce};
|
|||||||
pub mod encryption;
|
pub mod encryption;
|
||||||
|
|
||||||
#[derive(Default, EnumDiscriminants)]
|
#[derive(Default, EnumDiscriminants)]
|
||||||
#[strum_discriminants(derive(Reply), vis(pub))]
|
#[strum_discriminants(derive(Reply), vis(pub), name(KeyHolderState))]
|
||||||
enum State {
|
enum State {
|
||||||
#[default]
|
#[default]
|
||||||
Unbootstrapped,
|
Unbootstrapped,
|
||||||
@@ -325,7 +325,7 @@ impl KeyHolder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[message]
|
#[message]
|
||||||
pub fn get_state(&self) -> StateDiscriminants {
|
pub fn get_state(&self) -> KeyHolderState {
|
||||||
self.state.discriminant()
|
self.state.discriminant()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,15 +3,18 @@ use miette::Diagnostic;
|
|||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
actors::{bootstrap::Bootstrapper, evm::EvmActor, keyholder::KeyHolder, router::MessageRouter},
|
actors::{
|
||||||
|
bootstrap::Bootstrapper, evm::EvmActor, flow_coordinator::FlowCoordinator,
|
||||||
|
keyholder::KeyHolder,
|
||||||
|
},
|
||||||
db,
|
db,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub mod bootstrap;
|
pub mod bootstrap;
|
||||||
pub mod client;
|
pub mod client;
|
||||||
mod evm;
|
mod evm;
|
||||||
|
pub mod flow_coordinator;
|
||||||
pub mod keyholder;
|
pub mod keyholder;
|
||||||
pub mod router;
|
|
||||||
pub mod user_agent;
|
pub mod user_agent;
|
||||||
|
|
||||||
#[derive(Error, Debug, Diagnostic)]
|
#[derive(Error, Debug, Diagnostic)]
|
||||||
@@ -30,7 +33,7 @@ pub enum SpawnError {
|
|||||||
pub struct GlobalActors {
|
pub struct GlobalActors {
|
||||||
pub key_holder: ActorRef<KeyHolder>,
|
pub key_holder: ActorRef<KeyHolder>,
|
||||||
pub bootstrapper: ActorRef<Bootstrapper>,
|
pub bootstrapper: ActorRef<Bootstrapper>,
|
||||||
pub router: ActorRef<MessageRouter>,
|
pub flow_coordinator: ActorRef<FlowCoordinator>,
|
||||||
pub evm: ActorRef<EvmActor>,
|
pub evm: ActorRef<EvmActor>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +44,7 @@ impl GlobalActors {
|
|||||||
bootstrapper: Bootstrapper::spawn(Bootstrapper::new(&db).await?),
|
bootstrapper: Bootstrapper::spawn(Bootstrapper::new(&db).await?),
|
||||||
evm: EvmActor::spawn(EvmActor::new(key_holder.clone(), db)),
|
evm: EvmActor::spawn(EvmActor::new(key_holder.clone(), db)),
|
||||||
key_holder,
|
key_holder,
|
||||||
router: MessageRouter::spawn(MessageRouter::default()),
|
flow_coordinator: FlowCoordinator::spawn(FlowCoordinator::default()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,174 +0,0 @@
|
|||||||
use std::{collections::HashMap, ops::ControlFlow};
|
|
||||||
|
|
||||||
use ed25519_dalek::VerifyingKey;
|
|
||||||
use kameo::{
|
|
||||||
Actor,
|
|
||||||
actor::{ActorId, ActorRef},
|
|
||||||
messages,
|
|
||||||
prelude::{ActorStopReason, Context, WeakActorRef},
|
|
||||||
reply::DelegatedReply,
|
|
||||||
};
|
|
||||||
use tokio::{sync::watch, task::JoinSet};
|
|
||||||
use tracing::{info, warn};
|
|
||||||
|
|
||||||
use crate::actors::{
|
|
||||||
client::session::ClientSession,
|
|
||||||
user_agent::session::{RequestNewClientApproval, UserAgentSession},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct MessageRouter {
|
|
||||||
pub user_agents: HashMap<ActorId, ActorRef<UserAgentSession>>,
|
|
||||||
pub clients: HashMap<ActorId, ActorRef<ClientSession>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Actor for MessageRouter {
|
|
||||||
type Args = Self;
|
|
||||||
|
|
||||||
type Error = ();
|
|
||||||
|
|
||||||
async fn on_start(args: Self::Args, _: ActorRef<Self>) -> Result<Self, Self::Error> {
|
|
||||||
Ok(args)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn on_link_died(
|
|
||||||
&mut self,
|
|
||||||
_: WeakActorRef<Self>,
|
|
||||||
id: ActorId,
|
|
||||||
_: ActorStopReason,
|
|
||||||
) -> Result<ControlFlow<ActorStopReason>, Self::Error> {
|
|
||||||
if self.user_agents.remove(&id).is_some() {
|
|
||||||
info!(
|
|
||||||
?id,
|
|
||||||
actor = "MessageRouter",
|
|
||||||
event = "useragent.disconnected"
|
|
||||||
);
|
|
||||||
} else if self.clients.remove(&id).is_some() {
|
|
||||||
info!(?id, actor = "MessageRouter", event = "client.disconnected");
|
|
||||||
} else {
|
|
||||||
info!(
|
|
||||||
?id,
|
|
||||||
actor = "MessageRouter",
|
|
||||||
event = "unknown.actor.disconnected"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Ok(ControlFlow::Continue(()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq, Hash)]
|
|
||||||
pub enum ApprovalError {
|
|
||||||
#[error("No user agents connected")]
|
|
||||||
NoUserAgentsConnected,
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn request_client_approval(
|
|
||||||
user_agents: &[WeakActorRef<UserAgentSession>],
|
|
||||||
client_pubkey: VerifyingKey,
|
|
||||||
) -> Result<bool, ApprovalError> {
|
|
||||||
if user_agents.is_empty() {
|
|
||||||
return Err(ApprovalError::NoUserAgentsConnected);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut pool = JoinSet::new();
|
|
||||||
let (cancel_tx, cancel_rx) = watch::channel(());
|
|
||||||
|
|
||||||
for weak_ref in user_agents {
|
|
||||||
match weak_ref.upgrade() {
|
|
||||||
Some(agent) => {
|
|
||||||
let cancel_rx = cancel_rx.clone();
|
|
||||||
pool.spawn(async move {
|
|
||||||
agent
|
|
||||||
.ask(RequestNewClientApproval {
|
|
||||||
client_pubkey,
|
|
||||||
cancel_flag: cancel_rx.clone(),
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
});
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
warn!(
|
|
||||||
id = weak_ref.id().to_string(),
|
|
||||||
actor = "MessageRouter",
|
|
||||||
event = "useragent.disconnected_before_approval"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
while let Some(result) = pool.join_next().await {
|
|
||||||
match result {
|
|
||||||
Ok(Ok(approved)) => {
|
|
||||||
// cancel other pending requests
|
|
||||||
let _ = cancel_tx.send(());
|
|
||||||
return Ok(approved);
|
|
||||||
}
|
|
||||||
Ok(Err(err)) => {
|
|
||||||
warn!(
|
|
||||||
?err,
|
|
||||||
actor = "MessageRouter",
|
|
||||||
event = "useragent.approval_error"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
warn!(
|
|
||||||
?err,
|
|
||||||
actor = "MessageRouter",
|
|
||||||
event = "useragent.approval_task_failed"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(ApprovalError::NoUserAgentsConnected)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[messages]
|
|
||||||
impl MessageRouter {
|
|
||||||
#[message(ctx)]
|
|
||||||
pub async fn register_user_agent(
|
|
||||||
&mut self,
|
|
||||||
actor: ActorRef<UserAgentSession>,
|
|
||||||
ctx: &mut Context<Self, ()>,
|
|
||||||
) {
|
|
||||||
info!(id = %actor.id(), actor = "MessageRouter", event = "useragent.connected");
|
|
||||||
ctx.actor_ref().link(&actor).await;
|
|
||||||
self.user_agents.insert(actor.id(), actor);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[message(ctx)]
|
|
||||||
pub async fn register_client(
|
|
||||||
&mut self,
|
|
||||||
actor: ActorRef<ClientSession>,
|
|
||||||
ctx: &mut Context<Self, ()>,
|
|
||||||
) {
|
|
||||||
info!(id = %actor.id(), actor = "MessageRouter", event = "client.connected");
|
|
||||||
ctx.actor_ref().link(&actor).await;
|
|
||||||
self.clients.insert(actor.id(), actor);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[message(ctx)]
|
|
||||||
pub async fn request_client_approval(
|
|
||||||
&mut self,
|
|
||||||
client_pubkey: VerifyingKey,
|
|
||||||
ctx: &mut Context<Self, DelegatedReply<Result<bool, ApprovalError>>>,
|
|
||||||
) -> DelegatedReply<Result<bool, ApprovalError>> {
|
|
||||||
let (reply, Some(reply_sender)) = ctx.reply_sender() else {
|
|
||||||
unreachable!("Expected `request_client_approval` to have callback channel");
|
|
||||||
};
|
|
||||||
|
|
||||||
let weak_refs = self
|
|
||||||
.user_agents
|
|
||||||
.values()
|
|
||||||
.map(|agent| agent.downgrade())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
// handle in subtask to not to lock the actor
|
|
||||||
tokio::task::spawn(async move {
|
|
||||||
let result = request_client_approval(&weak_refs, client_pubkey).await;
|
|
||||||
reply_sender.send(result);
|
|
||||||
});
|
|
||||||
|
|
||||||
reply
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,74 +1,82 @@
|
|||||||
|
use arbiter_proto::transport::Bi;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
use crate::actors::user_agent::{
|
use crate::actors::user_agent::{
|
||||||
Request, UserAgentConnection,
|
AuthPublicKey, UserAgentConnection,
|
||||||
auth::state::{AuthContext, AuthStateMachine},
|
auth::state::{AuthContext, AuthStateMachine},
|
||||||
AuthPublicKey,
|
|
||||||
session::UserAgentSession,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug, PartialEq)]
|
|
||||||
pub enum Error {
|
|
||||||
#[error("Unexpected message payload")]
|
|
||||||
UnexpectedMessagePayload,
|
|
||||||
#[error("Invalid client public key length")]
|
|
||||||
InvalidClientPubkeyLength,
|
|
||||||
#[error("Invalid client public key encoding")]
|
|
||||||
InvalidAuthPubkeyEncoding,
|
|
||||||
#[error("Database pool unavailable")]
|
|
||||||
DatabasePoolUnavailable,
|
|
||||||
#[error("Database operation failed")]
|
|
||||||
DatabaseOperationFailed,
|
|
||||||
#[error("Public key not registered")]
|
|
||||||
PublicKeyNotRegistered,
|
|
||||||
#[error("Transport error")]
|
|
||||||
Transport,
|
|
||||||
#[error("Invalid bootstrap token")]
|
|
||||||
InvalidBootstrapToken,
|
|
||||||
#[error("Bootstrapper actor unreachable")]
|
|
||||||
BootstrapperActorUnreachable,
|
|
||||||
#[error("Invalid challenge solution")]
|
|
||||||
InvalidChallengeSolution,
|
|
||||||
}
|
|
||||||
|
|
||||||
mod state;
|
mod state;
|
||||||
use state::*;
|
use state::*;
|
||||||
|
|
||||||
fn parse_auth_event(payload: Request) -> Result<AuthEvents, Error> {
|
#[derive(Debug, Clone)]
|
||||||
match payload {
|
pub enum Inbound {
|
||||||
Request::AuthChallengeRequest {
|
AuthChallengeRequest {
|
||||||
pubkey,
|
pubkey: AuthPublicKey,
|
||||||
bootstrap_token: None,
|
bootstrap_token: Option<String>,
|
||||||
} => Ok(AuthEvents::AuthRequest(ChallengeRequest { pubkey })),
|
},
|
||||||
Request::AuthChallengeRequest {
|
AuthChallengeSolution {
|
||||||
pubkey,
|
signature: Vec<u8>,
|
||||||
bootstrap_token: Some(token),
|
},
|
||||||
} => Ok(AuthEvents::BootstrapAuthRequest(BootstrapAuthRequest {
|
}
|
||||||
pubkey,
|
|
||||||
token,
|
#[derive(Debug)]
|
||||||
})),
|
pub enum Error {
|
||||||
Request::AuthChallengeSolution { signature } => {
|
UnregisteredPublicKey,
|
||||||
Ok(AuthEvents::ReceivedSolution(ChallengeSolution {
|
InvalidChallengeSolution,
|
||||||
solution: signature,
|
InvalidBootstrapToken,
|
||||||
}))
|
Internal { details: String },
|
||||||
|
Transport,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error {
|
||||||
|
fn internal(details: impl Into<String>) -> Self {
|
||||||
|
Self::Internal {
|
||||||
|
details: details.into(),
|
||||||
}
|
}
|
||||||
_ => Err(Error::UnexpectedMessagePayload),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn authenticate(props: &mut UserAgentConnection) -> Result<AuthPublicKey, Error> {
|
#[derive(Debug, Clone)]
|
||||||
let mut state = AuthStateMachine::new(AuthContext::new(props));
|
pub enum Outbound {
|
||||||
|
AuthChallenge { nonce: i32 },
|
||||||
|
AuthSuccess,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_auth_event(payload: Inbound) -> AuthEvents {
|
||||||
|
match payload {
|
||||||
|
Inbound::AuthChallengeRequest {
|
||||||
|
pubkey,
|
||||||
|
bootstrap_token: None,
|
||||||
|
} => AuthEvents::AuthRequest(ChallengeRequest { pubkey }),
|
||||||
|
Inbound::AuthChallengeRequest {
|
||||||
|
pubkey,
|
||||||
|
bootstrap_token: Some(token),
|
||||||
|
} => AuthEvents::BootstrapAuthRequest(BootstrapAuthRequest { pubkey, token }),
|
||||||
|
Inbound::AuthChallengeSolution { signature } => {
|
||||||
|
AuthEvents::ReceivedSolution(ChallengeSolution {
|
||||||
|
solution: signature,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn authenticate<T>(
|
||||||
|
props: &mut UserAgentConnection,
|
||||||
|
transport: T,
|
||||||
|
) -> Result<AuthPublicKey, Error>
|
||||||
|
where
|
||||||
|
T: Bi<Inbound, Result<Outbound, Error>> + Send,
|
||||||
|
{
|
||||||
|
let mut state = AuthStateMachine::new(AuthContext::new(props, transport));
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
// `state` holds a mutable reference to `props` so we can't access it directly here
|
// `state` holds a mutable reference to `props` so we can't access it directly here
|
||||||
let transport = state.context_mut().conn.transport.as_mut();
|
let Some(payload) = state.context_mut().transport.recv().await else {
|
||||||
let Some(payload) = transport.recv().await else {
|
|
||||||
return Err(Error::Transport);
|
return Err(Error::Transport);
|
||||||
};
|
};
|
||||||
|
|
||||||
let event = parse_auth_event(payload)?;
|
match state.process_event(parse_auth_event(payload)).await {
|
||||||
|
|
||||||
match state.process_event(event).await {
|
|
||||||
Ok(AuthStates::AuthOk(key)) => return Ok(key.clone()),
|
Ok(AuthStates::AuthOk(key)) => return Ok(key.clone()),
|
||||||
Err(AuthError::ActionFailed(err)) => {
|
Err(AuthError::ActionFailed(err)) => {
|
||||||
error!(?err, "State machine action failed");
|
error!(?err, "State machine action failed");
|
||||||
@@ -91,11 +99,3 @@ pub async fn authenticate(props: &mut UserAgentConnection) -> Result<AuthPublicK
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn authenticate_and_create(
|
|
||||||
mut props: UserAgentConnection,
|
|
||||||
) -> Result<UserAgentSession, Error> {
|
|
||||||
let _key = authenticate(&mut props).await?;
|
|
||||||
let session = UserAgentSession::new(props);
|
|
||||||
Ok(session)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use arbiter_proto::transport::Bi;
|
||||||
use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl, update};
|
use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl, update};
|
||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::RunQueryDsl;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
@@ -6,7 +7,7 @@ use super::Error;
|
|||||||
use crate::{
|
use crate::{
|
||||||
actors::{
|
actors::{
|
||||||
bootstrap::ConsumeToken,
|
bootstrap::ConsumeToken,
|
||||||
user_agent::{AuthPublicKey, Response, UserAgentConnection},
|
user_agent::{AuthPublicKey, UserAgentConnection, auth::Outbound},
|
||||||
},
|
},
|
||||||
db::schema,
|
db::schema,
|
||||||
};
|
};
|
||||||
@@ -42,7 +43,7 @@ smlang::statemachine!(
|
|||||||
async fn create_nonce(db: &crate::db::DatabasePool, pubkey_bytes: &[u8]) -> Result<i32, Error> {
|
async fn create_nonce(db: &crate::db::DatabasePool, pubkey_bytes: &[u8]) -> Result<i32, Error> {
|
||||||
let mut db_conn = db.get().await.map_err(|e| {
|
let mut db_conn = db.get().await.map_err(|e| {
|
||||||
error!(error = ?e, "Database pool error");
|
error!(error = ?e, "Database pool error");
|
||||||
Error::DatabasePoolUnavailable
|
Error::internal("Database unavailable")
|
||||||
})?;
|
})?;
|
||||||
db_conn
|
db_conn
|
||||||
.exclusive_transaction(|conn| {
|
.exclusive_transaction(|conn| {
|
||||||
@@ -66,11 +67,11 @@ async fn create_nonce(db: &crate::db::DatabasePool, pubkey_bytes: &[u8]) -> Resu
|
|||||||
.optional()
|
.optional()
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
error!(error = ?e, "Database error");
|
error!(error = ?e, "Database error");
|
||||||
Error::DatabaseOperationFailed
|
Error::internal("Database operation failed")
|
||||||
})?
|
})?
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
error!(?pubkey_bytes, "Public key not found in database");
|
error!(?pubkey_bytes, "Public key not found in database");
|
||||||
Error::PublicKeyNotRegistered
|
Error::UnregisteredPublicKey
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +80,7 @@ async fn register_key(db: &crate::db::DatabasePool, pubkey: &AuthPublicKey) -> R
|
|||||||
let key_type = pubkey.key_type();
|
let key_type = pubkey.key_type();
|
||||||
let mut conn = db.get().await.map_err(|e| {
|
let mut conn = db.get().await.map_err(|e| {
|
||||||
error!(error = ?e, "Database pool error");
|
error!(error = ?e, "Database pool error");
|
||||||
Error::DatabasePoolUnavailable
|
Error::internal("Database unavailable")
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
diesel::insert_into(schema::useragent_client::table)
|
diesel::insert_into(schema::useragent_client::table)
|
||||||
@@ -92,23 +93,27 @@ async fn register_key(db: &crate::db::DatabasePool, pubkey: &AuthPublicKey) -> R
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
error!(error = ?e, "Database error");
|
error!(error = ?e, "Database error");
|
||||||
Error::DatabaseOperationFailed
|
Error::internal("Database operation failed")
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AuthContext<'a> {
|
pub struct AuthContext<'a, T> {
|
||||||
pub(super) conn: &'a mut UserAgentConnection,
|
pub(super) conn: &'a mut UserAgentConnection,
|
||||||
|
pub(super) transport: T,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> AuthContext<'a> {
|
impl<'a, T> AuthContext<'a, T> {
|
||||||
pub fn new(conn: &'a mut UserAgentConnection) -> Self {
|
pub fn new(conn: &'a mut UserAgentConnection, transport: T) -> Self {
|
||||||
Self { conn }
|
Self { conn, transport }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AuthStateMachineContext for AuthContext<'_> {
|
impl<T> AuthStateMachineContext for AuthContext<'_, T>
|
||||||
|
where
|
||||||
|
T: Bi<super::Inbound, Result<super::Outbound, Error>> + Send,
|
||||||
|
{
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
async fn prepare_challenge(
|
async fn prepare_challenge(
|
||||||
@@ -118,9 +123,8 @@ impl AuthStateMachineContext for AuthContext<'_> {
|
|||||||
let stored_bytes = pubkey.to_stored_bytes();
|
let stored_bytes = pubkey.to_stored_bytes();
|
||||||
let nonce = create_nonce(&self.conn.db, &stored_bytes).await?;
|
let nonce = create_nonce(&self.conn.db, &stored_bytes).await?;
|
||||||
|
|
||||||
self.conn
|
self.transport
|
||||||
.transport
|
.send(Ok(Outbound::AuthChallenge { nonce }))
|
||||||
.send(Ok(Response::AuthChallenge { nonce }))
|
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
error!(?e, "Failed to send auth challenge");
|
error!(?e, "Failed to send auth challenge");
|
||||||
@@ -149,7 +153,7 @@ impl AuthStateMachineContext for AuthContext<'_> {
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
error!(?e, "Failed to consume bootstrap token");
|
error!(?e, "Failed to consume bootstrap token");
|
||||||
Error::BootstrapperActorUnreachable
|
Error::internal("Failed to consume bootstrap token")
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if !token_ok {
|
if !token_ok {
|
||||||
@@ -159,9 +163,8 @@ impl AuthStateMachineContext for AuthContext<'_> {
|
|||||||
|
|
||||||
register_key(&self.conn.db, &pubkey).await?;
|
register_key(&self.conn.db, &pubkey).await?;
|
||||||
|
|
||||||
self.conn
|
self.transport
|
||||||
.transport
|
.send(Ok(Outbound::AuthSuccess))
|
||||||
.send(Ok(Response::AuthOk))
|
|
||||||
.await
|
.await
|
||||||
.map_err(|_| Error::Transport)?;
|
.map_err(|_| Error::Transport)?;
|
||||||
|
|
||||||
@@ -172,7 +175,10 @@ impl AuthStateMachineContext for AuthContext<'_> {
|
|||||||
#[allow(clippy::unused_unit)]
|
#[allow(clippy::unused_unit)]
|
||||||
async fn verify_solution(
|
async fn verify_solution(
|
||||||
&mut self,
|
&mut self,
|
||||||
ChallengeContext { challenge_nonce, key }: &ChallengeContext,
|
ChallengeContext {
|
||||||
|
challenge_nonce,
|
||||||
|
key,
|
||||||
|
}: &ChallengeContext,
|
||||||
ChallengeSolution { solution }: ChallengeSolution,
|
ChallengeSolution { solution }: ChallengeSolution,
|
||||||
) -> Result<AuthPublicKey, Self::Error> {
|
) -> Result<AuthPublicKey, Self::Error> {
|
||||||
let formatted = arbiter_proto::format_challenge(*challenge_nonce, &key.to_stored_bytes());
|
let formatted = arbiter_proto::format_challenge(*challenge_nonce, &key.to_stored_bytes());
|
||||||
@@ -205,9 +211,8 @@ impl AuthStateMachineContext for AuthContext<'_> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if valid {
|
if valid {
|
||||||
self.conn
|
self.transport
|
||||||
.transport
|
.send(Ok(Outbound::AuthSuccess))
|
||||||
.send(Ok(Response::AuthOk))
|
|
||||||
.await
|
.await
|
||||||
.map_err(|_| Error::Transport)?;
|
.map_err(|_| Error::Transport)?;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,8 @@
|
|||||||
use alloy::primitives::Address;
|
|
||||||
use arbiter_proto::transport::Bi;
|
|
||||||
use kameo::actor::Spawn as _;
|
|
||||||
use tracing::{error, info};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
actors::{GlobalActors, evm, user_agent::session::UserAgentSession},
|
actors::{GlobalActors, client::ClientProfile},
|
||||||
db::{self, models::KeyType},
|
db::{self, models::KeyType},
|
||||||
evm::policies::SharedGrantSettings,
|
|
||||||
evm::policies::{Grant, SpecificGrant},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error, PartialEq)]
|
|
||||||
pub enum TransportResponseError {
|
|
||||||
#[error("Unexpected request payload")]
|
|
||||||
UnexpectedRequestPayload,
|
|
||||||
#[error("Invalid state for unseal encrypted key")]
|
|
||||||
InvalidStateForUnsealEncryptedKey,
|
|
||||||
#[error("client_pubkey must be 32 bytes")]
|
|
||||||
InvalidClientPubkeyLength,
|
|
||||||
#[error("State machine error")]
|
|
||||||
StateTransitionFailed,
|
|
||||||
#[error("Vault is not available")]
|
|
||||||
KeyHolderActorUnreachable,
|
|
||||||
#[error(transparent)]
|
|
||||||
Auth(#[from] auth::Error),
|
|
||||||
#[error("Failed registering connection")]
|
|
||||||
ConnectionRegistrationFailed,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Abstraction over Ed25519 / ECDSA-secp256k1 / RSA public keys used during the auth handshake.
|
/// Abstraction over Ed25519 / ECDSA-secp256k1 / RSA public keys used during the auth handshake.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum AuthPublicKey {
|
pub enum AuthPublicKey {
|
||||||
@@ -65,119 +40,55 @@ impl AuthPublicKey {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
impl TryFrom<(KeyType, Vec<u8>)> for AuthPublicKey {
|
||||||
pub enum UnsealError {
|
type Error = &'static str;
|
||||||
InvalidKey,
|
|
||||||
Unbootstrapped,
|
fn try_from(value: (KeyType, Vec<u8>)) -> Result<Self, Self::Error> {
|
||||||
}
|
let (key_type, bytes) = value;
|
||||||
|
match key_type {
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
KeyType::Ed25519 => {
|
||||||
pub enum BootstrapError {
|
let bytes: [u8; 32] = bytes.try_into().map_err(|_| "invalid Ed25519 key length")?;
|
||||||
AlreadyBootstrapped,
|
let key = ed25519_dalek::VerifyingKey::from_bytes(&bytes)
|
||||||
InvalidKey,
|
.map_err(|_e| "invalid Ed25519 key")?;
|
||||||
}
|
Ok(AuthPublicKey::Ed25519(key))
|
||||||
|
}
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
KeyType::EcdsaSecp256k1 => {
|
||||||
pub enum VaultState {
|
let point =
|
||||||
Unbootstrapped,
|
k256::EncodedPoint::from_bytes(&bytes).map_err(|_e| "invalid ECDSA key")?;
|
||||||
Sealed,
|
let key = k256::ecdsa::VerifyingKey::from_encoded_point(&point)
|
||||||
Unsealed,
|
.map_err(|_e| "invalid ECDSA key")?;
|
||||||
}
|
Ok(AuthPublicKey::EcdsaSecp256k1(key))
|
||||||
|
}
|
||||||
#[derive(Debug, Clone)]
|
KeyType::Rsa => {
|
||||||
pub enum Request {
|
use rsa::pkcs8::DecodePublicKey as _;
|
||||||
AuthChallengeRequest {
|
let key = rsa::RsaPublicKey::from_public_key_der(&bytes)
|
||||||
pubkey: AuthPublicKey,
|
.map_err(|_e| "invalid RSA key")?;
|
||||||
bootstrap_token: Option<String>,
|
Ok(AuthPublicKey::Rsa(key))
|
||||||
},
|
}
|
||||||
AuthChallengeSolution {
|
}
|
||||||
signature: Vec<u8>,
|
}
|
||||||
},
|
|
||||||
UnsealStart {
|
|
||||||
client_pubkey: x25519_dalek::PublicKey,
|
|
||||||
},
|
|
||||||
UnsealEncryptedKey {
|
|
||||||
nonce: Vec<u8>,
|
|
||||||
ciphertext: Vec<u8>,
|
|
||||||
associated_data: Vec<u8>,
|
|
||||||
},
|
|
||||||
BootstrapEncryptedKey {
|
|
||||||
nonce: Vec<u8>,
|
|
||||||
ciphertext: Vec<u8>,
|
|
||||||
associated_data: Vec<u8>,
|
|
||||||
},
|
|
||||||
QueryVaultState,
|
|
||||||
EvmWalletCreate,
|
|
||||||
EvmWalletList,
|
|
||||||
ClientConnectionResponse {
|
|
||||||
approved: bool,
|
|
||||||
},
|
|
||||||
|
|
||||||
ListGrants,
|
|
||||||
EvmGrantCreate {
|
|
||||||
client_id: i32,
|
|
||||||
shared: SharedGrantSettings,
|
|
||||||
specific: SpecificGrant,
|
|
||||||
},
|
|
||||||
EvmGrantDelete {
|
|
||||||
grant_id: i32,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Messages, sent by user agent to connection client without having a request
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Response {
|
pub enum OutOfBand {
|
||||||
AuthChallenge {
|
ClientConnectionRequest { profile: ClientProfile },
|
||||||
nonce: i32,
|
ClientConnectionCancel { pubkey: ed25519_dalek::VerifyingKey },
|
||||||
},
|
|
||||||
AuthOk,
|
|
||||||
UnsealStartResponse {
|
|
||||||
server_pubkey: x25519_dalek::PublicKey,
|
|
||||||
},
|
|
||||||
UnsealResult(Result<(), UnsealError>),
|
|
||||||
BootstrapResult(Result<(), BootstrapError>),
|
|
||||||
VaultState(VaultState),
|
|
||||||
ClientConnectionRequest {
|
|
||||||
pubkey: ed25519_dalek::VerifyingKey,
|
|
||||||
},
|
|
||||||
ClientConnectionCancel,
|
|
||||||
EvmWalletCreate(Result<(), evm::Error>),
|
|
||||||
EvmWalletList(Vec<Address>),
|
|
||||||
|
|
||||||
ListGrants(Vec<Grant<SpecificGrant>>),
|
|
||||||
EvmGrantCreate(Result<i32, evm::Error>),
|
|
||||||
EvmGrantDelete(Result<(), evm::Error>),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Transport = Box<dyn Bi<Request, Result<Response, TransportResponseError>> + Send>;
|
|
||||||
|
|
||||||
pub struct UserAgentConnection {
|
pub struct UserAgentConnection {
|
||||||
db: db::DatabasePool,
|
pub(crate) db: db::DatabasePool,
|
||||||
actors: GlobalActors,
|
pub(crate) actors: GlobalActors,
|
||||||
transport: Transport,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UserAgentConnection {
|
impl UserAgentConnection {
|
||||||
pub fn new(db: db::DatabasePool, actors: GlobalActors, transport: Transport) -> Self {
|
pub fn new(db: db::DatabasePool, actors: GlobalActors) -> Self {
|
||||||
Self {
|
Self { db, actors }
|
||||||
db,
|
|
||||||
actors,
|
|
||||||
transport,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod session;
|
pub mod session;
|
||||||
|
|
||||||
#[tracing::instrument(skip(props))]
|
pub use auth::authenticate;
|
||||||
pub async fn connect_user_agent(props: UserAgentConnection) {
|
pub use session::UserAgentSession;
|
||||||
match auth::authenticate_and_create(props).await {
|
|
||||||
Ok(session) => {
|
|
||||||
UserAgentSession::spawn(session);
|
|
||||||
info!("User authenticated, session started");
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
error!(?err, "Authentication failed, closing connection");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,93 +1,95 @@
|
|||||||
|
use std::{borrow::Cow, collections::HashMap};
|
||||||
|
|
||||||
|
use arbiter_proto::transport::Sender;
|
||||||
|
use async_trait::async_trait;
|
||||||
use ed25519_dalek::VerifyingKey;
|
use ed25519_dalek::VerifyingKey;
|
||||||
use kameo::{Actor, messages, prelude::Context};
|
use kameo::{Actor, actor::ActorRef, messages};
|
||||||
use tokio::{select, sync::watch};
|
use thiserror::Error;
|
||||||
use tracing::{error, info};
|
use tracing::error;
|
||||||
|
|
||||||
use crate::actors::{
|
use crate::actors::{
|
||||||
router::RegisterUserAgent,
|
client::ClientProfile,
|
||||||
user_agent::{
|
flow_coordinator::{RegisterUserAgent, client_connect_approval::ClientApprovalController},
|
||||||
Request, Response, TransportResponseError,
|
user_agent::{OutOfBand, UserAgentConnection},
|
||||||
UserAgentConnection,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
mod state;
|
mod state;
|
||||||
use state::{DummyContext, UserAgentEvents, UserAgentStateMachine};
|
use state::{DummyContext, UserAgentEvents, UserAgentStateMachine};
|
||||||
|
|
||||||
// Error for consumption by other actors
|
#[derive(Debug, Error)]
|
||||||
#[derive(Debug, thiserror::Error, PartialEq)]
|
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("User agent session ended due to connection loss")]
|
#[error("State transition failed")]
|
||||||
ConnectionLost,
|
State,
|
||||||
|
|
||||||
#[error("User agent session ended due to unexpected message")]
|
#[error("Internal error: {message}")]
|
||||||
UnexpectedMessage,
|
Internal { message: Cow<'static, str> },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<crate::db::PoolError> for Error {
|
||||||
|
fn from(err: crate::db::PoolError) -> Self {
|
||||||
|
error!(?err, "Database pool error");
|
||||||
|
Self::internal("Database pool error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<diesel::result::Error> for Error {
|
||||||
|
fn from(err: diesel::result::Error) -> Self {
|
||||||
|
error!(?err, "Database error");
|
||||||
|
Self::internal("Database error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error {
|
||||||
|
pub fn internal(message: impl Into<Cow<'static, str>>) -> Self {
|
||||||
|
Self::Internal {
|
||||||
|
message: message.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PendingClientApproval {
|
||||||
|
controller: ActorRef<ClientApprovalController>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct UserAgentSession {
|
pub struct UserAgentSession {
|
||||||
props: UserAgentConnection,
|
props: UserAgentConnection,
|
||||||
state: UserAgentStateMachine<DummyContext>,
|
state: UserAgentStateMachine<DummyContext>,
|
||||||
|
sender: Box<dyn Sender<OutOfBand>>,
|
||||||
|
|
||||||
|
pending_client_approvals: HashMap<VerifyingKey, PendingClientApproval>,
|
||||||
}
|
}
|
||||||
|
|
||||||
mod connection;
|
pub mod connection;
|
||||||
|
|
||||||
impl UserAgentSession {
|
impl UserAgentSession {
|
||||||
pub(crate) fn new(props: UserAgentConnection) -> Self {
|
pub(crate) fn new(props: UserAgentConnection, sender: Box<dyn Sender<OutOfBand>>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
props,
|
props,
|
||||||
state: UserAgentStateMachine::new(DummyContext),
|
state: UserAgentStateMachine::new(DummyContext),
|
||||||
|
sender,
|
||||||
|
pending_client_approvals: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) async fn send_msg<Reply: kameo::Reply>(
|
pub fn new_test(db: crate::db::DatabasePool, actors: crate::actors::GlobalActors) -> Self {
|
||||||
|
struct DummySender;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Sender<OutOfBand> for DummySender {
|
||||||
|
async fn send(
|
||||||
&mut self,
|
&mut self,
|
||||||
msg: Response,
|
_item: OutOfBand,
|
||||||
_ctx: &mut Context<Self, Reply>,
|
) -> Result<(), arbiter_proto::transport::Error> {
|
||||||
) -> Result<(), Error> {
|
Ok(())
|
||||||
self.props.transport.send(Ok(msg)).await.map_err(|_| {
|
}
|
||||||
error!(
|
|
||||||
actor = "useragent",
|
|
||||||
reason = "channel closed",
|
|
||||||
"send.failed"
|
|
||||||
);
|
|
||||||
Error::ConnectionLost
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn expect_msg<Extractor, Msg, Reply>(
|
Self::new(UserAgentConnection::new(db, actors), Box::new(DummySender))
|
||||||
&mut self,
|
|
||||||
extractor: Extractor,
|
|
||||||
ctx: &mut Context<Self, Reply>,
|
|
||||||
) -> Result<Msg, Error>
|
|
||||||
where
|
|
||||||
Extractor: FnOnce(Request) -> Option<Msg>,
|
|
||||||
Reply: kameo::Reply,
|
|
||||||
{
|
|
||||||
let msg = self.props.transport.recv().await.ok_or_else(|| {
|
|
||||||
error!(
|
|
||||||
actor = "useragent",
|
|
||||||
reason = "channel closed",
|
|
||||||
"recv.failed"
|
|
||||||
);
|
|
||||||
ctx.stop();
|
|
||||||
Error::ConnectionLost
|
|
||||||
})?;
|
|
||||||
|
|
||||||
extractor(msg).ok_or_else(|| {
|
|
||||||
error!(
|
|
||||||
actor = "useragent",
|
|
||||||
reason = "unexpected message",
|
|
||||||
"recv.failed"
|
|
||||||
);
|
|
||||||
ctx.stop();
|
|
||||||
Error::UnexpectedMessage
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn transition(&mut self, event: UserAgentEvents) -> Result<(), TransportResponseError> {
|
fn transition(&mut self, event: UserAgentEvents) -> Result<(), Error> {
|
||||||
self.state.process_event(event).map_err(|e| {
|
self.state.process_event(event).map_err(|e| {
|
||||||
error!(?e, "State transition failed");
|
error!(?e, "State transition failed");
|
||||||
TransportResponseError::StateTransitionFailed
|
Error::State
|
||||||
})?;
|
})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -95,52 +97,36 @@ impl UserAgentSession {
|
|||||||
|
|
||||||
#[messages]
|
#[messages]
|
||||||
impl UserAgentSession {
|
impl UserAgentSession {
|
||||||
// TODO: Think about refactoring it to state-machine based flow, as we already have one
|
#[message]
|
||||||
#[message(ctx)]
|
pub async fn begin_new_client_approval(
|
||||||
pub async fn request_new_client_approval(
|
|
||||||
&mut self,
|
&mut self,
|
||||||
client_pubkey: VerifyingKey,
|
client: ClientProfile,
|
||||||
mut cancel_flag: watch::Receiver<()>,
|
controller: ActorRef<ClientApprovalController>,
|
||||||
ctx: &mut Context<Self, Result<bool, Error>>,
|
) {
|
||||||
) -> Result<bool, Error> {
|
if let Err(e) = self
|
||||||
self.send_msg(
|
.sender
|
||||||
Response::ClientConnectionRequest {
|
.send(OutOfBand::ClientConnectionRequest {
|
||||||
pubkey: client_pubkey,
|
profile: client.clone(),
|
||||||
},
|
})
|
||||||
ctx,
|
.await
|
||||||
)
|
{
|
||||||
.await?;
|
error!(
|
||||||
|
?e,
|
||||||
|
actor = "user_agent",
|
||||||
|
event = "failed to announce new client connection"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let extractor = |msg| {
|
self.pending_client_approvals
|
||||||
if let Request::ClientConnectionResponse { approved } = msg {
|
.insert(client.pubkey, PendingClientApproval { controller });
|
||||||
Some(approved)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
tokio::select! {
|
|
||||||
_ = cancel_flag.changed() => {
|
|
||||||
info!(actor = "useragent", "client connection approval cancelled");
|
|
||||||
self.send_msg(
|
|
||||||
Response::ClientConnectionCancel,
|
|
||||||
ctx,
|
|
||||||
).await?;
|
|
||||||
Ok(false)
|
|
||||||
}
|
|
||||||
result = self.expect_msg(extractor, ctx) => {
|
|
||||||
let result = result?;
|
|
||||||
info!(actor = "useragent", "received client connection approval result: approved={}", result);
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Actor for UserAgentSession {
|
impl Actor for UserAgentSession {
|
||||||
type Args = Self;
|
type Args = Self;
|
||||||
|
|
||||||
type Error = TransportResponseError;
|
type Error = Error;
|
||||||
|
|
||||||
async fn on_start(
|
async fn on_start(
|
||||||
args: Self::Args,
|
args: Self::Args,
|
||||||
@@ -148,63 +134,48 @@ impl Actor for UserAgentSession {
|
|||||||
) -> Result<Self, Self::Error> {
|
) -> Result<Self, Self::Error> {
|
||||||
args.props
|
args.props
|
||||||
.actors
|
.actors
|
||||||
.router
|
.flow_coordinator
|
||||||
.ask(RegisterUserAgent {
|
.ask(RegisterUserAgent {
|
||||||
actor: this.clone(),
|
actor: this.clone(),
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
error!(?err, "Failed to register user agent connection with router");
|
error!(
|
||||||
TransportResponseError::ConnectionRegistrationFailed
|
?err,
|
||||||
|
"Failed to register user agent connection with flow coordinator"
|
||||||
|
);
|
||||||
|
Error::internal("Failed to register user agent connection with flow coordinator")
|
||||||
})?;
|
})?;
|
||||||
Ok(args)
|
Ok(args)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn next(
|
async fn on_link_died(
|
||||||
&mut self,
|
&mut self,
|
||||||
_actor_ref: kameo::prelude::WeakActorRef<Self>,
|
_: kameo::prelude::WeakActorRef<Self>,
|
||||||
mailbox_rx: &mut kameo::prelude::MailboxReceiver<Self>,
|
id: kameo::prelude::ActorId,
|
||||||
) -> Option<kameo::mailbox::Signal<Self>> {
|
_: kameo::prelude::ActorStopReason,
|
||||||
loop {
|
) -> Result<std::ops::ControlFlow<kameo::prelude::ActorStopReason>, Self::Error> {
|
||||||
select! {
|
let cancelled_pubkey = self
|
||||||
signal = mailbox_rx.recv() => {
|
.pending_client_approvals
|
||||||
return signal;
|
.iter()
|
||||||
}
|
.find_map(|(k, v)| (v.controller.id() == id).then_some(*k));
|
||||||
msg = self.props.transport.recv() => {
|
|
||||||
match msg {
|
|
||||||
Some(request) => {
|
|
||||||
match self.process_transport_inbound(request).await {
|
|
||||||
Ok(response) => {
|
|
||||||
if self.props.transport.send(Ok(response)).await.is_err() {
|
|
||||||
error!(actor = "useragent", reason = "channel closed", "send.failed");
|
|
||||||
return Some(kameo::mailbox::Signal::Stop);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
let _ = self.props.transport.send(Err(err)).await;
|
|
||||||
return Some(kameo::mailbox::Signal::Stop);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
info!(actor = "useragent", "transport.closed");
|
|
||||||
return Some(kameo::mailbox::Signal::Stop);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UserAgentSession {
|
if let Some(pubkey) = cancelled_pubkey {
|
||||||
pub fn new_test(db: crate::db::DatabasePool, actors: crate::actors::GlobalActors) -> Self {
|
self.pending_client_approvals.remove(&pubkey);
|
||||||
use arbiter_proto::transport::DummyTransport;
|
|
||||||
let transport: super::Transport = Box::new(DummyTransport::new());
|
if let Err(e) = self
|
||||||
let props = UserAgentConnection::new(db, actors, transport);
|
.sender
|
||||||
Self {
|
.send(OutOfBand::ClientConnectionCancel { pubkey })
|
||||||
props,
|
.await
|
||||||
state: UserAgentStateMachine::new(DummyContext),
|
{
|
||||||
|
error!(
|
||||||
|
?e,
|
||||||
|
actor = "user_agent",
|
||||||
|
event = "failed to announce client connection cancellation"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(std::ops::ControlFlow::Continue(()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,24 @@
|
|||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
use alloy::primitives::Address;
|
||||||
use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
|
use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
|
||||||
|
use diesel::sql_types::ops::Add;
|
||||||
|
use diesel::{BoolExpressionMethods as _, ExpressionMethods as _, QueryDsl as _, SelectableHelper};
|
||||||
|
use diesel_async::{AsyncConnection, RunQueryDsl};
|
||||||
use kameo::error::SendError;
|
use kameo::error::SendError;
|
||||||
|
use kameo::prelude::Context;
|
||||||
|
use kameo::{message, messages};
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
use x25519_dalek::{EphemeralSecret, PublicKey};
|
use x25519_dalek::{EphemeralSecret, PublicKey};
|
||||||
|
|
||||||
|
use crate::actors::flow_coordinator::client_connect_approval::ClientApprovalAnswer;
|
||||||
|
use crate::actors::keyholder::KeyHolderState;
|
||||||
|
use crate::actors::user_agent::session::Error;
|
||||||
|
use crate::db::models::{
|
||||||
|
CoreEvmWalletAccess, EvmWalletAccess, NewEvmWalletAccess, ProgramClient, ProgramClientMetadata,
|
||||||
|
};
|
||||||
|
use crate::db::schema::evm_wallet_access;
|
||||||
|
use crate::evm::policies::{Grant, SpecificGrant};
|
||||||
use crate::safe_cell::SafeCell;
|
use crate::safe_cell::SafeCell;
|
||||||
use crate::{
|
use crate::{
|
||||||
actors::{
|
actors::{
|
||||||
@@ -12,67 +26,19 @@ use crate::{
|
|||||||
Generate, ListWallets, UseragentCreateGrant, UseragentDeleteGrant, UseragentListGrants,
|
Generate, ListWallets, UseragentCreateGrant, UseragentDeleteGrant, UseragentListGrants,
|
||||||
},
|
},
|
||||||
keyholder::{self, Bootstrap, TryUnseal},
|
keyholder::{self, Bootstrap, TryUnseal},
|
||||||
user_agent::{
|
user_agent::session::{
|
||||||
BootstrapError, Request, Response, TransportResponseError, UnsealError, VaultState,
|
|
||||||
session::{
|
|
||||||
UserAgentSession,
|
UserAgentSession,
|
||||||
state::{UnsealContext, UserAgentEvents, UserAgentStates},
|
state::{UnsealContext, UserAgentEvents, UserAgentStates},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
|
||||||
safe_cell::SafeCellHandle as _,
|
safe_cell::SafeCellHandle as _,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl UserAgentSession {
|
impl UserAgentSession {
|
||||||
pub async fn process_transport_inbound(&mut self, req: Request) -> Output {
|
fn take_unseal_secret(&mut self) -> Result<(EphemeralSecret, PublicKey), Error> {
|
||||||
match req {
|
|
||||||
Request::UnsealStart { client_pubkey } => {
|
|
||||||
self.handle_unseal_request(client_pubkey).await
|
|
||||||
}
|
|
||||||
Request::UnsealEncryptedKey {
|
|
||||||
nonce,
|
|
||||||
ciphertext,
|
|
||||||
associated_data,
|
|
||||||
} => {
|
|
||||||
self.handle_unseal_encrypted_key(nonce, ciphertext, associated_data)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
Request::BootstrapEncryptedKey {
|
|
||||||
nonce,
|
|
||||||
ciphertext,
|
|
||||||
associated_data,
|
|
||||||
} => {
|
|
||||||
self.handle_bootstrap_encrypted_key(nonce, ciphertext, associated_data)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
Request::ListGrants => self.handle_grant_list().await,
|
|
||||||
Request::QueryVaultState => self.handle_query_vault_state().await,
|
|
||||||
Request::EvmWalletCreate => self.handle_evm_wallet_create().await,
|
|
||||||
Request::EvmWalletList => self.handle_evm_wallet_list().await,
|
|
||||||
Request::AuthChallengeRequest { .. }
|
|
||||||
| Request::AuthChallengeSolution { .. }
|
|
||||||
| Request::ClientConnectionResponse { .. } => {
|
|
||||||
Err(TransportResponseError::UnexpectedRequestPayload)
|
|
||||||
}
|
|
||||||
Request::EvmGrantCreate {
|
|
||||||
client_id,
|
|
||||||
shared,
|
|
||||||
specific,
|
|
||||||
} => self.handle_grant_create(client_id, shared, specific).await,
|
|
||||||
Request::EvmGrantDelete { grant_id } => self.handle_grant_delete(grant_id).await,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Output = Result<Response, TransportResponseError>;
|
|
||||||
|
|
||||||
impl UserAgentSession {
|
|
||||||
fn take_unseal_secret(
|
|
||||||
&mut self,
|
|
||||||
) -> Result<(EphemeralSecret, PublicKey), TransportResponseError> {
|
|
||||||
let UserAgentStates::WaitingForUnsealKey(unseal_context) = self.state.state() else {
|
let UserAgentStates::WaitingForUnsealKey(unseal_context) = self.state.state() else {
|
||||||
error!("Received encrypted key in invalid state");
|
error!("Received encrypted key in invalid state");
|
||||||
return Err(TransportResponseError::InvalidStateForUnsealEncryptedKey);
|
return Err(Error::internal("Invalid state for unseal encrypted key"));
|
||||||
};
|
};
|
||||||
|
|
||||||
let ephemeral_secret = {
|
let ephemeral_secret = {
|
||||||
@@ -87,7 +53,7 @@ impl UserAgentSession {
|
|||||||
None => {
|
None => {
|
||||||
drop(secret_lock);
|
drop(secret_lock);
|
||||||
error!("Ephemeral secret already taken");
|
error!("Ephemeral secret already taken");
|
||||||
return Err(TransportResponseError::StateTransitionFailed);
|
return Err(Error::internal("Ephemeral secret already taken"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -121,8 +87,38 @@ impl UserAgentSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn handle_unseal_request(&mut self, client_pubkey: x25519_dalek::PublicKey) -> Output {
|
pub struct UnsealStartResponse {
|
||||||
|
pub server_pubkey: PublicKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum UnsealError {
|
||||||
|
#[error("Invalid key provided for unsealing")]
|
||||||
|
InvalidKey,
|
||||||
|
#[error("Internal error during unsealing process")]
|
||||||
|
General(#[from] super::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum BootstrapError {
|
||||||
|
#[error("Invalid key provided for bootstrapping")]
|
||||||
|
InvalidKey,
|
||||||
|
#[error("Vault is already bootstrapped")]
|
||||||
|
AlreadyBootstrapped,
|
||||||
|
|
||||||
|
#[error("Internal error during bootstrapping process")]
|
||||||
|
General(#[from] super::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[messages]
|
||||||
|
impl UserAgentSession {
|
||||||
|
#[message]
|
||||||
|
pub async fn handle_unseal_request(
|
||||||
|
&mut self,
|
||||||
|
client_pubkey: x25519_dalek::PublicKey,
|
||||||
|
) -> Result<UnsealStartResponse, Error> {
|
||||||
let secret = EphemeralSecret::random();
|
let secret = EphemeralSecret::random();
|
||||||
let public_key = PublicKey::from(&secret);
|
let public_key = PublicKey::from(&secret);
|
||||||
|
|
||||||
@@ -131,24 +127,27 @@ impl UserAgentSession {
|
|||||||
client_public_key: client_pubkey,
|
client_public_key: client_pubkey,
|
||||||
}))?;
|
}))?;
|
||||||
|
|
||||||
Ok(Response::UnsealStartResponse {
|
Ok(UnsealStartResponse {
|
||||||
server_pubkey: public_key,
|
server_pubkey: public_key,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_unseal_encrypted_key(
|
#[message]
|
||||||
|
pub async fn handle_unseal_encrypted_key(
|
||||||
&mut self,
|
&mut self,
|
||||||
nonce: Vec<u8>,
|
nonce: Vec<u8>,
|
||||||
ciphertext: Vec<u8>,
|
ciphertext: Vec<u8>,
|
||||||
associated_data: Vec<u8>,
|
associated_data: Vec<u8>,
|
||||||
) -> Output {
|
) -> Result<(), UnsealError> {
|
||||||
let (ephemeral_secret, client_public_key) = match self.take_unseal_secret() {
|
let (ephemeral_secret, client_public_key) = match self.take_unseal_secret() {
|
||||||
Ok(values) => values,
|
Ok(values) => values,
|
||||||
Err(TransportResponseError::StateTransitionFailed) => {
|
Err(Error::State) => {
|
||||||
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
||||||
return Ok(Response::UnsealResult(Err(UnsealError::InvalidKey)));
|
return Err(UnsealError::InvalidKey);
|
||||||
|
}
|
||||||
|
Err(_err) => {
|
||||||
|
return Err(Error::internal("Failed to take unseal secret").into());
|
||||||
}
|
}
|
||||||
Err(err) => return Err(err),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let seal_key_buffer = match Self::decrypt_client_key_material(
|
let seal_key_buffer = match Self::decrypt_client_key_material(
|
||||||
@@ -161,7 +160,7 @@ impl UserAgentSession {
|
|||||||
Ok(buffer) => buffer,
|
Ok(buffer) => buffer,
|
||||||
Err(()) => {
|
Err(()) => {
|
||||||
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
||||||
return Ok(Response::UnsealResult(Err(UnsealError::InvalidKey)));
|
return Err(UnsealError::InvalidKey);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -177,38 +176,39 @@ impl UserAgentSession {
|
|||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
info!("Successfully unsealed key with client-provided key");
|
info!("Successfully unsealed key with client-provided key");
|
||||||
self.transition(UserAgentEvents::ReceivedValidKey)?;
|
self.transition(UserAgentEvents::ReceivedValidKey)?;
|
||||||
Ok(Response::UnsealResult(Ok(())))
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(SendError::HandlerError(keyholder::Error::InvalidKey)) => {
|
Err(SendError::HandlerError(keyholder::Error::InvalidKey)) => {
|
||||||
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
||||||
Ok(Response::UnsealResult(Err(UnsealError::InvalidKey)))
|
Err(UnsealError::InvalidKey)
|
||||||
}
|
}
|
||||||
Err(SendError::HandlerError(err)) => {
|
Err(SendError::HandlerError(err)) => {
|
||||||
error!(?err, "Keyholder failed to unseal key");
|
error!(?err, "Keyholder failed to unseal key");
|
||||||
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
||||||
Ok(Response::UnsealResult(Err(UnsealError::InvalidKey)))
|
Err(UnsealError::InvalidKey)
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(?err, "Failed to send unseal request to keyholder");
|
error!(?err, "Failed to send unseal request to keyholder");
|
||||||
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
||||||
Err(TransportResponseError::KeyHolderActorUnreachable)
|
Err(Error::internal("Vault actor error").into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_bootstrap_encrypted_key(
|
#[message]
|
||||||
|
pub(crate) async fn handle_bootstrap_encrypted_key(
|
||||||
&mut self,
|
&mut self,
|
||||||
nonce: Vec<u8>,
|
nonce: Vec<u8>,
|
||||||
ciphertext: Vec<u8>,
|
ciphertext: Vec<u8>,
|
||||||
associated_data: Vec<u8>,
|
associated_data: Vec<u8>,
|
||||||
) -> Output {
|
) -> Result<(), BootstrapError> {
|
||||||
let (ephemeral_secret, client_public_key) = match self.take_unseal_secret() {
|
let (ephemeral_secret, client_public_key) = match self.take_unseal_secret() {
|
||||||
Ok(values) => values,
|
Ok(values) => values,
|
||||||
Err(TransportResponseError::StateTransitionFailed) => {
|
Err(Error::State) => {
|
||||||
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
||||||
return Ok(Response::BootstrapResult(Err(BootstrapError::InvalidKey)));
|
return Err(BootstrapError::InvalidKey);
|
||||||
}
|
}
|
||||||
Err(err) => return Err(err),
|
Err(err) => return Err(err.into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let seal_key_buffer = match Self::decrypt_client_key_material(
|
let seal_key_buffer = match Self::decrypt_client_key_material(
|
||||||
@@ -221,7 +221,7 @@ impl UserAgentSession {
|
|||||||
Ok(buffer) => buffer,
|
Ok(buffer) => buffer,
|
||||||
Err(()) => {
|
Err(()) => {
|
||||||
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
||||||
return Ok(Response::BootstrapResult(Err(BootstrapError::InvalidKey)));
|
return Err(BootstrapError::InvalidKey);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -237,107 +237,110 @@ impl UserAgentSession {
|
|||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
info!("Successfully bootstrapped vault with client-provided key");
|
info!("Successfully bootstrapped vault with client-provided key");
|
||||||
self.transition(UserAgentEvents::ReceivedValidKey)?;
|
self.transition(UserAgentEvents::ReceivedValidKey)?;
|
||||||
Ok(Response::BootstrapResult(Ok(())))
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(SendError::HandlerError(keyholder::Error::AlreadyBootstrapped)) => {
|
Err(SendError::HandlerError(keyholder::Error::AlreadyBootstrapped)) => {
|
||||||
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
||||||
Ok(Response::BootstrapResult(Err(
|
Err(BootstrapError::AlreadyBootstrapped)
|
||||||
BootstrapError::AlreadyBootstrapped,
|
|
||||||
)))
|
|
||||||
}
|
}
|
||||||
Err(SendError::HandlerError(err)) => {
|
Err(SendError::HandlerError(err)) => {
|
||||||
error!(?err, "Keyholder failed to bootstrap vault");
|
error!(?err, "Keyholder failed to bootstrap vault");
|
||||||
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
||||||
Ok(Response::BootstrapResult(Err(BootstrapError::InvalidKey)))
|
Err(BootstrapError::InvalidKey)
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(?err, "Failed to send bootstrap request to keyholder");
|
error!(?err, "Failed to send bootstrap request to keyholder");
|
||||||
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
self.transition(UserAgentEvents::ReceivedInvalidKey)?;
|
||||||
Err(TransportResponseError::KeyHolderActorUnreachable)
|
Err(BootstrapError::General(Error::internal(
|
||||||
|
"Vault actor error",
|
||||||
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[messages]
|
||||||
impl UserAgentSession {
|
impl UserAgentSession {
|
||||||
async fn handle_query_vault_state(&mut self) -> Output {
|
#[message]
|
||||||
use crate::actors::keyholder::{GetState, StateDiscriminants};
|
pub(crate) async fn handle_query_vault_state(&mut self) -> Result<KeyHolderState, Error> {
|
||||||
|
use crate::actors::keyholder::GetState;
|
||||||
|
|
||||||
let vault_state = match self.props.actors.key_holder.ask(GetState {}).await {
|
let vault_state = match self.props.actors.key_holder.ask(GetState {}).await {
|
||||||
Ok(StateDiscriminants::Unbootstrapped) => VaultState::Unbootstrapped,
|
Ok(state) => state,
|
||||||
Ok(StateDiscriminants::Sealed) => VaultState::Sealed,
|
|
||||||
Ok(StateDiscriminants::Unsealed) => VaultState::Unsealed,
|
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(?err, actor = "useragent", "keyholder.query.failed");
|
error!(?err, actor = "useragent", "keyholder.query.failed");
|
||||||
return Err(TransportResponseError::KeyHolderActorUnreachable);
|
return Err(Error::internal("Vault is in broken state"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Response::VaultState(vault_state))
|
Ok(vault_state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[messages]
|
||||||
impl UserAgentSession {
|
impl UserAgentSession {
|
||||||
async fn handle_evm_wallet_create(&mut self) -> Output {
|
#[message]
|
||||||
let result = match self.props.actors.evm.ask(Generate {}).await {
|
pub(crate) async fn handle_evm_wallet_create(&mut self) -> Result<(i32, Address), Error> {
|
||||||
Ok(_address) => return Ok(Response::EvmWalletCreate(Ok(()))),
|
match self.props.actors.evm.ask(Generate {}).await {
|
||||||
Err(SendError::HandlerError(err)) => Err(err),
|
Ok(address) => Ok(address),
|
||||||
|
Err(SendError::HandlerError(err)) => Err(Error::internal(format!(
|
||||||
|
"EVM wallet generation failed: {err}"
|
||||||
|
))),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(?err, "EVM actor unreachable during wallet create");
|
error!(?err, "EVM actor unreachable during wallet create");
|
||||||
return Err(TransportResponseError::KeyHolderActorUnreachable);
|
Err(Error::internal("EVM actor unreachable"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
Ok(Response::EvmWalletCreate(result))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_evm_wallet_list(&mut self) -> Output {
|
#[message]
|
||||||
|
pub(crate) async fn handle_evm_wallet_list(&mut self) -> Result<Vec<(i32, Address)>, Error> {
|
||||||
match self.props.actors.evm.ask(ListWallets {}).await {
|
match self.props.actors.evm.ask(ListWallets {}).await {
|
||||||
Ok(wallets) => Ok(Response::EvmWalletList(wallets)),
|
Ok(wallets) => Ok(wallets),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(?err, "EVM wallet list failed");
|
error!(?err, "EVM wallet list failed");
|
||||||
Err(TransportResponseError::KeyHolderActorUnreachable)
|
Err(Error::internal("Failed to list EVM wallets"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[messages]
|
||||||
impl UserAgentSession {
|
impl UserAgentSession {
|
||||||
async fn handle_grant_list(&mut self) -> Output {
|
#[message]
|
||||||
|
pub(crate) async fn handle_grant_list(&mut self) -> Result<Vec<Grant<SpecificGrant>>, Error> {
|
||||||
match self.props.actors.evm.ask(UseragentListGrants {}).await {
|
match self.props.actors.evm.ask(UseragentListGrants {}).await {
|
||||||
Ok(grants) => Ok(Response::ListGrants(grants)),
|
Ok(grants) => Ok(grants),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(?err, "EVM grant list failed");
|
error!(?err, "EVM grant list failed");
|
||||||
Err(TransportResponseError::KeyHolderActorUnreachable)
|
Err(Error::internal("Failed to list EVM grants"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_grant_create(
|
#[message]
|
||||||
|
pub(crate) async fn handle_grant_create(
|
||||||
&mut self,
|
&mut self,
|
||||||
client_id: i32,
|
|
||||||
basic: crate::evm::policies::SharedGrantSettings,
|
basic: crate::evm::policies::SharedGrantSettings,
|
||||||
grant: crate::evm::policies::SpecificGrant,
|
grant: crate::evm::policies::SpecificGrant,
|
||||||
) -> Output {
|
) -> Result<i32, Error> {
|
||||||
match self
|
match self
|
||||||
.props
|
.props
|
||||||
.actors
|
.actors
|
||||||
.evm
|
.evm
|
||||||
.ask(UseragentCreateGrant {
|
.ask(UseragentCreateGrant { basic, grant })
|
||||||
client_id,
|
|
||||||
basic,
|
|
||||||
grant,
|
|
||||||
})
|
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(grant_id) => Ok(Response::EvmGrantCreate(Ok(grant_id))),
|
Ok(grant_id) => Ok(grant_id),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(?err, "EVM grant create failed");
|
error!(?err, "EVM grant create failed");
|
||||||
Err(TransportResponseError::KeyHolderActorUnreachable)
|
Err(Error::internal("Failed to create EVM grant"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_grant_delete(&mut self, grant_id: i32) -> Output {
|
#[message]
|
||||||
|
pub(crate) async fn handle_grant_delete(&mut self, grant_id: i32) -> Result<(), Error> {
|
||||||
match self
|
match self
|
||||||
.props
|
.props
|
||||||
.actors
|
.actors
|
||||||
@@ -345,11 +348,126 @@ impl UserAgentSession {
|
|||||||
.ask(UseragentDeleteGrant { grant_id })
|
.ask(UseragentDeleteGrant { grant_id })
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(()) => Ok(Response::EvmGrantDelete(Ok(()))),
|
Ok(()) => Ok(()),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(?err, "EVM grant delete failed");
|
error!(?err, "EVM grant delete failed");
|
||||||
Err(TransportResponseError::KeyHolderActorUnreachable)
|
Err(Error::internal("Failed to delete EVM grant"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[message]
|
||||||
|
pub(crate) async fn handle_grant_evm_wallet_access(
|
||||||
|
&mut self,
|
||||||
|
entries: Vec<NewEvmWalletAccess>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let mut conn = self.props.db.get().await?;
|
||||||
|
conn.transaction(|conn| {
|
||||||
|
Box::pin(async move {
|
||||||
|
use crate::db::schema::evm_wallet_access;
|
||||||
|
|
||||||
|
for entry in entries {
|
||||||
|
diesel::insert_into(evm_wallet_access::table)
|
||||||
|
.values(&entry)
|
||||||
|
.on_conflict_do_nothing()
|
||||||
|
.execute(conn)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result::<_, Error>::Ok(())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[message]
|
||||||
|
pub(crate) async fn handle_revoke_evm_wallet_access(
|
||||||
|
&mut self,
|
||||||
|
entries: Vec<i32>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let mut conn = self.props.db.get().await?;
|
||||||
|
conn.transaction(|conn| {
|
||||||
|
Box::pin(async move {
|
||||||
|
use crate::db::schema::evm_wallet_access;
|
||||||
|
for entry in entries {
|
||||||
|
diesel::delete(evm_wallet_access::table)
|
||||||
|
.filter(evm_wallet_access::wallet_id.eq(entry))
|
||||||
|
.execute(conn)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result::<_, Error>::Ok(())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[message]
|
||||||
|
pub(crate) async fn handle_list_wallet_access(
|
||||||
|
&mut self,
|
||||||
|
) -> Result<Vec<EvmWalletAccess>, Error> {
|
||||||
|
let mut conn = self.props.db.get().await?;
|
||||||
|
use crate::db::schema::evm_wallet_access;
|
||||||
|
let access_entries = evm_wallet_access::table
|
||||||
|
.select(EvmWalletAccess::as_select())
|
||||||
|
.load::<_>(&mut conn)
|
||||||
|
.await?;
|
||||||
|
Ok(access_entries)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[messages]
|
||||||
|
impl UserAgentSession {
|
||||||
|
#[message(ctx)]
|
||||||
|
pub(crate) async fn handle_new_client_approve(
|
||||||
|
&mut self,
|
||||||
|
approved: bool,
|
||||||
|
pubkey: ed25519_dalek::VerifyingKey,
|
||||||
|
ctx: &mut Context<Self, Result<(), Error>>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let pending_approval = match self.pending_client_approvals.remove(&pubkey) {
|
||||||
|
Some(approval) => approval,
|
||||||
|
None => {
|
||||||
|
error!("Received client connection response for unknown client");
|
||||||
|
return Err(Error::internal("Unknown client in connection response"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pending_approval
|
||||||
|
.controller
|
||||||
|
.tell(ClientApprovalAnswer { approved })
|
||||||
|
.await
|
||||||
|
.map_err(|err| {
|
||||||
|
error!(
|
||||||
|
?err,
|
||||||
|
"Failed to send client approval response to controller"
|
||||||
|
);
|
||||||
|
Error::internal("Failed to send client approval response to controller")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
ctx.actor_ref().unlink(&pending_approval.controller).await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[message]
|
||||||
|
pub(crate) async fn handle_sdk_client_list(
|
||||||
|
&mut self,
|
||||||
|
) -> Result<Vec<(ProgramClient, ProgramClientMetadata)>, Error> {
|
||||||
|
use crate::db::schema::{client_metadata, program_client};
|
||||||
|
let mut conn = self.props.db.get().await?;
|
||||||
|
|
||||||
|
let clients = program_client::table
|
||||||
|
.inner_join(client_metadata::table)
|
||||||
|
.select((
|
||||||
|
ProgramClient::as_select(),
|
||||||
|
ProgramClientMetadata::as_select(),
|
||||||
|
))
|
||||||
|
.load::<(ProgramClient, ProgramClientMetadata)>(&mut conn)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(clients)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
72
server/crates/arbiter-server/src/cli.rs
Normal file
72
server/crates/arbiter-server/src/cli.rs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
use std::{
|
||||||
|
net::{Ipv4Addr, SocketAddr, SocketAddrV4},
|
||||||
|
path::PathBuf,
|
||||||
|
};
|
||||||
|
|
||||||
|
use clap::{Args, Parser, Subcommand};
|
||||||
|
|
||||||
|
const DEFAULT_LISTEN_ADDR: SocketAddr = SocketAddr::V4(SocketAddrV4::new(
|
||||||
|
Ipv4Addr::LOCALHOST,
|
||||||
|
arbiter_proto::DEFAULT_SERVER_PORT,
|
||||||
|
));
|
||||||
|
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
#[command(name = "arbiter-server")]
|
||||||
|
#[command(about = "Arbiter gRPC server")]
|
||||||
|
pub struct Cli {
|
||||||
|
#[command(subcommand)]
|
||||||
|
pub command: Option<Command>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Subcommand)]
|
||||||
|
pub enum Command {
|
||||||
|
/// Run server in foreground mode.
|
||||||
|
Run(RunArgs),
|
||||||
|
/// Manage service lifecycle.
|
||||||
|
Service {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: ServiceCommand,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Args)]
|
||||||
|
pub struct RunArgs {
|
||||||
|
#[arg(long, default_value_t = DEFAULT_LISTEN_ADDR)]
|
||||||
|
pub listen_addr: SocketAddr,
|
||||||
|
#[arg(long)]
|
||||||
|
pub data_dir: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for RunArgs {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
listen_addr: DEFAULT_LISTEN_ADDR,
|
||||||
|
data_dir: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Subcommand)]
|
||||||
|
pub enum ServiceCommand {
|
||||||
|
/// Install Windows service in Service Control Manager.
|
||||||
|
Install(ServiceInstallArgs),
|
||||||
|
/// Internal service entrypoint. SCM only.
|
||||||
|
#[command(hide = true)]
|
||||||
|
Run(ServiceRunArgs),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Args)]
|
||||||
|
pub struct ServiceInstallArgs {
|
||||||
|
#[arg(long)]
|
||||||
|
pub start: bool,
|
||||||
|
#[arg(long)]
|
||||||
|
pub data_dir: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Args)]
|
||||||
|
pub struct ServiceRunArgs {
|
||||||
|
#[arg(long, default_value_t = DEFAULT_LISTEN_ADDR)]
|
||||||
|
pub listen_addr: SocketAddr,
|
||||||
|
#[arg(long)]
|
||||||
|
pub data_dir: Option<PathBuf>,
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::string::FromUtf8Error;
|
use std::{net::IpAddr, string::FromUtf8Error};
|
||||||
|
|
||||||
use diesel::{ExpressionMethods as _, QueryDsl, SelectableHelper as _};
|
use diesel::{ExpressionMethods as _, QueryDsl, SelectableHelper as _};
|
||||||
use diesel_async::{AsyncConnection, RunQueryDsl};
|
use diesel_async::{AsyncConnection, RunQueryDsl};
|
||||||
@@ -6,7 +6,7 @@ use miette::Diagnostic;
|
|||||||
use pem::Pem;
|
use pem::Pem;
|
||||||
use rcgen::{
|
use rcgen::{
|
||||||
BasicConstraints, Certificate, CertificateParams, CertifiedIssuer, DistinguishedName, DnType,
|
BasicConstraints, Certificate, CertificateParams, CertifiedIssuer, DistinguishedName, DnType,
|
||||||
IsCa, Issuer, KeyPair, KeyUsagePurpose,
|
IsCa, Issuer, KeyPair, KeyUsagePurpose, SanType,
|
||||||
};
|
};
|
||||||
use rustls::pki_types::pem::PemObject;
|
use rustls::pki_types::pem::PemObject;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
@@ -114,6 +114,11 @@ impl TlsCa {
|
|||||||
KeyUsagePurpose::DigitalSignature,
|
KeyUsagePurpose::DigitalSignature,
|
||||||
KeyUsagePurpose::KeyEncipherment,
|
KeyUsagePurpose::KeyEncipherment,
|
||||||
];
|
];
|
||||||
|
params
|
||||||
|
.subject_alt_names
|
||||||
|
.push(SanType::IpAddress(IpAddr::from([
|
||||||
|
127, 0, 0, 1,
|
||||||
|
])));
|
||||||
|
|
||||||
let mut dn = DistinguishedName::new();
|
let mut dn = DistinguishedName::new();
|
||||||
dn.push(DnType::CommonName, "Arbiter Instance Leaf");
|
dn.push(DnType::CommonName, "Arbiter Instance Leaf");
|
||||||
|
|||||||
@@ -44,6 +44,14 @@ pub enum DatabaseSetupError {
|
|||||||
Pool(#[from] PoolInitError),
|
Pool(#[from] PoolInitError),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum DatabaseError {
|
||||||
|
#[error("Database connection error")]
|
||||||
|
Pool(#[from] PoolError),
|
||||||
|
#[error("Database query error")]
|
||||||
|
Connection(#[from] diesel::result::Error),
|
||||||
|
}
|
||||||
|
|
||||||
#[tracing::instrument(level = "info")]
|
#[tracing::instrument(level = "info")]
|
||||||
fn database_path() -> Result<std::path::PathBuf, DatabaseSetupError> {
|
fn database_path() -> Result<std::path::PathBuf, DatabaseSetupError> {
|
||||||
let arbiter_home = arbiter_proto::home_path().map_err(DatabaseSetupError::HomeDir)?;
|
let arbiter_home = arbiter_proto::home_path().map_err(DatabaseSetupError::HomeDir)?;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ pub mod types {
|
|||||||
sqlite::{Sqlite, SqliteType},
|
sqlite::{Sqlite, SqliteType},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, FromSqlRow, AsExpression)]
|
#[derive(Debug, FromSqlRow, AsExpression, Clone)]
|
||||||
#[diesel(sql_type = Integer)]
|
#[diesel(sql_type = Integer)]
|
||||||
#[repr(transparent)] // hint compiler to optimize the wrapper struct away
|
#[repr(transparent)] // hint compiler to optimize the wrapper struct away
|
||||||
pub struct SqliteTimestamp(pub DateTime<Utc>);
|
pub struct SqliteTimestamp(pub DateTime<Utc>);
|
||||||
@@ -185,12 +185,53 @@ pub struct EvmWallet {
|
|||||||
pub created_at: SqliteTimestamp,
|
pub created_at: SqliteTimestamp,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Queryable, Debug, Insertable, Selectable)]
|
#[derive(Models, Queryable, Debug, Insertable, Selectable, Clone)]
|
||||||
|
#[diesel(table_name = schema::evm_wallet_access, check_for_backend(Sqlite))]
|
||||||
|
#[view(
|
||||||
|
NewEvmWalletAccess,
|
||||||
|
derive(Insertable),
|
||||||
|
omit(id, created_at),
|
||||||
|
attributes_with = "deriveless"
|
||||||
|
)]
|
||||||
|
#[view(
|
||||||
|
CoreEvmWalletAccess,
|
||||||
|
derive(Insertable),
|
||||||
|
omit(created_at),
|
||||||
|
attributes_with = "deriveless"
|
||||||
|
)]
|
||||||
|
pub struct EvmWalletAccess {
|
||||||
|
pub id: i32,
|
||||||
|
pub wallet_id: i32,
|
||||||
|
pub client_id: i32,
|
||||||
|
pub created_at: SqliteTimestamp,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Models, Queryable, Debug, Insertable, Selectable)]
|
||||||
|
#[diesel(table_name = schema::client_metadata, check_for_backend(Sqlite))]
|
||||||
|
pub struct ProgramClientMetadata {
|
||||||
|
pub id: i32,
|
||||||
|
pub name: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub version: Option<String>,
|
||||||
|
pub created_at: SqliteTimestamp,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Models, Queryable, Debug, Insertable, Selectable)]
|
||||||
|
#[diesel(table_name = schema::client_metadata_history, check_for_backend(Sqlite))]
|
||||||
|
pub struct ProgramClientMetadataHistory {
|
||||||
|
pub id: i32,
|
||||||
|
pub metadata_id: i32,
|
||||||
|
pub client_id: i32,
|
||||||
|
pub created_at: SqliteTimestamp,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Models, Queryable, Debug, Insertable, Selectable)]
|
||||||
#[diesel(table_name = schema::program_client, check_for_backend(Sqlite))]
|
#[diesel(table_name = schema::program_client, check_for_backend(Sqlite))]
|
||||||
pub struct ProgramClient {
|
pub struct ProgramClient {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub nonce: i32,
|
pub nonce: i32,
|
||||||
pub public_key: Vec<u8>,
|
pub public_key: Vec<u8>,
|
||||||
|
pub metadata_id: i32,
|
||||||
pub created_at: SqliteTimestamp,
|
pub created_at: SqliteTimestamp,
|
||||||
pub updated_at: SqliteTimestamp,
|
pub updated_at: SqliteTimestamp,
|
||||||
}
|
}
|
||||||
@@ -230,8 +271,7 @@ pub struct EvmEtherTransferLimit {
|
|||||||
)]
|
)]
|
||||||
pub struct EvmBasicGrant {
|
pub struct EvmBasicGrant {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub wallet_id: i32, // references evm_wallet.id
|
pub wallet_access_id: i32, // references evm_wallet_access.id
|
||||||
pub client_id: i32, // references program_client.id
|
|
||||||
pub chain_id: i32,
|
pub chain_id: i32,
|
||||||
pub valid_from: Option<SqliteTimestamp>,
|
pub valid_from: Option<SqliteTimestamp>,
|
||||||
pub valid_until: Option<SqliteTimestamp>,
|
pub valid_until: Option<SqliteTimestamp>,
|
||||||
@@ -254,8 +294,7 @@ pub struct EvmBasicGrant {
|
|||||||
pub struct EvmTransactionLog {
|
pub struct EvmTransactionLog {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub grant_id: i32,
|
pub grant_id: i32,
|
||||||
pub client_id: i32,
|
pub wallet_access_id: i32,
|
||||||
pub wallet_id: i32,
|
|
||||||
pub chain_id: i32,
|
pub chain_id: i32,
|
||||||
pub eth_value: Vec<u8>,
|
pub eth_value: Vec<u8>,
|
||||||
pub signed_at: SqliteTimestamp,
|
pub signed_at: SqliteTimestamp,
|
||||||
|
|||||||
@@ -20,11 +20,29 @@ diesel::table! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
client_metadata (id) {
|
||||||
|
id -> Integer,
|
||||||
|
name -> Text,
|
||||||
|
description -> Nullable<Text>,
|
||||||
|
version -> Nullable<Text>,
|
||||||
|
created_at -> Integer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
client_metadata_history (id) {
|
||||||
|
id -> Integer,
|
||||||
|
metadata_id -> Integer,
|
||||||
|
client_id -> Integer,
|
||||||
|
created_at -> Integer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
evm_basic_grant (id) {
|
evm_basic_grant (id) {
|
||||||
id -> Integer,
|
id -> Integer,
|
||||||
wallet_id -> Integer,
|
wallet_access_id -> Integer,
|
||||||
client_id -> Integer,
|
|
||||||
chain_id -> Integer,
|
chain_id -> Integer,
|
||||||
valid_from -> Nullable<Integer>,
|
valid_from -> Nullable<Integer>,
|
||||||
valid_until -> Nullable<Integer>,
|
valid_until -> Nullable<Integer>,
|
||||||
@@ -95,9 +113,8 @@ diesel::table! {
|
|||||||
diesel::table! {
|
diesel::table! {
|
||||||
evm_transaction_log (id) {
|
evm_transaction_log (id) {
|
||||||
id -> Integer,
|
id -> Integer,
|
||||||
|
wallet_access_id -> Integer,
|
||||||
grant_id -> Integer,
|
grant_id -> Integer,
|
||||||
client_id -> Integer,
|
|
||||||
wallet_id -> Integer,
|
|
||||||
chain_id -> Integer,
|
chain_id -> Integer,
|
||||||
eth_value -> Binary,
|
eth_value -> Binary,
|
||||||
signed_at -> Integer,
|
signed_at -> Integer,
|
||||||
@@ -113,11 +130,21 @@ diesel::table! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
evm_wallet_access (id) {
|
||||||
|
id -> Integer,
|
||||||
|
wallet_id -> Integer,
|
||||||
|
client_id -> Integer,
|
||||||
|
created_at -> Integer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
program_client (id) {
|
program_client (id) {
|
||||||
id -> Integer,
|
id -> Integer,
|
||||||
nonce -> Integer,
|
nonce -> Integer,
|
||||||
public_key -> Binary,
|
public_key -> Binary,
|
||||||
|
metadata_id -> Integer,
|
||||||
created_at -> Integer,
|
created_at -> Integer,
|
||||||
updated_at -> Integer,
|
updated_at -> Integer,
|
||||||
}
|
}
|
||||||
@@ -151,17 +178,18 @@ diesel::table! {
|
|||||||
id -> Integer,
|
id -> Integer,
|
||||||
nonce -> Integer,
|
nonce -> Integer,
|
||||||
public_key -> Binary,
|
public_key -> Binary,
|
||||||
|
key_type -> Integer,
|
||||||
created_at -> Integer,
|
created_at -> Integer,
|
||||||
updated_at -> Integer,
|
updated_at -> Integer,
|
||||||
key_type -> Integer,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
diesel::joinable!(aead_encrypted -> root_key_history (associated_root_key_id));
|
diesel::joinable!(aead_encrypted -> root_key_history (associated_root_key_id));
|
||||||
diesel::joinable!(arbiter_settings -> root_key_history (root_key_id));
|
diesel::joinable!(arbiter_settings -> root_key_history (root_key_id));
|
||||||
diesel::joinable!(arbiter_settings -> tls_history (tls_id));
|
diesel::joinable!(arbiter_settings -> tls_history (tls_id));
|
||||||
diesel::joinable!(evm_basic_grant -> evm_wallet (wallet_id));
|
diesel::joinable!(client_metadata_history -> client_metadata (metadata_id));
|
||||||
diesel::joinable!(evm_basic_grant -> program_client (client_id));
|
diesel::joinable!(client_metadata_history -> program_client (client_id));
|
||||||
|
diesel::joinable!(evm_basic_grant -> evm_wallet_access (wallet_access_id));
|
||||||
diesel::joinable!(evm_ether_transfer_grant -> evm_basic_grant (basic_grant_id));
|
diesel::joinable!(evm_ether_transfer_grant -> evm_basic_grant (basic_grant_id));
|
||||||
diesel::joinable!(evm_ether_transfer_grant -> evm_ether_transfer_limit (limit_id));
|
diesel::joinable!(evm_ether_transfer_grant -> evm_ether_transfer_limit (limit_id));
|
||||||
diesel::joinable!(evm_ether_transfer_grant_target -> evm_ether_transfer_grant (grant_id));
|
diesel::joinable!(evm_ether_transfer_grant_target -> evm_ether_transfer_grant (grant_id));
|
||||||
@@ -169,11 +197,18 @@ diesel::joinable!(evm_token_transfer_grant -> evm_basic_grant (basic_grant_id));
|
|||||||
diesel::joinable!(evm_token_transfer_log -> evm_token_transfer_grant (grant_id));
|
diesel::joinable!(evm_token_transfer_log -> evm_token_transfer_grant (grant_id));
|
||||||
diesel::joinable!(evm_token_transfer_log -> evm_transaction_log (log_id));
|
diesel::joinable!(evm_token_transfer_log -> evm_transaction_log (log_id));
|
||||||
diesel::joinable!(evm_token_transfer_volume_limit -> evm_token_transfer_grant (grant_id));
|
diesel::joinable!(evm_token_transfer_volume_limit -> evm_token_transfer_grant (grant_id));
|
||||||
|
diesel::joinable!(evm_transaction_log -> evm_basic_grant (grant_id));
|
||||||
|
diesel::joinable!(evm_transaction_log -> evm_wallet_access (wallet_access_id));
|
||||||
diesel::joinable!(evm_wallet -> aead_encrypted (aead_encrypted_id));
|
diesel::joinable!(evm_wallet -> aead_encrypted (aead_encrypted_id));
|
||||||
|
diesel::joinable!(evm_wallet_access -> evm_wallet (wallet_id));
|
||||||
|
diesel::joinable!(evm_wallet_access -> program_client (client_id));
|
||||||
|
diesel::joinable!(program_client -> client_metadata (metadata_id));
|
||||||
|
|
||||||
diesel::allow_tables_to_appear_in_same_query!(
|
diesel::allow_tables_to_appear_in_same_query!(
|
||||||
aead_encrypted,
|
aead_encrypted,
|
||||||
arbiter_settings,
|
arbiter_settings,
|
||||||
|
client_metadata,
|
||||||
|
client_metadata_history,
|
||||||
evm_basic_grant,
|
evm_basic_grant,
|
||||||
evm_ether_transfer_grant,
|
evm_ether_transfer_grant,
|
||||||
evm_ether_transfer_grant_target,
|
evm_ether_transfer_grant_target,
|
||||||
@@ -183,6 +218,7 @@ diesel::allow_tables_to_appear_in_same_query!(
|
|||||||
evm_token_transfer_volume_limit,
|
evm_token_transfer_volume_limit,
|
||||||
evm_transaction_log,
|
evm_transaction_log,
|
||||||
evm_wallet,
|
evm_wallet,
|
||||||
|
evm_wallet_access,
|
||||||
program_client,
|
program_client,
|
||||||
root_key_history,
|
root_key_history,
|
||||||
tls_history,
|
tls_history,
|
||||||
|
|||||||
@@ -6,13 +6,16 @@ use alloy::{
|
|||||||
primitives::{TxKind, U256},
|
primitives::{TxKind, U256},
|
||||||
};
|
};
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use diesel::{ExpressionMethods as _, QueryDsl, QueryResult, insert_into, sqlite::Sqlite};
|
use diesel::{ExpressionMethods as _, QueryDsl as _, QueryResult, insert_into, sqlite::Sqlite};
|
||||||
use diesel_async::{AsyncConnection, RunQueryDsl};
|
use diesel_async::{AsyncConnection, RunQueryDsl};
|
||||||
|
use tracing_subscriber::registry::Data;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
db::{
|
db::{
|
||||||
self,
|
self, DatabaseError,
|
||||||
models::{EvmBasicGrant, NewEvmBasicGrant, NewEvmTransactionLog, SqliteTimestamp},
|
models::{
|
||||||
|
EvmBasicGrant, EvmWalletAccess, NewEvmBasicGrant, NewEvmTransactionLog, SqliteTimestamp,
|
||||||
|
},
|
||||||
schema::{self, evm_transaction_log},
|
schema::{self, evm_transaction_log},
|
||||||
},
|
},
|
||||||
evm::policies::{
|
evm::policies::{
|
||||||
@@ -28,12 +31,8 @@ mod utils;
|
|||||||
/// Errors that can only occur once the transaction meaning is known (during policy evaluation)
|
/// Errors that can only occur once the transaction meaning is known (during policy evaluation)
|
||||||
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
|
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
|
||||||
pub enum PolicyError {
|
pub enum PolicyError {
|
||||||
#[error("Database connection pool error")]
|
#[error("Database error")]
|
||||||
#[diagnostic(code(arbiter_server::evm::policy_error::pool))]
|
Error(#[from] crate::db::DatabaseError),
|
||||||
Pool(#[from] db::PoolError),
|
|
||||||
#[error("Database returned error")]
|
|
||||||
#[diagnostic(code(arbiter_server::evm::policy_error::database))]
|
|
||||||
Database(#[from] diesel::result::Error),
|
|
||||||
#[error("Transaction violates policy: {0:?}")]
|
#[error("Transaction violates policy: {0:?}")]
|
||||||
#[diagnostic(code(arbiter_server::evm::policy_error::violation))]
|
#[diagnostic(code(arbiter_server::evm::policy_error::violation))]
|
||||||
Violations(Vec<EvalViolation>),
|
Violations(Vec<EvalViolation>),
|
||||||
@@ -55,16 +54,6 @@ pub enum VetError {
|
|||||||
Evaluated(SpecificMeaning, #[source] PolicyError),
|
Evaluated(SpecificMeaning, #[source] PolicyError),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
|
|
||||||
pub enum SignError {
|
|
||||||
#[error("Database connection pool error")]
|
|
||||||
#[diagnostic(code(arbiter_server::evm::database_error))]
|
|
||||||
Pool(#[from] db::PoolError),
|
|
||||||
#[error("Database returned error")]
|
|
||||||
#[diagnostic(code(arbiter_server::evm::database_error))]
|
|
||||||
Database(#[from] diesel::result::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
|
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
|
||||||
pub enum AnalyzeError {
|
pub enum AnalyzeError {
|
||||||
#[error("Engine doesn't support granting permissions for contract creation")]
|
#[error("Engine doesn't support granting permissions for contract creation")]
|
||||||
@@ -76,28 +65,6 @@ pub enum AnalyzeError {
|
|||||||
UnsupportedTransactionType,
|
UnsupportedTransactionType,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
|
|
||||||
pub enum CreationError {
|
|
||||||
#[error("Database connection pool error")]
|
|
||||||
#[diagnostic(code(arbiter_server::evm::creation_error::database_error))]
|
|
||||||
Pool(#[from] db::PoolError),
|
|
||||||
|
|
||||||
#[error("Database returned error")]
|
|
||||||
#[diagnostic(code(arbiter_server::evm::creation_error::database_error))]
|
|
||||||
Database(#[from] diesel::result::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
|
|
||||||
pub enum ListGrantsError {
|
|
||||||
#[error("Database connection pool error")]
|
|
||||||
#[diagnostic(code(arbiter_server::evm::list_grants_error::pool))]
|
|
||||||
Pool(#[from] db::PoolError),
|
|
||||||
|
|
||||||
#[error("Database returned error")]
|
|
||||||
#[diagnostic(code(arbiter_server::evm::list_grants_error::database))]
|
|
||||||
Database(#[from] diesel::result::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Controls whether a transaction should be executed or only validated
|
/// Controls whether a transaction should be executed or only validated
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum RunKind {
|
pub enum RunKind {
|
||||||
@@ -165,16 +132,22 @@ impl Engine {
|
|||||||
meaning: &P::Meaning,
|
meaning: &P::Meaning,
|
||||||
run_kind: RunKind,
|
run_kind: RunKind,
|
||||||
) -> Result<(), PolicyError> {
|
) -> Result<(), PolicyError> {
|
||||||
let mut conn = self.db.get().await?;
|
let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
|
||||||
|
|
||||||
let grant = P::try_find_grant(&context, &mut conn)
|
let grant = P::try_find_grant(&context, &mut conn)
|
||||||
.await?
|
.await
|
||||||
|
.map_err(DatabaseError::from)?
|
||||||
.ok_or(PolicyError::NoMatchingGrant)?;
|
.ok_or(PolicyError::NoMatchingGrant)?;
|
||||||
|
|
||||||
let mut violations =
|
let mut violations =
|
||||||
check_shared_constraints(&context, &grant.shared, grant.shared_grant_id, &mut conn)
|
check_shared_constraints(&context, &grant.shared, grant.shared_grant_id, &mut conn)
|
||||||
.await?;
|
.await
|
||||||
violations.extend(P::evaluate(&context, meaning, &grant, &mut conn).await?);
|
.map_err(DatabaseError::from)?;
|
||||||
|
violations.extend(
|
||||||
|
P::evaluate(&context, meaning, &grant, &mut conn)
|
||||||
|
.await
|
||||||
|
.map_err(DatabaseError::from)?,
|
||||||
|
);
|
||||||
|
|
||||||
if !violations.is_empty() {
|
if !violations.is_empty() {
|
||||||
return Err(PolicyError::Violations(violations));
|
return Err(PolicyError::Violations(violations));
|
||||||
@@ -184,8 +157,7 @@ impl Engine {
|
|||||||
let log_id: i32 = insert_into(evm_transaction_log::table)
|
let log_id: i32 = insert_into(evm_transaction_log::table)
|
||||||
.values(&NewEvmTransactionLog {
|
.values(&NewEvmTransactionLog {
|
||||||
grant_id: grant.shared_grant_id,
|
grant_id: grant.shared_grant_id,
|
||||||
client_id: context.client_id,
|
wallet_access_id: context.target.id,
|
||||||
wallet_id: context.wallet_id,
|
|
||||||
chain_id: context.chain as i32,
|
chain_id: context.chain as i32,
|
||||||
eth_value: utils::u256_to_bytes(context.value).to_vec(),
|
eth_value: utils::u256_to_bytes(context.value).to_vec(),
|
||||||
signed_at: Utc::now().into(),
|
signed_at: Utc::now().into(),
|
||||||
@@ -199,7 +171,8 @@ impl Engine {
|
|||||||
QueryResult::Ok(())
|
QueryResult::Ok(())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.await?;
|
.await
|
||||||
|
.map_err(DatabaseError::from)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -213,9 +186,8 @@ impl Engine {
|
|||||||
|
|
||||||
pub async fn create_grant<P: Policy>(
|
pub async fn create_grant<P: Policy>(
|
||||||
&self,
|
&self,
|
||||||
client_id: i32,
|
|
||||||
full_grant: FullGrant<P::Settings>,
|
full_grant: FullGrant<P::Settings>,
|
||||||
) -> Result<i32, CreationError> {
|
) -> Result<i32, DatabaseError> {
|
||||||
let mut conn = self.db.get().await?;
|
let mut conn = self.db.get().await?;
|
||||||
|
|
||||||
let id = conn
|
let id = conn
|
||||||
@@ -225,9 +197,8 @@ impl Engine {
|
|||||||
|
|
||||||
let basic_grant: EvmBasicGrant = insert_into(evm_basic_grant::table)
|
let basic_grant: EvmBasicGrant = insert_into(evm_basic_grant::table)
|
||||||
.values(&NewEvmBasicGrant {
|
.values(&NewEvmBasicGrant {
|
||||||
wallet_id: full_grant.basic.wallet_id,
|
|
||||||
chain_id: full_grant.basic.chain as i32,
|
chain_id: full_grant.basic.chain as i32,
|
||||||
client_id,
|
wallet_access_id: full_grant.basic.wallet_access_id,
|
||||||
valid_from: full_grant.basic.valid_from.map(SqliteTimestamp),
|
valid_from: full_grant.basic.valid_from.map(SqliteTimestamp),
|
||||||
valid_until: full_grant.basic.valid_until.map(SqliteTimestamp),
|
valid_until: full_grant.basic.valid_until.map(SqliteTimestamp),
|
||||||
max_gas_fee_per_gas: full_grant
|
max_gas_fee_per_gas: full_grant
|
||||||
@@ -262,7 +233,7 @@ impl Engine {
|
|||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_all_grants(&self) -> Result<Vec<Grant<SpecificGrant>>, ListGrantsError> {
|
pub async fn list_all_grants(&self) -> Result<Vec<Grant<SpecificGrant>>, DatabaseError> {
|
||||||
let mut conn = self.db.get().await?;
|
let mut conn = self.db.get().await?;
|
||||||
|
|
||||||
let mut grants: Vec<Grant<SpecificGrant>> = Vec::new();
|
let mut grants: Vec<Grant<SpecificGrant>> = Vec::new();
|
||||||
@@ -295,8 +266,7 @@ impl Engine {
|
|||||||
|
|
||||||
pub async fn evaluate_transaction(
|
pub async fn evaluate_transaction(
|
||||||
&self,
|
&self,
|
||||||
wallet_id: i32,
|
target: EvmWalletAccess,
|
||||||
client_id: i32,
|
|
||||||
transaction: TxEip1559,
|
transaction: TxEip1559,
|
||||||
run_kind: RunKind,
|
run_kind: RunKind,
|
||||||
) -> Result<SpecificMeaning, VetError> {
|
) -> Result<SpecificMeaning, VetError> {
|
||||||
@@ -304,8 +274,7 @@ impl Engine {
|
|||||||
return Err(VetError::ContractCreationNotSupported);
|
return Err(VetError::ContractCreationNotSupported);
|
||||||
};
|
};
|
||||||
let context = policies::EvalContext {
|
let context = policies::EvalContext {
|
||||||
wallet_id,
|
target,
|
||||||
client_id,
|
|
||||||
chain: transaction.chain_id,
|
chain: transaction.chain_id,
|
||||||
to,
|
to,
|
||||||
value: transaction.value,
|
value: transaction.value,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use miette::Diagnostic;
|
|||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
db::models::{self, EvmBasicGrant},
|
db::models::{self, EvmBasicGrant, EvmWalletAccess},
|
||||||
evm::utils,
|
evm::utils,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -19,9 +19,8 @@ pub mod token_transfers;
|
|||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct EvalContext {
|
pub struct EvalContext {
|
||||||
// Which wallet is this transaction for
|
// Which wallet is this transaction for and who requested it
|
||||||
pub client_id: i32,
|
pub target: EvmWalletAccess,
|
||||||
pub wallet_id: i32,
|
|
||||||
|
|
||||||
// The transaction data
|
// The transaction data
|
||||||
pub chain: ChainId,
|
pub chain: ChainId,
|
||||||
@@ -145,8 +144,7 @@ pub struct VolumeRateLimit {
|
|||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
pub struct SharedGrantSettings {
|
pub struct SharedGrantSettings {
|
||||||
pub wallet_id: i32,
|
pub wallet_access_id: i32,
|
||||||
pub client_id: i32,
|
|
||||||
pub chain: ChainId,
|
pub chain: ChainId,
|
||||||
|
|
||||||
pub valid_from: Option<DateTime<Utc>>,
|
pub valid_from: Option<DateTime<Utc>>,
|
||||||
@@ -161,8 +159,7 @@ pub struct SharedGrantSettings {
|
|||||||
impl SharedGrantSettings {
|
impl SharedGrantSettings {
|
||||||
fn try_from_model(model: EvmBasicGrant) -> QueryResult<Self> {
|
fn try_from_model(model: EvmBasicGrant) -> QueryResult<Self> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
wallet_id: model.wallet_id,
|
wallet_access_id: model.wallet_access_id,
|
||||||
client_id: model.client_id,
|
|
||||||
chain: model.chain_id as u64, // safe because chain_id is stored as i32 but is guaranteed to be a valid ChainId by the API when creating grants
|
chain: model.chain_id as u64, // safe because chain_id is stored as i32 but is guaranteed to be a valid ChainId by the API when creating grants
|
||||||
valid_from: model.valid_from.map(Into::into),
|
valid_from: model.valid_from.map(Into::into),
|
||||||
valid_until: model.valid_until.map(Into::into),
|
valid_until: model.valid_until.map(Into::into),
|
||||||
|
|||||||
@@ -196,9 +196,8 @@ impl Policy for EtherTransfer {
|
|||||||
.inner_join(evm_basic_grant::table)
|
.inner_join(evm_basic_grant::table)
|
||||||
.inner_join(evm_ether_transfer_grant_target::table)
|
.inner_join(evm_ether_transfer_grant_target::table)
|
||||||
.filter(
|
.filter(
|
||||||
evm_basic_grant::wallet_id
|
evm_basic_grant::wallet_access_id
|
||||||
.eq(context.wallet_id)
|
.eq(context.target.id)
|
||||||
.and(evm_basic_grant::client_id.eq(context.client_id))
|
|
||||||
.and(evm_basic_grant::revoked_at.is_null())
|
.and(evm_basic_grant::revoked_at.is_null())
|
||||||
.and(evm_ether_transfer_grant_target::address.eq(&target_bytes)),
|
.and(evm_ether_transfer_grant_target::address.eq(&target_bytes)),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ use diesel_async::RunQueryDsl;
|
|||||||
|
|
||||||
use crate::db::{
|
use crate::db::{
|
||||||
self, DatabaseConnection,
|
self, DatabaseConnection,
|
||||||
models::{EvmBasicGrant, NewEvmBasicGrant, NewEvmTransactionLog, SqliteTimestamp},
|
models::{
|
||||||
|
EvmBasicGrant, EvmWalletAccess, NewEvmBasicGrant, NewEvmTransactionLog, SqliteTimestamp,
|
||||||
|
},
|
||||||
schema::{evm_basic_grant, evm_transaction_log},
|
schema::{evm_basic_grant, evm_transaction_log},
|
||||||
};
|
};
|
||||||
use crate::evm::{
|
use crate::evm::{
|
||||||
@@ -15,8 +17,7 @@ use crate::evm::{
|
|||||||
|
|
||||||
use super::{EtherTransfer, Settings};
|
use super::{EtherTransfer, Settings};
|
||||||
|
|
||||||
const WALLET_ID: i32 = 1;
|
const WALLET_ACCESS_ID: i32 = 1;
|
||||||
const CLIENT_ID: i32 = 2;
|
|
||||||
const CHAIN_ID: u64 = 1;
|
const CHAIN_ID: u64 = 1;
|
||||||
|
|
||||||
const ALLOWED: Address = address!("1111111111111111111111111111111111111111");
|
const ALLOWED: Address = address!("1111111111111111111111111111111111111111");
|
||||||
@@ -24,8 +25,12 @@ const OTHER: Address = address!("2222222222222222222222222222222222222222");
|
|||||||
|
|
||||||
fn ctx(to: Address, value: U256) -> EvalContext {
|
fn ctx(to: Address, value: U256) -> EvalContext {
|
||||||
EvalContext {
|
EvalContext {
|
||||||
wallet_id: WALLET_ID,
|
target: EvmWalletAccess {
|
||||||
client_id: CLIENT_ID,
|
id: WALLET_ACCESS_ID,
|
||||||
|
wallet_id: 10,
|
||||||
|
client_id: 20,
|
||||||
|
created_at: SqliteTimestamp(Utc::now()),
|
||||||
|
},
|
||||||
chain: CHAIN_ID,
|
chain: CHAIN_ID,
|
||||||
to,
|
to,
|
||||||
value,
|
value,
|
||||||
@@ -38,8 +43,7 @@ fn ctx(to: Address, value: U256) -> EvalContext {
|
|||||||
async fn insert_basic(conn: &mut DatabaseConnection, revoked: bool) -> EvmBasicGrant {
|
async fn insert_basic(conn: &mut DatabaseConnection, revoked: bool) -> EvmBasicGrant {
|
||||||
insert_into(evm_basic_grant::table)
|
insert_into(evm_basic_grant::table)
|
||||||
.values(NewEvmBasicGrant {
|
.values(NewEvmBasicGrant {
|
||||||
wallet_id: WALLET_ID,
|
wallet_access_id: WALLET_ACCESS_ID,
|
||||||
client_id: CLIENT_ID,
|
|
||||||
chain_id: CHAIN_ID as i32,
|
chain_id: CHAIN_ID as i32,
|
||||||
valid_from: None,
|
valid_from: None,
|
||||||
valid_until: None,
|
valid_until: None,
|
||||||
@@ -67,14 +71,13 @@ fn make_settings(targets: Vec<Address>, max_volume: u64) -> Settings {
|
|||||||
|
|
||||||
fn shared() -> SharedGrantSettings {
|
fn shared() -> SharedGrantSettings {
|
||||||
SharedGrantSettings {
|
SharedGrantSettings {
|
||||||
wallet_id: WALLET_ID,
|
wallet_access_id: WALLET_ACCESS_ID,
|
||||||
chain: CHAIN_ID,
|
chain: CHAIN_ID,
|
||||||
valid_from: None,
|
valid_from: None,
|
||||||
valid_until: None,
|
valid_until: None,
|
||||||
max_gas_fee_per_gas: None,
|
max_gas_fee_per_gas: None,
|
||||||
max_priority_fee_per_gas: None,
|
max_priority_fee_per_gas: None,
|
||||||
rate_limit: None,
|
rate_limit: None,
|
||||||
client_id: CLIENT_ID,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,8 +156,7 @@ async fn evaluate_passes_when_volume_within_limit() {
|
|||||||
insert_into(evm_transaction_log::table)
|
insert_into(evm_transaction_log::table)
|
||||||
.values(NewEvmTransactionLog {
|
.values(NewEvmTransactionLog {
|
||||||
grant_id,
|
grant_id,
|
||||||
client_id: CLIENT_ID,
|
wallet_access_id: WALLET_ACCESS_ID,
|
||||||
wallet_id: WALLET_ID,
|
|
||||||
chain_id: CHAIN_ID as i32,
|
chain_id: CHAIN_ID as i32,
|
||||||
eth_value: utils::u256_to_bytes(U256::from(500u64)).to_vec(),
|
eth_value: utils::u256_to_bytes(U256::from(500u64)).to_vec(),
|
||||||
signed_at: SqliteTimestamp(Utc::now()),
|
signed_at: SqliteTimestamp(Utc::now()),
|
||||||
@@ -194,8 +196,7 @@ async fn evaluate_rejects_volume_over_limit() {
|
|||||||
insert_into(evm_transaction_log::table)
|
insert_into(evm_transaction_log::table)
|
||||||
.values(NewEvmTransactionLog {
|
.values(NewEvmTransactionLog {
|
||||||
grant_id,
|
grant_id,
|
||||||
client_id: CLIENT_ID,
|
wallet_access_id: WALLET_ACCESS_ID,
|
||||||
wallet_id: WALLET_ID,
|
|
||||||
chain_id: CHAIN_ID as i32,
|
chain_id: CHAIN_ID as i32,
|
||||||
eth_value: utils::u256_to_bytes(U256::from(1_001u64)).to_vec(),
|
eth_value: utils::u256_to_bytes(U256::from(1_001u64)).to_vec(),
|
||||||
signed_at: SqliteTimestamp(Utc::now()),
|
signed_at: SqliteTimestamp(Utc::now()),
|
||||||
@@ -236,8 +237,7 @@ async fn evaluate_passes_at_exactly_volume_limit() {
|
|||||||
insert_into(evm_transaction_log::table)
|
insert_into(evm_transaction_log::table)
|
||||||
.values(NewEvmTransactionLog {
|
.values(NewEvmTransactionLog {
|
||||||
grant_id,
|
grant_id,
|
||||||
client_id: CLIENT_ID,
|
wallet_access_id: WALLET_ACCESS_ID,
|
||||||
wallet_id: WALLET_ID,
|
|
||||||
chain_id: CHAIN_ID as i32,
|
chain_id: CHAIN_ID as i32,
|
||||||
eth_value: utils::u256_to_bytes(U256::from(1_000u64)).to_vec(),
|
eth_value: utils::u256_to_bytes(U256::from(1_000u64)).to_vec(),
|
||||||
signed_at: SqliteTimestamp(Utc::now()),
|
signed_at: SqliteTimestamp(Utc::now()),
|
||||||
|
|||||||
@@ -209,8 +209,7 @@ impl Policy for TokenTransfer {
|
|||||||
|
|
||||||
let grant: Option<(EvmBasicGrant, EvmTokenTransferGrant)> = grant_join()
|
let grant: Option<(EvmBasicGrant, EvmTokenTransferGrant)> = grant_join()
|
||||||
.filter(evm_basic_grant::revoked_at.is_null())
|
.filter(evm_basic_grant::revoked_at.is_null())
|
||||||
.filter(evm_basic_grant::wallet_id.eq(context.wallet_id))
|
.filter(evm_basic_grant::wallet_access_id.eq(context.target.id))
|
||||||
.filter(evm_basic_grant::client_id.eq(context.client_id))
|
|
||||||
.filter(evm_token_transfer_grant::token_contract.eq(&token_contract_bytes))
|
.filter(evm_token_transfer_grant::token_contract.eq(&token_contract_bytes))
|
||||||
.select((
|
.select((
|
||||||
EvmBasicGrant::as_select(),
|
EvmBasicGrant::as_select(),
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use diesel_async::RunQueryDsl;
|
|||||||
|
|
||||||
use crate::db::{
|
use crate::db::{
|
||||||
self, DatabaseConnection,
|
self, DatabaseConnection,
|
||||||
models::{EvmBasicGrant, NewEvmBasicGrant, SqliteTimestamp},
|
models::{EvmBasicGrant, EvmWalletAccess, NewEvmBasicGrant, SqliteTimestamp},
|
||||||
schema::evm_basic_grant,
|
schema::evm_basic_grant,
|
||||||
};
|
};
|
||||||
use crate::evm::{
|
use crate::evm::{
|
||||||
@@ -21,8 +21,7 @@ use super::{Settings, TokenTransfer};
|
|||||||
const CHAIN_ID: u64 = 1;
|
const CHAIN_ID: u64 = 1;
|
||||||
const DAI: Address = address!("6B175474E89094C44Da98b954EedeAC495271d0F");
|
const DAI: Address = address!("6B175474E89094C44Da98b954EedeAC495271d0F");
|
||||||
|
|
||||||
const WALLET_ID: i32 = 1;
|
const WALLET_ACCESS_ID: i32 = 1;
|
||||||
const CLIENT_ID: i32 = 2;
|
|
||||||
|
|
||||||
const RECIPIENT: Address = address!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
|
const RECIPIENT: Address = address!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
|
||||||
const OTHER: Address = address!("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
|
const OTHER: Address = address!("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
|
||||||
@@ -38,8 +37,12 @@ fn transfer_calldata(to: Address, value: U256) -> Bytes {
|
|||||||
|
|
||||||
fn ctx(to: Address, calldata: Bytes) -> EvalContext {
|
fn ctx(to: Address, calldata: Bytes) -> EvalContext {
|
||||||
EvalContext {
|
EvalContext {
|
||||||
wallet_id: WALLET_ID,
|
target: EvmWalletAccess {
|
||||||
client_id: CLIENT_ID,
|
id: WALLET_ACCESS_ID,
|
||||||
|
wallet_id: 10,
|
||||||
|
client_id: 20,
|
||||||
|
created_at: SqliteTimestamp(Utc::now()),
|
||||||
|
},
|
||||||
chain: CHAIN_ID,
|
chain: CHAIN_ID,
|
||||||
to,
|
to,
|
||||||
value: U256::ZERO,
|
value: U256::ZERO,
|
||||||
@@ -52,8 +55,7 @@ fn ctx(to: Address, calldata: Bytes) -> EvalContext {
|
|||||||
async fn insert_basic(conn: &mut DatabaseConnection, revoked: bool) -> EvmBasicGrant {
|
async fn insert_basic(conn: &mut DatabaseConnection, revoked: bool) -> EvmBasicGrant {
|
||||||
insert_into(evm_basic_grant::table)
|
insert_into(evm_basic_grant::table)
|
||||||
.values(NewEvmBasicGrant {
|
.values(NewEvmBasicGrant {
|
||||||
wallet_id: WALLET_ID,
|
wallet_access_id: WALLET_ACCESS_ID,
|
||||||
client_id: CLIENT_ID,
|
|
||||||
chain_id: CHAIN_ID as i32,
|
chain_id: CHAIN_ID as i32,
|
||||||
valid_from: None,
|
valid_from: None,
|
||||||
valid_until: None,
|
valid_until: None,
|
||||||
@@ -86,14 +88,13 @@ fn make_settings(target: Option<Address>, max_volume: Option<u64>) -> Settings {
|
|||||||
|
|
||||||
fn shared() -> SharedGrantSettings {
|
fn shared() -> SharedGrantSettings {
|
||||||
SharedGrantSettings {
|
SharedGrantSettings {
|
||||||
wallet_id: WALLET_ID,
|
wallet_access_id: WALLET_ACCESS_ID,
|
||||||
chain: CHAIN_ID,
|
chain: CHAIN_ID,
|
||||||
valid_from: None,
|
valid_from: None,
|
||||||
valid_until: None,
|
valid_until: None,
|
||||||
max_gas_fee_per_gas: None,
|
max_gas_fee_per_gas: None,
|
||||||
max_priority_fee_per_gas: None,
|
max_priority_fee_per_gas: None,
|
||||||
rate_limit: None,
|
rate_limit: None,
|
||||||
client_id: CLIENT_ID,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,137 +1,118 @@
|
|||||||
use arbiter_proto::{
|
use arbiter_proto::{
|
||||||
proto::client::{
|
proto::client::{
|
||||||
AuthChallenge as ProtoAuthChallenge,
|
ClientRequest, ClientResponse, VaultState as ProtoVaultState,
|
||||||
AuthChallengeRequest as ProtoAuthChallengeRequest,
|
|
||||||
AuthChallengeSolution as ProtoAuthChallengeSolution, AuthOk as ProtoAuthOk,
|
|
||||||
ClientConnectError, ClientRequest, ClientResponse,
|
|
||||||
client_connect_error::Code as ProtoClientConnectErrorCode,
|
|
||||||
client_request::Payload as ClientRequestPayload,
|
client_request::Payload as ClientRequestPayload,
|
||||||
client_response::Payload as ClientResponsePayload,
|
client_response::Payload as ClientResponsePayload,
|
||||||
},
|
},
|
||||||
transport::{Bi, Error as TransportError},
|
transport::{Receiver, Sender, grpc::GrpcBi},
|
||||||
};
|
};
|
||||||
use async_trait::async_trait;
|
use kameo::{
|
||||||
use futures::StreamExt as _;
|
actor::{ActorRef, Spawn as _},
|
||||||
use tokio::sync::mpsc;
|
error::SendError,
|
||||||
use tonic::{Status, Streaming};
|
|
||||||
|
|
||||||
use crate::actors::client::{
|
|
||||||
self, ClientError, ConnectErrorCode, Request as DomainRequest, Response as DomainResponse,
|
|
||||||
};
|
};
|
||||||
|
use tonic::Status;
|
||||||
|
use tracing::{info, warn};
|
||||||
|
|
||||||
pub struct GrpcTransport {
|
use crate::{
|
||||||
sender: mpsc::Sender<Result<ClientResponse, Status>>,
|
actors::{
|
||||||
receiver: Streaming<ClientRequest>,
|
client::{
|
||||||
}
|
self, ClientConnection,
|
||||||
|
session::{ClientSession, Error, HandleQueryVaultState},
|
||||||
impl GrpcTransport {
|
|
||||||
pub fn new(
|
|
||||||
sender: mpsc::Sender<Result<ClientResponse, Status>>,
|
|
||||||
receiver: Streaming<ClientRequest>,
|
|
||||||
) -> Self {
|
|
||||||
Self { sender, receiver }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn request_to_domain(request: ClientRequest) -> Result<DomainRequest, Status> {
|
|
||||||
match request.payload {
|
|
||||||
Some(ClientRequestPayload::AuthChallengeRequest(ProtoAuthChallengeRequest {
|
|
||||||
pubkey,
|
|
||||||
})) => Ok(DomainRequest::AuthChallengeRequest { pubkey }),
|
|
||||||
Some(ClientRequestPayload::AuthChallengeSolution(
|
|
||||||
ProtoAuthChallengeSolution { signature },
|
|
||||||
)) => Ok(DomainRequest::AuthChallengeSolution { signature }),
|
|
||||||
None => Err(Status::invalid_argument("Missing client request payload")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn response_to_proto(response: DomainResponse) -> ClientResponse {
|
|
||||||
let payload = match response {
|
|
||||||
DomainResponse::AuthChallenge { pubkey, nonce } => {
|
|
||||||
ClientResponsePayload::AuthChallenge(ProtoAuthChallenge { pubkey, nonce })
|
|
||||||
}
|
|
||||||
DomainResponse::AuthOk => ClientResponsePayload::AuthOk(ProtoAuthOk {}),
|
|
||||||
DomainResponse::ClientConnectError { code } => {
|
|
||||||
ClientResponsePayload::ClientConnectError(ClientConnectError {
|
|
||||||
code: match code {
|
|
||||||
ConnectErrorCode::Unknown => ProtoClientConnectErrorCode::Unknown,
|
|
||||||
ConnectErrorCode::ApprovalDenied => {
|
|
||||||
ProtoClientConnectErrorCode::ApprovalDenied
|
|
||||||
}
|
|
||||||
ConnectErrorCode::NoUserAgentsOnline => {
|
|
||||||
ProtoClientConnectErrorCode::NoUserAgentsOnline
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.into(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ClientResponse {
|
|
||||||
payload: Some(payload),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn error_to_status(value: ClientError) -> Status {
|
|
||||||
match value {
|
|
||||||
ClientError::MissingRequestPayload | ClientError::UnexpectedRequestPayload => {
|
|
||||||
Status::invalid_argument("Expected message with payload")
|
|
||||||
}
|
|
||||||
ClientError::StateTransitionFailed => Status::internal("State machine error"),
|
|
||||||
ClientError::Auth(ref err) => auth_error_status(err),
|
|
||||||
ClientError::ConnectionRegistrationFailed => {
|
|
||||||
Status::internal("Connection registration failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Bi<DomainRequest, Result<DomainResponse, ClientError>> for GrpcTransport {
|
|
||||||
async fn send(&mut self, item: Result<DomainResponse, ClientError>) -> Result<(), TransportError> {
|
|
||||||
let outbound = match item {
|
|
||||||
Ok(message) => Ok(Self::response_to_proto(message)),
|
|
||||||
Err(err) => Err(Self::error_to_status(err)),
|
|
||||||
};
|
|
||||||
|
|
||||||
self.sender
|
|
||||||
.send(outbound)
|
|
||||||
.await
|
|
||||||
.map_err(|_| TransportError::ChannelClosed)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn recv(&mut self) -> Option<DomainRequest> {
|
|
||||||
match self.receiver.next().await {
|
|
||||||
Some(Ok(item)) => match Self::request_to_domain(item) {
|
|
||||||
Ok(request) => Some(request),
|
|
||||||
Err(status) => {
|
|
||||||
let _ = self.sender.send(Err(status)).await;
|
|
||||||
None
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
Some(Err(error)) => {
|
keyholder::KeyHolderState,
|
||||||
tracing::error!(error = ?error, "grpc client recv failed; closing stream");
|
},
|
||||||
None
|
grpc::request_tracker::RequestTracker,
|
||||||
|
};
|
||||||
|
|
||||||
|
mod auth;
|
||||||
|
mod inbound;
|
||||||
|
mod outbound;
|
||||||
|
|
||||||
|
async fn dispatch_loop(
|
||||||
|
mut bi: GrpcBi<ClientRequest, ClientResponse>,
|
||||||
|
actor: ActorRef<ClientSession>,
|
||||||
|
mut request_tracker: RequestTracker,
|
||||||
|
) {
|
||||||
|
loop {
|
||||||
|
let Some(message) = bi.recv().await else { return };
|
||||||
|
|
||||||
|
let conn = match message {
|
||||||
|
Ok(conn) => conn,
|
||||||
|
Err(err) => {
|
||||||
|
warn!(error = ?err, "Failed to receive client request");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let request_id = match request_tracker.request(conn.request_id) {
|
||||||
|
Ok(id) => id,
|
||||||
|
Err(err) => {
|
||||||
|
let _ = bi.send(Err(err)).await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(payload) = conn.payload else {
|
||||||
|
let _ = bi.send(Err(Status::invalid_argument("Missing client request payload"))).await;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
match dispatch_inner(&actor, payload).await {
|
||||||
|
Ok(response) => {
|
||||||
|
if bi.send(Ok(ClientResponse {
|
||||||
|
request_id: Some(request_id),
|
||||||
|
payload: Some(response),
|
||||||
|
})).await.is_err() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(status) => {
|
||||||
|
let _ = bi.send(Err(status)).await;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
None => None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn auth_error_status(value: &client::auth::Error) -> Status {
|
async fn dispatch_inner(
|
||||||
use client::auth::Error;
|
actor: &ActorRef<ClientSession>,
|
||||||
|
payload: ClientRequestPayload,
|
||||||
match value {
|
) -> Result<ClientResponsePayload, Status> {
|
||||||
Error::UnexpectedMessagePayload | Error::InvalidClientPubkeyLength => {
|
match payload {
|
||||||
Status::invalid_argument(value.to_string())
|
ClientRequestPayload::QueryVaultState(_) => {
|
||||||
|
let state = match actor.ask(HandleQueryVaultState {}).await {
|
||||||
|
Ok(KeyHolderState::Unbootstrapped) => ProtoVaultState::Unbootstrapped,
|
||||||
|
Ok(KeyHolderState::Sealed) => ProtoVaultState::Sealed,
|
||||||
|
Ok(KeyHolderState::Unsealed) => ProtoVaultState::Unsealed,
|
||||||
|
Err(SendError::HandlerError(Error::Internal)) => ProtoVaultState::Error,
|
||||||
|
Err(err) => {
|
||||||
|
warn!(error = ?err, "Failed to query vault state");
|
||||||
|
ProtoVaultState::Error
|
||||||
}
|
}
|
||||||
Error::InvalidAuthPubkeyEncoding => {
|
};
|
||||||
Status::invalid_argument("Failed to convert pubkey to VerifyingKey")
|
Ok(ClientResponsePayload::VaultState(state.into()))
|
||||||
|
}
|
||||||
|
payload => {
|
||||||
|
warn!(?payload, "Unsupported post-auth client request");
|
||||||
|
Err(Status::invalid_argument("Unsupported client request"))
|
||||||
}
|
}
|
||||||
Error::InvalidChallengeSolution => Status::unauthenticated(value.to_string()),
|
|
||||||
Error::ApproveError(_) => Status::permission_denied(value.to_string()),
|
|
||||||
Error::Transport => Status::internal("Transport error"),
|
|
||||||
Error::DatabasePoolUnavailable => Status::internal("Database pool error"),
|
|
||||||
Error::DatabaseOperationFailed => Status::internal("Database error"),
|
|
||||||
Error::InternalError => Status::internal("Internal error"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn start(mut conn: ClientConnection, mut bi: GrpcBi<ClientRequest, ClientResponse>) {
|
||||||
|
let mut request_tracker = RequestTracker::default();
|
||||||
|
|
||||||
|
if let Err(e) = auth::start(&mut conn, &mut bi, &mut request_tracker).await {
|
||||||
|
let mut transport = auth::AuthTransportAdapter::new(&mut bi, &mut request_tracker);
|
||||||
|
let _ = transport.send(Err(e.clone())).await;
|
||||||
|
warn!(error = ?e, "Client authentication failed");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let actor = client::session::ClientSession::spawn(client::session::ClientSession::new(conn));
|
||||||
|
let actor_for_cleanup = actor.clone();
|
||||||
|
|
||||||
|
info!("Client authenticated successfully");
|
||||||
|
dispatch_loop(bi, actor, request_tracker).await;
|
||||||
|
actor_for_cleanup.kill();
|
||||||
|
}
|
||||||
|
|||||||
188
server/crates/arbiter-server/src/grpc/client/auth.rs
Normal file
188
server/crates/arbiter-server/src/grpc/client/auth.rs
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
use arbiter_proto::{
|
||||||
|
ClientMetadata, proto::client::{
|
||||||
|
AuthChallenge as ProtoAuthChallenge, AuthChallengeRequest as ProtoAuthChallengeRequest,
|
||||||
|
AuthChallengeSolution as ProtoAuthChallengeSolution, AuthResult as ProtoAuthResult,
|
||||||
|
ClientInfo as ProtoClientInfo, ClientRequest, ClientResponse,
|
||||||
|
client_request::Payload as ClientRequestPayload,
|
||||||
|
client_response::Payload as ClientResponsePayload,
|
||||||
|
}, transport::{Bi, Error as TransportError, Receiver, Sender, grpc::GrpcBi}
|
||||||
|
};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use tonic::Status;
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
actors::client::{self, ClientConnection, auth},
|
||||||
|
grpc::request_tracker::RequestTracker,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct AuthTransportAdapter<'a> {
|
||||||
|
bi: &'a mut GrpcBi<ClientRequest, ClientResponse>,
|
||||||
|
request_tracker: &'a mut RequestTracker,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> AuthTransportAdapter<'a> {
|
||||||
|
pub fn new(
|
||||||
|
bi: &'a mut GrpcBi<ClientRequest, ClientResponse>,
|
||||||
|
request_tracker: &'a mut RequestTracker,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
bi,
|
||||||
|
request_tracker,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn response_to_proto(response: auth::Outbound) -> ClientResponsePayload {
|
||||||
|
match response {
|
||||||
|
auth::Outbound::AuthChallenge { pubkey, nonce } => {
|
||||||
|
ClientResponsePayload::AuthChallenge(ProtoAuthChallenge {
|
||||||
|
pubkey: pubkey.to_bytes().to_vec(),
|
||||||
|
nonce,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
auth::Outbound::AuthSuccess => {
|
||||||
|
ClientResponsePayload::AuthResult(ProtoAuthResult::Success.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn error_to_proto(error: auth::Error) -> ClientResponsePayload {
|
||||||
|
ClientResponsePayload::AuthResult(
|
||||||
|
match error {
|
||||||
|
auth::Error::InvalidChallengeSolution => ProtoAuthResult::InvalidSignature,
|
||||||
|
auth::Error::ApproveError(auth::ApproveError::Denied) => {
|
||||||
|
ProtoAuthResult::ApprovalDenied
|
||||||
|
}
|
||||||
|
auth::Error::ApproveError(auth::ApproveError::Upstream(
|
||||||
|
crate::actors::flow_coordinator::ApprovalError::NoUserAgentsConnected,
|
||||||
|
)) => ProtoAuthResult::NoUserAgentsOnline,
|
||||||
|
auth::Error::ApproveError(auth::ApproveError::Internal)
|
||||||
|
| auth::Error::DatabasePoolUnavailable
|
||||||
|
| auth::Error::DatabaseOperationFailed
|
||||||
|
| auth::Error::Transport => ProtoAuthResult::Internal,
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_client_response(
|
||||||
|
&mut self,
|
||||||
|
payload: ClientResponsePayload,
|
||||||
|
) -> Result<(), TransportError> {
|
||||||
|
self.bi
|
||||||
|
.send(Ok(ClientResponse {
|
||||||
|
request_id: Some(self.request_tracker.current_request_id()),
|
||||||
|
payload: Some(payload),
|
||||||
|
}))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_auth_result(&mut self, result: ProtoAuthResult) -> Result<(), TransportError> {
|
||||||
|
self.send_client_response(ClientResponsePayload::AuthResult(result.into()))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Sender<Result<auth::Outbound, auth::Error>> for AuthTransportAdapter<'_> {
|
||||||
|
async fn send(
|
||||||
|
&mut self,
|
||||||
|
item: Result<auth::Outbound, auth::Error>,
|
||||||
|
) -> Result<(), TransportError> {
|
||||||
|
let payload = match item {
|
||||||
|
Ok(message) => AuthTransportAdapter::response_to_proto(message),
|
||||||
|
Err(err) => AuthTransportAdapter::error_to_proto(err),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.send_client_response(payload).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
|
||||||
|
async fn recv(&mut self) -> Option<auth::Inbound> {
|
||||||
|
let request = match self.bi.recv().await? {
|
||||||
|
Ok(request) => request,
|
||||||
|
Err(error) => {
|
||||||
|
warn!(error = ?error, "grpc client recv failed; closing stream");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match self.request_tracker.request(request.request_id) {
|
||||||
|
Ok(request_id) => request_id,
|
||||||
|
Err(error) => {
|
||||||
|
let _ = self.bi.send(Err(error)).await;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let payload = request.payload?;
|
||||||
|
|
||||||
|
match payload {
|
||||||
|
ClientRequestPayload::AuthChallengeRequest(ProtoAuthChallengeRequest {
|
||||||
|
pubkey,
|
||||||
|
client_info,
|
||||||
|
}) => {
|
||||||
|
let Some(client_info) = client_info else {
|
||||||
|
let _ = self
|
||||||
|
.bi
|
||||||
|
.send(Err(Status::invalid_argument("Missing client info")))
|
||||||
|
.await;
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let Ok(pubkey) = <[u8; 32]>::try_from(pubkey) else {
|
||||||
|
let _ = self.send_auth_result(ProtoAuthResult::InvalidKey).await;
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let Ok(pubkey) = ed25519_dalek::VerifyingKey::from_bytes(&pubkey) else {
|
||||||
|
let _ = self.send_auth_result(ProtoAuthResult::InvalidKey).await;
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
Some(auth::Inbound::AuthChallengeRequest {
|
||||||
|
pubkey,
|
||||||
|
metadata: client_metadata_from_proto(client_info),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ClientRequestPayload::AuthChallengeSolution(ProtoAuthChallengeSolution {
|
||||||
|
signature,
|
||||||
|
}) => {
|
||||||
|
let Ok(signature) = ed25519_dalek::Signature::try_from(signature.as_slice()) else {
|
||||||
|
let _ = self
|
||||||
|
.send_auth_result(ProtoAuthResult::InvalidSignature)
|
||||||
|
.await;
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
Some(auth::Inbound::AuthChallengeSolution { signature })
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let _ = self
|
||||||
|
.bi
|
||||||
|
.send(Err(Status::invalid_argument(
|
||||||
|
"Unsupported client auth request",
|
||||||
|
)))
|
||||||
|
.await;
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Bi<auth::Inbound, Result<auth::Outbound, auth::Error>> for AuthTransportAdapter<'_> {}
|
||||||
|
|
||||||
|
fn client_metadata_from_proto(metadata: ProtoClientInfo) -> ClientMetadata {
|
||||||
|
ClientMetadata {
|
||||||
|
name: metadata.name,
|
||||||
|
description: metadata.description,
|
||||||
|
version: metadata.version,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn start(
|
||||||
|
conn: &mut ClientConnection,
|
||||||
|
bi: &mut GrpcBi<ClientRequest, ClientResponse>,
|
||||||
|
request_tracker: &mut RequestTracker,
|
||||||
|
) -> Result<(), auth::Error> {
|
||||||
|
let mut transport = AuthTransportAdapter::new(bi, request_tracker);
|
||||||
|
client::auth::authenticate(conn, &mut transport).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -1,21 +1,36 @@
|
|||||||
|
use arbiter_proto::{
|
||||||
use arbiter_proto::proto::{
|
proto::{
|
||||||
client::{ClientRequest, ClientResponse},
|
client::{ClientRequest, ClientResponse},
|
||||||
user_agent::{UserAgentRequest, UserAgentResponse},
|
user_agent::{UserAgentRequest, UserAgentResponse},
|
||||||
|
},
|
||||||
|
transport::grpc::GrpcBi,
|
||||||
};
|
};
|
||||||
use tokio::sync::mpsc;
|
|
||||||
use tokio_stream::wrappers::ReceiverStream;
|
use tokio_stream::wrappers::ReceiverStream;
|
||||||
use tonic::{Request, Response, Status, async_trait};
|
use tonic::{Request, Response, Status, async_trait};
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
DEFAULT_CHANNEL_SIZE,
|
actors::{client::ClientConnection, user_agent::UserAgentConnection},
|
||||||
actors::{client::{ClientConnection, connect_client}, user_agent::{UserAgentConnection, connect_user_agent}},
|
grpc::user_agent::start,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub mod client;
|
pub mod client;
|
||||||
|
mod request_tracker;
|
||||||
pub mod user_agent;
|
pub mod user_agent;
|
||||||
|
|
||||||
|
pub trait Convert {
|
||||||
|
type Output;
|
||||||
|
|
||||||
|
fn convert(self) -> Self::Output;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait TryConvert {
|
||||||
|
type Output;
|
||||||
|
type Error;
|
||||||
|
|
||||||
|
fn try_convert(self) -> Result<Self::Output, Self::Error>;
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl arbiter_proto::proto::arbiter_service_server::ArbiterService for super::Server {
|
impl arbiter_proto::proto::arbiter_service_server::ArbiterService for super::Server {
|
||||||
type UserAgentStream = ReceiverStream<Result<UserAgentResponse, Status>>;
|
type UserAgentStream = ReceiverStream<Result<UserAgentResponse, Status>>;
|
||||||
@@ -27,19 +42,13 @@ impl arbiter_proto::proto::arbiter_service_server::ArbiterService for super::Ser
|
|||||||
request: Request<tonic::Streaming<ClientRequest>>,
|
request: Request<tonic::Streaming<ClientRequest>>,
|
||||||
) -> Result<Response<Self::ClientStream>, Status> {
|
) -> Result<Response<Self::ClientStream>, Status> {
|
||||||
let req_stream = request.into_inner();
|
let req_stream = request.into_inner();
|
||||||
let (tx, rx) = mpsc::channel(DEFAULT_CHANNEL_SIZE);
|
let (bi, rx) = GrpcBi::from_bi_stream(req_stream);
|
||||||
|
let props = ClientConnection::new(self.context.db.clone(), self.context.actors.clone());
|
||||||
let transport = client::GrpcTransport::new(tx, req_stream);
|
tokio::spawn(client::start(props, bi));
|
||||||
let props = ClientConnection::new(
|
|
||||||
self.context.db.clone(),
|
|
||||||
Box::new(transport),
|
|
||||||
self.context.actors.clone(),
|
|
||||||
);
|
|
||||||
tokio::spawn(connect_client(props));
|
|
||||||
|
|
||||||
info!(event = "connection established", "grpc.client");
|
info!(event = "connection established", "grpc.client");
|
||||||
|
|
||||||
Ok(Response::new(ReceiverStream::new(rx)))
|
Ok(Response::new(rx))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(level = "debug", skip(self))]
|
#[tracing::instrument(level = "debug", skip(self))]
|
||||||
@@ -48,18 +57,19 @@ impl arbiter_proto::proto::arbiter_service_server::ArbiterService for super::Ser
|
|||||||
request: Request<tonic::Streaming<UserAgentRequest>>,
|
request: Request<tonic::Streaming<UserAgentRequest>>,
|
||||||
) -> Result<Response<Self::UserAgentStream>, Status> {
|
) -> Result<Response<Self::UserAgentStream>, Status> {
|
||||||
let req_stream = request.into_inner();
|
let req_stream = request.into_inner();
|
||||||
let (tx, rx) = mpsc::channel(DEFAULT_CHANNEL_SIZE);
|
|
||||||
|
|
||||||
let transport = user_agent::GrpcTransport::new(tx, req_stream);
|
let (bi, rx) = GrpcBi::from_bi_stream(req_stream);
|
||||||
let props = UserAgentConnection::new(
|
|
||||||
self.context.db.clone(),
|
tokio::spawn(start(
|
||||||
self.context.actors.clone(),
|
UserAgentConnection {
|
||||||
Box::new(transport),
|
db: self.context.db.clone(),
|
||||||
);
|
actors: self.context.actors.clone(),
|
||||||
tokio::spawn(connect_user_agent(props));
|
},
|
||||||
|
bi,
|
||||||
|
));
|
||||||
|
|
||||||
info!(event = "connection established", "grpc.user_agent");
|
info!(event = "connection established", "grpc.user_agent");
|
||||||
|
|
||||||
Ok(Response::new(ReceiverStream::new(rx)))
|
Ok(Response::new(rx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
26
server/crates/arbiter-server/src/grpc/request_tracker.rs
Normal file
26
server/crates/arbiter-server/src/grpc/request_tracker.rs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
use tonic::Status;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct RequestTracker {
|
||||||
|
next_request_id: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RequestTracker {
|
||||||
|
pub fn request(&mut self, id: i32) -> Result<i32, Status> {
|
||||||
|
if id < self.next_request_id {
|
||||||
|
return Err(Status::invalid_argument("Duplicate request id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.next_request_id = id
|
||||||
|
.checked_add(1)
|
||||||
|
.ok_or_else(|| Status::invalid_argument("Invalid request id"))?;
|
||||||
|
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is used to set the response id for auth responses, which need to match the request id of the auth challenge request.
|
||||||
|
// -1 offset is needed because request() increments the next_request_id after returning the current request id.
|
||||||
|
pub fn current_request_id(&self) -> i32 {
|
||||||
|
self.next_request_id - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,509 +1,465 @@
|
|||||||
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
use arbiter_proto::{
|
use arbiter_proto::{
|
||||||
proto::{
|
proto::{
|
||||||
self,
|
client::ClientInfo as ProtoClientMetadata,
|
||||||
evm::{
|
evm::{
|
||||||
EtherTransferSettings as ProtoEtherTransferSettings, EvmError as ProtoEvmError,
|
EvmError as ProtoEvmError, EvmGrantCreateRequest, EvmGrantCreateResponse,
|
||||||
EvmGrantCreateRequest, EvmGrantCreateResponse, EvmGrantDeleteRequest,
|
EvmGrantDeleteRequest, EvmGrantDeleteResponse, EvmGrantList, EvmGrantListResponse,
|
||||||
EvmGrantDeleteResponse, EvmGrantList, EvmGrantListResponse, GrantEntry,
|
GrantEntry, WalletCreateResponse, WalletEntry, WalletList, WalletListResponse,
|
||||||
SharedSettings as ProtoSharedSettings, SpecificGrant as ProtoSpecificGrant,
|
evm_grant_create_response::Result as EvmGrantCreateResult,
|
||||||
TokenTransferSettings as ProtoTokenTransferSettings,
|
|
||||||
VolumeRateLimit as ProtoVolumeRateLimit, WalletCreateResponse, WalletEntry, WalletList,
|
|
||||||
WalletListResponse, evm_grant_create_response::Result as EvmGrantCreateResult,
|
|
||||||
evm_grant_delete_response::Result as EvmGrantDeleteResult,
|
evm_grant_delete_response::Result as EvmGrantDeleteResult,
|
||||||
evm_grant_list_response::Result as EvmGrantListResult,
|
evm_grant_list_response::Result as EvmGrantListResult,
|
||||||
specific_grant::Grant as ProtoSpecificGrantType,
|
|
||||||
wallet_create_response::Result as WalletCreateResult,
|
wallet_create_response::Result as WalletCreateResult,
|
||||||
wallet_list_response::Result as WalletListResult,
|
wallet_list_response::Result as WalletListResult,
|
||||||
},
|
},
|
||||||
user_agent::{
|
user_agent::{
|
||||||
AuthChallenge as ProtoAuthChallenge, AuthChallengeRequest as ProtoAuthChallengeRequest,
|
|
||||||
AuthChallengeSolution as ProtoAuthChallengeSolution, AuthOk as ProtoAuthOk,
|
|
||||||
BootstrapEncryptedKey as ProtoBootstrapEncryptedKey,
|
BootstrapEncryptedKey as ProtoBootstrapEncryptedKey,
|
||||||
BootstrapResult as ProtoBootstrapResult, ClientConnectionCancel,
|
BootstrapResult as ProtoBootstrapResult, ListWalletAccessResponse,
|
||||||
ClientConnectionRequest, ClientConnectionResponse, KeyType as ProtoKeyType,
|
SdkClientConnectionCancel as ProtoSdkClientConnectionCancel,
|
||||||
UnsealEncryptedKey as ProtoUnsealEncryptedKey, UnsealResult as ProtoUnsealResult,
|
SdkClientConnectionRequest as ProtoSdkClientConnectionRequest,
|
||||||
UnsealStart, UnsealStartResponse, UserAgentRequest, UserAgentResponse,
|
SdkClientEntry as ProtoSdkClientEntry, SdkClientError as ProtoSdkClientError,
|
||||||
VaultState as ProtoVaultState, user_agent_request::Payload as UserAgentRequestPayload,
|
SdkClientGrantWalletAccess, SdkClientList as ProtoSdkClientList,
|
||||||
|
SdkClientListResponse as ProtoSdkClientListResponse, SdkClientRevokeWalletAccess,
|
||||||
|
SdkClientWalletAccess, UnsealEncryptedKey as ProtoUnsealEncryptedKey,
|
||||||
|
UnsealResult as ProtoUnsealResult, UnsealStart, UserAgentRequest, UserAgentResponse,
|
||||||
|
VaultState as ProtoVaultState,
|
||||||
|
sdk_client_list_response::Result as ProtoSdkClientListResult,
|
||||||
|
user_agent_request::Payload as UserAgentRequestPayload,
|
||||||
user_agent_response::Payload as UserAgentResponsePayload,
|
user_agent_response::Payload as UserAgentResponsePayload,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
transport::{Bi, Error as TransportError},
|
transport::{Error as TransportError, Receiver, Sender, grpc::GrpcBi},
|
||||||
};
|
};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use futures::StreamExt as _;
|
use kameo::{
|
||||||
use prost_types::Timestamp;
|
actor::{ActorRef, Spawn as _},
|
||||||
use tokio::sync::mpsc;
|
error::SendError,
|
||||||
use tonic::{Status, Streaming};
|
};
|
||||||
|
use tonic::Status;
|
||||||
|
use tracing::{error, info, warn};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
actors::user_agent::{
|
actors::{
|
||||||
self, AuthPublicKey, BootstrapError, Request as DomainRequest, Response as DomainResponse,
|
keyholder::KeyHolderState,
|
||||||
TransportResponseError, UnsealError, VaultState,
|
user_agent::{
|
||||||
},
|
OutOfBand, UserAgentConnection, UserAgentSession,
|
||||||
evm::{
|
session::connection::{
|
||||||
policies::{Grant, SpecificGrant},
|
BootstrapError, HandleBootstrapEncryptedKey, HandleEvmWalletCreate,
|
||||||
policies::{
|
HandleEvmWalletList, HandleGrantCreate, HandleGrantDelete,
|
||||||
SharedGrantSettings, TransactionRateLimit, VolumeRateLimit, ether_transfer,
|
HandleGrantEvmWalletAccess, HandleGrantList, HandleListWalletAccess,
|
||||||
token_transfers,
|
HandleNewClientApprove, HandleQueryVaultState, HandleRevokeEvmWalletAccess,
|
||||||
|
HandleSdkClientList, HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
db::models::{CoreEvmWalletAccess, NewEvmWalletAccess},
|
||||||
|
grpc::{Convert, TryConvert, request_tracker::RequestTracker},
|
||||||
};
|
};
|
||||||
use alloy::primitives::{Address, U256};
|
mod auth;
|
||||||
use chrono::{DateTime, TimeZone, Utc};
|
mod inbound;
|
||||||
|
mod outbound;
|
||||||
|
|
||||||
pub struct GrpcTransport {
|
pub struct OutOfBandAdapter(mpsc::Sender<OutOfBand>);
|
||||||
sender: mpsc::Sender<Result<UserAgentResponse, Status>>,
|
|
||||||
receiver: Streaming<UserAgentRequest>,
|
#[async_trait]
|
||||||
|
impl Sender<OutOfBand> for OutOfBandAdapter {
|
||||||
|
async fn send(&mut self, item: OutOfBand) -> Result<(), TransportError> {
|
||||||
|
self.0.send(item).await.map_err(|e| {
|
||||||
|
warn!(error = ?e, "Failed to send out-of-band message");
|
||||||
|
TransportError::ChannelClosed
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GrpcTransport {
|
async fn dispatch_loop(
|
||||||
pub fn new(
|
mut bi: GrpcBi<UserAgentRequest, UserAgentResponse>,
|
||||||
sender: mpsc::Sender<Result<UserAgentResponse, Status>>,
|
actor: ActorRef<UserAgentSession>,
|
||||||
receiver: Streaming<UserAgentRequest>,
|
mut receiver: mpsc::Receiver<OutOfBand>,
|
||||||
) -> Self {
|
mut request_tracker: RequestTracker,
|
||||||
Self { sender, receiver }
|
) {
|
||||||
}
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
oob = receiver.recv() => {
|
||||||
|
let Some(oob) = oob else {
|
||||||
|
warn!("Out-of-band message channel closed");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
fn request_to_domain(request: UserAgentRequest) -> Result<DomainRequest, Status> {
|
let payload = match oob {
|
||||||
match request.payload {
|
OutOfBand::ClientConnectionRequest { profile } => {
|
||||||
Some(UserAgentRequestPayload::AuthChallengeRequest(ProtoAuthChallengeRequest {
|
UserAgentResponsePayload::SdkClientConnectionRequest(ProtoSdkClientConnectionRequest {
|
||||||
pubkey,
|
pubkey: profile.pubkey.to_bytes().to_vec(),
|
||||||
bootstrap_token,
|
info: Some(ProtoClientMetadata {
|
||||||
key_type,
|
name: profile.metadata.name,
|
||||||
})) => Ok(DomainRequest::AuthChallengeRequest {
|
description: profile.metadata.description,
|
||||||
pubkey: parse_auth_pubkey(key_type, pubkey)?,
|
version: profile.metadata.version,
|
||||||
bootstrap_token,
|
|
||||||
}),
|
}),
|
||||||
Some(UserAgentRequestPayload::AuthChallengeSolution(ProtoAuthChallengeSolution {
|
|
||||||
signature,
|
|
||||||
})) => Ok(DomainRequest::AuthChallengeSolution { signature }),
|
|
||||||
Some(UserAgentRequestPayload::UnsealStart(UnsealStart { client_pubkey })) => {
|
|
||||||
let client_pubkey: [u8; 32] = client_pubkey
|
|
||||||
.as_slice()
|
|
||||||
.try_into()
|
|
||||||
.map_err(|_| Status::invalid_argument("client_pubkey must be 32 bytes"))?;
|
|
||||||
Ok(DomainRequest::UnsealStart {
|
|
||||||
client_pubkey: x25519_dalek::PublicKey::from(client_pubkey),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Some(UserAgentRequestPayload::UnsealEncryptedKey(ProtoUnsealEncryptedKey {
|
OutOfBand::ClientConnectionCancel { pubkey } => {
|
||||||
nonce,
|
UserAgentResponsePayload::SdkClientConnectionCancel(ProtoSdkClientConnectionCancel {
|
||||||
ciphertext,
|
|
||||||
associated_data,
|
|
||||||
})) => Ok(DomainRequest::UnsealEncryptedKey {
|
|
||||||
nonce,
|
|
||||||
ciphertext,
|
|
||||||
associated_data,
|
|
||||||
}),
|
|
||||||
Some(UserAgentRequestPayload::BootstrapEncryptedKey(ProtoBootstrapEncryptedKey {
|
|
||||||
nonce,
|
|
||||||
ciphertext,
|
|
||||||
associated_data,
|
|
||||||
})) => Ok(DomainRequest::BootstrapEncryptedKey {
|
|
||||||
nonce,
|
|
||||||
ciphertext,
|
|
||||||
associated_data,
|
|
||||||
}),
|
|
||||||
Some(UserAgentRequestPayload::QueryVaultState(_)) => Ok(DomainRequest::QueryVaultState),
|
|
||||||
Some(UserAgentRequestPayload::EvmWalletCreate(_)) => Ok(DomainRequest::EvmWalletCreate),
|
|
||||||
Some(UserAgentRequestPayload::EvmWalletList(_)) => Ok(DomainRequest::EvmWalletList),
|
|
||||||
Some(UserAgentRequestPayload::ClientConnectionResponse(ClientConnectionResponse {
|
|
||||||
approved,
|
|
||||||
})) => Ok(DomainRequest::ClientConnectionResponse { approved }),
|
|
||||||
|
|
||||||
Some(UserAgentRequestPayload::EvmGrantList(_)) => Ok(DomainRequest::ListGrants),
|
|
||||||
Some(UserAgentRequestPayload::EvmGrantCreate(EvmGrantCreateRequest {
|
|
||||||
client_id,
|
|
||||||
shared,
|
|
||||||
specific,
|
|
||||||
})) => {
|
|
||||||
let shared = parse_shared_settings(client_id, shared)?;
|
|
||||||
let specific = parse_specific_grant(specific)?;
|
|
||||||
Ok(DomainRequest::EvmGrantCreate {
|
|
||||||
client_id,
|
|
||||||
shared,
|
|
||||||
specific,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
Some(UserAgentRequestPayload::EvmGrantDelete(EvmGrantDeleteRequest { grant_id })) => {
|
|
||||||
Ok(DomainRequest::EvmGrantDelete { grant_id })
|
|
||||||
}
|
|
||||||
None => Err(Status::invalid_argument(
|
|
||||||
"Missing user-agent request payload",
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn response_to_proto(response: DomainResponse) -> UserAgentResponse {
|
|
||||||
let payload = match response {
|
|
||||||
DomainResponse::AuthChallenge { nonce } => {
|
|
||||||
UserAgentResponsePayload::AuthChallenge(ProtoAuthChallenge {
|
|
||||||
pubkey: Vec::new(),
|
|
||||||
nonce,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
DomainResponse::AuthOk => UserAgentResponsePayload::AuthOk(ProtoAuthOk {}),
|
|
||||||
DomainResponse::UnsealStartResponse { server_pubkey } => {
|
|
||||||
UserAgentResponsePayload::UnsealStartResponse(UnsealStartResponse {
|
|
||||||
server_pubkey: server_pubkey.as_bytes().to_vec(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
DomainResponse::UnsealResult(result) => UserAgentResponsePayload::UnsealResult(
|
|
||||||
match result {
|
|
||||||
Ok(()) => ProtoUnsealResult::Success,
|
|
||||||
Err(UnsealError::InvalidKey) => ProtoUnsealResult::InvalidKey,
|
|
||||||
Err(UnsealError::Unbootstrapped) => ProtoUnsealResult::Unbootstrapped,
|
|
||||||
}
|
|
||||||
.into(),
|
|
||||||
),
|
|
||||||
DomainResponse::BootstrapResult(result) => UserAgentResponsePayload::BootstrapResult(
|
|
||||||
match result {
|
|
||||||
Ok(()) => ProtoBootstrapResult::Success,
|
|
||||||
Err(BootstrapError::AlreadyBootstrapped) => {
|
|
||||||
ProtoBootstrapResult::AlreadyBootstrapped
|
|
||||||
}
|
|
||||||
Err(BootstrapError::InvalidKey) => ProtoBootstrapResult::InvalidKey,
|
|
||||||
}
|
|
||||||
.into(),
|
|
||||||
),
|
|
||||||
DomainResponse::VaultState(state) => UserAgentResponsePayload::VaultState(
|
|
||||||
match state {
|
|
||||||
VaultState::Unbootstrapped => ProtoVaultState::Unbootstrapped,
|
|
||||||
VaultState::Sealed => ProtoVaultState::Sealed,
|
|
||||||
VaultState::Unsealed => ProtoVaultState::Unsealed,
|
|
||||||
}
|
|
||||||
.into(),
|
|
||||||
),
|
|
||||||
DomainResponse::ClientConnectionRequest { pubkey } => {
|
|
||||||
UserAgentResponsePayload::ClientConnectionRequest(ClientConnectionRequest {
|
|
||||||
pubkey: pubkey.to_bytes().to_vec(),
|
pubkey: pubkey.to_bytes().to_vec(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
DomainResponse::ClientConnectionCancel => {
|
};
|
||||||
UserAgentResponsePayload::ClientConnectionCancel(ClientConnectionCancel {})
|
|
||||||
|
if bi.send(Ok(UserAgentResponse { id: None, payload: Some(payload) })).await.is_err() {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
DomainResponse::EvmWalletCreate(result) => {
|
}
|
||||||
|
|
||||||
|
message = bi.recv() => {
|
||||||
|
let Some(message) = message else { return; };
|
||||||
|
|
||||||
|
let conn = match message {
|
||||||
|
Ok(conn) => conn,
|
||||||
|
Err(err) => {
|
||||||
|
warn!(error = ?err, "Failed to receive user agent request");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let request_id = match request_tracker.request(conn.id) {
|
||||||
|
Ok(id) => id,
|
||||||
|
Err(err) => {
|
||||||
|
let _ = bi.send(Err(err)).await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(payload) = conn.payload else {
|
||||||
|
let _ = bi.send(Err(Status::invalid_argument("Missing user-agent request payload"))).await;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
match dispatch_inner(&actor, payload).await {
|
||||||
|
Ok(Some(response)) => {
|
||||||
|
if bi.send(Ok(UserAgentResponse {
|
||||||
|
id: Some(request_id),
|
||||||
|
payload: Some(response),
|
||||||
|
})).await.is_err() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None) => {}
|
||||||
|
Err(status) => {
|
||||||
|
error!(?status, "Failed to process user agent request");
|
||||||
|
let _ = bi.send(Err(status)).await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn dispatch_inner(
|
||||||
|
actor: &ActorRef<UserAgentSession>,
|
||||||
|
payload: UserAgentRequestPayload,
|
||||||
|
) -> Result<Option<UserAgentResponsePayload>, Status> {
|
||||||
|
let response = match payload {
|
||||||
|
UserAgentRequestPayload::UnsealStart(UnsealStart { client_pubkey }) => {
|
||||||
|
let client_pubkey = <[u8; 32]>::try_from(client_pubkey)
|
||||||
|
.map(x25519_dalek::PublicKey::from)
|
||||||
|
.map_err(|_| Status::invalid_argument("Invalid X25519 public key"))?;
|
||||||
|
|
||||||
|
let response = actor
|
||||||
|
.ask(HandleUnsealRequest { client_pubkey })
|
||||||
|
.await
|
||||||
|
.map_err(|err| {
|
||||||
|
warn!(error = ?err, "Failed to handle unseal start request");
|
||||||
|
Status::internal("Failed to start unseal flow")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
UserAgentResponsePayload::UnsealStartResponse(
|
||||||
|
arbiter_proto::proto::user_agent::UnsealStartResponse {
|
||||||
|
server_pubkey: response.server_pubkey.as_bytes().to_vec(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
UserAgentRequestPayload::UnsealEncryptedKey(ProtoUnsealEncryptedKey {
|
||||||
|
nonce,
|
||||||
|
ciphertext,
|
||||||
|
associated_data,
|
||||||
|
}) => {
|
||||||
|
let result = match actor
|
||||||
|
.ask(HandleUnsealEncryptedKey {
|
||||||
|
nonce,
|
||||||
|
ciphertext,
|
||||||
|
associated_data,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(()) => ProtoUnsealResult::Success,
|
||||||
|
Err(SendError::HandlerError(UnsealError::InvalidKey)) => {
|
||||||
|
ProtoUnsealResult::InvalidKey
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!(error = ?err, "Failed to handle unseal request");
|
||||||
|
return Err(Status::internal("Failed to unseal vault"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
UserAgentResponsePayload::UnsealResult(result.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
UserAgentRequestPayload::BootstrapEncryptedKey(ProtoBootstrapEncryptedKey {
|
||||||
|
nonce,
|
||||||
|
ciphertext,
|
||||||
|
associated_data,
|
||||||
|
}) => {
|
||||||
|
let result = match actor
|
||||||
|
.ask(HandleBootstrapEncryptedKey {
|
||||||
|
nonce,
|
||||||
|
ciphertext,
|
||||||
|
associated_data,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(()) => ProtoBootstrapResult::Success,
|
||||||
|
Err(SendError::HandlerError(BootstrapError::InvalidKey)) => {
|
||||||
|
ProtoBootstrapResult::InvalidKey
|
||||||
|
}
|
||||||
|
Err(SendError::HandlerError(BootstrapError::AlreadyBootstrapped)) => {
|
||||||
|
ProtoBootstrapResult::AlreadyBootstrapped
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!(error = ?err, "Failed to handle bootstrap request");
|
||||||
|
return Err(Status::internal("Failed to bootstrap vault"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
UserAgentResponsePayload::BootstrapResult(result.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
UserAgentRequestPayload::QueryVaultState(_) => {
|
||||||
|
let state = match actor.ask(HandleQueryVaultState {}).await {
|
||||||
|
Ok(KeyHolderState::Unbootstrapped) => ProtoVaultState::Unbootstrapped,
|
||||||
|
Ok(KeyHolderState::Sealed) => ProtoVaultState::Sealed,
|
||||||
|
Ok(KeyHolderState::Unsealed) => ProtoVaultState::Unsealed,
|
||||||
|
Err(err) => {
|
||||||
|
warn!(error = ?err, "Failed to query vault state");
|
||||||
|
ProtoVaultState::Error
|
||||||
|
}
|
||||||
|
};
|
||||||
|
UserAgentResponsePayload::VaultState(state.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
UserAgentRequestPayload::EvmWalletCreate(_) => {
|
||||||
|
let result = match actor.ask(HandleEvmWalletCreate {}).await {
|
||||||
|
Ok((wallet_id, address)) => WalletCreateResult::Wallet(WalletEntry {
|
||||||
|
id: wallet_id,
|
||||||
|
address: address.to_vec(),
|
||||||
|
}),
|
||||||
|
Err(err) => {
|
||||||
|
warn!(error = ?err, "Failed to create EVM wallet");
|
||||||
|
WalletCreateResult::Error(ProtoEvmError::Internal.into())
|
||||||
|
}
|
||||||
|
};
|
||||||
UserAgentResponsePayload::EvmWalletCreate(WalletCreateResponse {
|
UserAgentResponsePayload::EvmWalletCreate(WalletCreateResponse {
|
||||||
result: Some(match result {
|
result: Some(result),
|
||||||
Ok(()) => WalletCreateResult::Wallet(WalletEntry {
|
|
||||||
address: Vec::new(),
|
|
||||||
}),
|
|
||||||
Err(_) => WalletCreateResult::Error(ProtoEvmError::Internal.into()),
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
DomainResponse::EvmWalletList(wallets) => {
|
|
||||||
UserAgentResponsePayload::EvmWalletList(WalletListResponse {
|
UserAgentRequestPayload::EvmWalletList(_) => {
|
||||||
result: Some(WalletListResult::Wallets(WalletList {
|
let result = match actor.ask(HandleEvmWalletList {}).await {
|
||||||
|
Ok(wallets) => WalletListResult::Wallets(WalletList {
|
||||||
wallets: wallets
|
wallets: wallets
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|addr| WalletEntry {
|
.map(|(id, address)| WalletEntry {
|
||||||
address: addr.as_slice().to_vec(),
|
address: address.to_vec(),
|
||||||
|
id,
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
})),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
DomainResponse::ListGrants(grants) => {
|
|
||||||
UserAgentResponsePayload::EvmGrantList(EvmGrantListResponse {
|
|
||||||
result: Some(EvmGrantListResult::Grants(EvmGrantList {
|
|
||||||
grants: grants.into_iter().map(grant_to_proto).collect(),
|
|
||||||
})),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
DomainResponse::EvmGrantCreate(result) => {
|
|
||||||
UserAgentResponsePayload::EvmGrantCreate(EvmGrantCreateResponse {
|
|
||||||
result: Some(match result {
|
|
||||||
Ok(grant_id) => EvmGrantCreateResult::GrantId(grant_id),
|
|
||||||
Err(_) => EvmGrantCreateResult::Error(ProtoEvmError::Internal.into()),
|
|
||||||
}),
|
}),
|
||||||
})
|
Err(err) => {
|
||||||
}
|
warn!(error = ?err, "Failed to list EVM wallets");
|
||||||
DomainResponse::EvmGrantDelete(result) => {
|
WalletListResult::Error(ProtoEvmError::Internal.into())
|
||||||
UserAgentResponsePayload::EvmGrantDelete(EvmGrantDeleteResponse {
|
|
||||||
result: Some(match result {
|
|
||||||
Ok(()) => EvmGrantDeleteResult::Ok(()),
|
|
||||||
Err(_) => EvmGrantDeleteResult::Error(ProtoEvmError::Internal.into()),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
UserAgentResponsePayload::EvmWalletList(WalletListResponse {
|
||||||
UserAgentResponse {
|
result: Some(result),
|
||||||
payload: Some(payload),
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn error_to_status(value: TransportResponseError) -> Status {
|
UserAgentRequestPayload::EvmGrantList(_) => {
|
||||||
match value {
|
let result = match actor.ask(HandleGrantList {}).await {
|
||||||
TransportResponseError::UnexpectedRequestPayload => {
|
Ok(grants) => EvmGrantListResult::Grants(EvmGrantList {
|
||||||
Status::invalid_argument("Expected message with payload")
|
grants: grants
|
||||||
}
|
.into_iter()
|
||||||
TransportResponseError::InvalidStateForUnsealEncryptedKey => {
|
.map(|grant| GrantEntry {
|
||||||
Status::failed_precondition("Invalid state for unseal encrypted key")
|
|
||||||
}
|
|
||||||
TransportResponseError::InvalidClientPubkeyLength => {
|
|
||||||
Status::invalid_argument("client_pubkey must be 32 bytes")
|
|
||||||
}
|
|
||||||
TransportResponseError::StateTransitionFailed => {
|
|
||||||
Status::internal("State machine error")
|
|
||||||
}
|
|
||||||
TransportResponseError::KeyHolderActorUnreachable => {
|
|
||||||
Status::internal("Vault is not available")
|
|
||||||
}
|
|
||||||
TransportResponseError::Auth(ref err) => auth_error_status(err),
|
|
||||||
TransportResponseError::ConnectionRegistrationFailed => {
|
|
||||||
Status::internal("Failed registering connection")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Bi<DomainRequest, Result<DomainResponse, TransportResponseError>> for GrpcTransport {
|
|
||||||
async fn send(
|
|
||||||
&mut self,
|
|
||||||
item: Result<DomainResponse, TransportResponseError>,
|
|
||||||
) -> Result<(), TransportError> {
|
|
||||||
let outbound = match item {
|
|
||||||
Ok(message) => Ok(Self::response_to_proto(message)),
|
|
||||||
Err(err) => Err(Self::error_to_status(err)),
|
|
||||||
};
|
|
||||||
|
|
||||||
self.sender
|
|
||||||
.send(outbound)
|
|
||||||
.await
|
|
||||||
.map_err(|_| TransportError::ChannelClosed)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn recv(&mut self) -> Option<DomainRequest> {
|
|
||||||
match self.receiver.next().await {
|
|
||||||
Some(Ok(item)) => match Self::request_to_domain(item) {
|
|
||||||
Ok(request) => Some(request),
|
|
||||||
Err(status) => {
|
|
||||||
let _ = self.sender.send(Err(status)).await;
|
|
||||||
None
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Some(Err(error)) => {
|
|
||||||
tracing::error!(error = ?error, "grpc user-agent recv failed; closing stream");
|
|
||||||
None
|
|
||||||
}
|
|
||||||
None => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn grant_to_proto(grant: Grant<SpecificGrant>) -> proto::evm::GrantEntry {
|
|
||||||
GrantEntry {
|
|
||||||
id: grant.id,
|
id: grant.id,
|
||||||
specific: Some(match grant.settings {
|
wallet_access_id: grant.shared.wallet_access_id,
|
||||||
SpecificGrant::EtherTransfer(settings) => ProtoSpecificGrant {
|
shared: Some(grant.shared.convert()),
|
||||||
grant: Some(ProtoSpecificGrantType::EtherTransfer(
|
specific: Some(grant.settings.convert()),
|
||||||
ProtoEtherTransferSettings {
|
|
||||||
targets: settings
|
|
||||||
.target
|
|
||||||
.into_iter()
|
|
||||||
.map(|addr| addr.as_slice().to_vec())
|
|
||||||
.collect(),
|
|
||||||
limit: Some(proto::evm::VolumeRateLimit {
|
|
||||||
max_volume: settings.limit.max_volume.to_be_bytes_vec(),
|
|
||||||
window_secs: settings.limit.window.num_seconds(),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
)),
|
|
||||||
},
|
|
||||||
SpecificGrant::TokenTransfer(settings) => ProtoSpecificGrant {
|
|
||||||
grant: Some(ProtoSpecificGrantType::TokenTransfer(
|
|
||||||
ProtoTokenTransferSettings {
|
|
||||||
token_contract: settings.token_contract.as_slice().to_vec(),
|
|
||||||
target: settings.target.map(|addr| addr.as_slice().to_vec()),
|
|
||||||
volume_limits: settings
|
|
||||||
.volume_limits
|
|
||||||
.into_iter()
|
|
||||||
.map(|vrl| proto::evm::VolumeRateLimit {
|
|
||||||
max_volume: vrl.max_volume.to_be_bytes_vec(),
|
|
||||||
window_secs: vrl.window.num_seconds(),
|
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
},
|
|
||||||
)),
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
client_id: grant.shared.client_id,
|
Err(err) => {
|
||||||
shared: Some(proto::evm::SharedSettings {
|
warn!(error = ?err, "Failed to list EVM grants");
|
||||||
wallet_id: grant.shared.wallet_id,
|
EvmGrantListResult::Error(ProtoEvmError::Internal.into())
|
||||||
chain_id: grant.shared.chain,
|
|
||||||
valid_from: grant.shared.valid_from.map(|dt| Timestamp {
|
|
||||||
seconds: dt.timestamp(),
|
|
||||||
nanos: 0,
|
|
||||||
}),
|
|
||||||
valid_until: grant.shared.valid_until.map(|dt| Timestamp {
|
|
||||||
seconds: dt.timestamp(),
|
|
||||||
nanos: 0,
|
|
||||||
}),
|
|
||||||
max_gas_fee_per_gas: grant
|
|
||||||
.shared
|
|
||||||
.max_gas_fee_per_gas
|
|
||||||
.map(|fee| fee.to_be_bytes_vec()),
|
|
||||||
max_priority_fee_per_gas: grant
|
|
||||||
.shared
|
|
||||||
.max_priority_fee_per_gas
|
|
||||||
.map(|fee| fee.to_be_bytes_vec()),
|
|
||||||
rate_limit: grant
|
|
||||||
.shared
|
|
||||||
.rate_limit
|
|
||||||
.map(|limit| proto::evm::TransactionRateLimit {
|
|
||||||
count: limit.count,
|
|
||||||
window_secs: limit.window.num_seconds(),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_volume_rate_limit(vrl: ProtoVolumeRateLimit) -> Result<VolumeRateLimit, Status> {
|
|
||||||
Ok(VolumeRateLimit {
|
|
||||||
max_volume: U256::from_be_slice(&vrl.max_volume),
|
|
||||||
window: chrono::Duration::seconds(vrl.window_secs),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_shared_settings(
|
|
||||||
client_id: i32,
|
|
||||||
proto: Option<ProtoSharedSettings>,
|
|
||||||
) -> Result<SharedGrantSettings, Status> {
|
|
||||||
let s = proto.ok_or_else(|| Status::invalid_argument("missing shared settings"))?;
|
|
||||||
let parse_u256 = |b: Vec<u8>| -> Result<U256, Status> {
|
|
||||||
if b.is_empty() {
|
|
||||||
Err(Status::invalid_argument("U256 bytes must not be empty"))
|
|
||||||
} else {
|
|
||||||
Ok(U256::from_be_slice(&b))
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let parse_ts = |ts: prost_types::Timestamp| -> Result<DateTime<Utc>, Status> {
|
UserAgentResponsePayload::EvmGrantList(EvmGrantListResponse {
|
||||||
Utc.timestamp_opt(ts.seconds, ts.nanos as u32)
|
result: Some(result),
|
||||||
.single()
|
})
|
||||||
.ok_or_else(|| Status::invalid_argument("invalid timestamp"))
|
}
|
||||||
|
|
||||||
|
UserAgentRequestPayload::EvmGrantCreate(EvmGrantCreateRequest { shared, specific }) => {
|
||||||
|
let basic = shared
|
||||||
|
.ok_or_else(|| Status::invalid_argument("Missing shared grant settings"))?
|
||||||
|
.try_convert()?;
|
||||||
|
let grant = specific
|
||||||
|
.ok_or_else(|| Status::invalid_argument("Missing specific grant settings"))?
|
||||||
|
.try_convert()?;
|
||||||
|
|
||||||
|
let result = match actor.ask(HandleGrantCreate { basic, grant }).await {
|
||||||
|
Ok(grant_id) => EvmGrantCreateResult::GrantId(grant_id),
|
||||||
|
Err(err) => {
|
||||||
|
warn!(error = ?err, "Failed to create EVM grant");
|
||||||
|
EvmGrantCreateResult::Error(ProtoEvmError::Internal.into())
|
||||||
|
}
|
||||||
};
|
};
|
||||||
Ok(SharedGrantSettings {
|
UserAgentResponsePayload::EvmGrantCreate(EvmGrantCreateResponse {
|
||||||
wallet_id: s.wallet_id,
|
result: Some(result),
|
||||||
client_id,
|
})
|
||||||
chain: s.chain_id,
|
}
|
||||||
valid_from: s.valid_from.map(parse_ts).transpose()?,
|
|
||||||
valid_until: s.valid_until.map(parse_ts).transpose()?,
|
UserAgentRequestPayload::EvmGrantDelete(EvmGrantDeleteRequest { grant_id }) => {
|
||||||
max_gas_fee_per_gas: s.max_gas_fee_per_gas.map(parse_u256).transpose()?,
|
let result = match actor.ask(HandleGrantDelete { grant_id }).await {
|
||||||
max_priority_fee_per_gas: s.max_priority_fee_per_gas.map(parse_u256).transpose()?,
|
Ok(()) => EvmGrantDeleteResult::Ok(()),
|
||||||
rate_limit: s.rate_limit.map(|rl| TransactionRateLimit {
|
Err(err) => {
|
||||||
count: rl.count,
|
warn!(error = ?err, "Failed to delete EVM grant");
|
||||||
window: chrono::Duration::seconds(rl.window_secs),
|
EvmGrantDeleteResult::Error(ProtoEvmError::Internal.into())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
UserAgentResponsePayload::EvmGrantDelete(EvmGrantDeleteResponse {
|
||||||
|
result: Some(result),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
UserAgentRequestPayload::SdkClientConnectionResponse(resp) => {
|
||||||
|
let pubkey_bytes = <[u8; 32]>::try_from(resp.pubkey)
|
||||||
|
.map_err(|_| Status::invalid_argument("Invalid Ed25519 public key length"))?;
|
||||||
|
let pubkey = ed25519_dalek::VerifyingKey::from_bytes(&pubkey_bytes)
|
||||||
|
.map_err(|_| Status::invalid_argument("Invalid Ed25519 public key"))?;
|
||||||
|
|
||||||
|
actor
|
||||||
|
.ask(HandleNewClientApprove {
|
||||||
|
approved: resp.approved,
|
||||||
|
pubkey,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|err| {
|
||||||
|
warn!(?err, "Failed to process client connection response");
|
||||||
|
Status::internal("Failed to process response")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
UserAgentRequestPayload::SdkClientRevoke(_) => todo!(),
|
||||||
|
|
||||||
|
UserAgentRequestPayload::SdkClientList(_) => {
|
||||||
|
let result = match actor.ask(HandleSdkClientList {}).await {
|
||||||
|
Ok(clients) => ProtoSdkClientListResult::Clients(ProtoSdkClientList {
|
||||||
|
clients: clients
|
||||||
|
.into_iter()
|
||||||
|
.map(|(client, metadata)| ProtoSdkClientEntry {
|
||||||
|
id: client.id,
|
||||||
|
pubkey: client.public_key,
|
||||||
|
info: Some(ProtoClientMetadata {
|
||||||
|
name: metadata.name,
|
||||||
|
description: metadata.description,
|
||||||
|
version: metadata.version,
|
||||||
}),
|
}),
|
||||||
|
created_at: client.created_at.0.timestamp() as i32,
|
||||||
})
|
})
|
||||||
}
|
.collect(),
|
||||||
|
}),
|
||||||
fn parse_specific_grant(proto: Option<proto::evm::SpecificGrant>) -> Result<SpecificGrant, Status> {
|
Err(err) => {
|
||||||
use proto::evm::specific_grant::Grant as ProtoGrant;
|
warn!(error = ?err, "Failed to list SDK clients");
|
||||||
let g = proto
|
ProtoSdkClientListResult::Error(ProtoSdkClientError::Internal.into())
|
||||||
.and_then(|sg| sg.grant)
|
|
||||||
.ok_or_else(|| Status::invalid_argument("missing specific grant"))?;
|
|
||||||
match g {
|
|
||||||
ProtoGrant::EtherTransfer(s) => {
|
|
||||||
let limit = parse_volume_rate_limit(
|
|
||||||
s.limit
|
|
||||||
.ok_or_else(|| Status::invalid_argument("missing ether transfer limit"))?,
|
|
||||||
)?;
|
|
||||||
let target = s
|
|
||||||
.targets
|
|
||||||
.into_iter()
|
|
||||||
.map(|b| {
|
|
||||||
if b.len() == 20 {
|
|
||||||
Ok(Address::from_slice(&b))
|
|
||||||
} else {
|
|
||||||
Err(Status::invalid_argument(
|
|
||||||
"ether transfer target must be 20 bytes",
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
UserAgentResponsePayload::SdkClientListResponse(ProtoSdkClientListResponse {
|
||||||
|
result: Some(result),
|
||||||
})
|
})
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
|
||||||
Ok(SpecificGrant::EtherTransfer(ether_transfer::Settings {
|
|
||||||
target,
|
|
||||||
limit,
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
ProtoGrant::TokenTransfer(s) => {
|
|
||||||
if s.token_contract.len() != 20 {
|
UserAgentRequestPayload::GrantWalletAccess(SdkClientGrantWalletAccess { accesses }) => {
|
||||||
return Err(Status::invalid_argument("token_contract must be 20 bytes"));
|
let entries: Vec<NewEvmWalletAccess> =
|
||||||
|
accesses.into_iter().map(|a| a.convert()).collect();
|
||||||
|
|
||||||
|
match actor.ask(HandleGrantEvmWalletAccess { entries }).await {
|
||||||
|
Ok(()) => {
|
||||||
|
info!("Successfully granted wallet access");
|
||||||
|
return Ok(None);
|
||||||
}
|
}
|
||||||
let target = s
|
Err(err) => {
|
||||||
.target
|
warn!(error = ?err, "Failed to grant wallet access");
|
||||||
.map(|b| {
|
return Err(Status::internal("Failed to grant wallet access"));
|
||||||
if b.len() == 20 {
|
|
||||||
Ok(Address::from_slice(&b))
|
|
||||||
} else {
|
|
||||||
Err(Status::invalid_argument(
|
|
||||||
"token transfer target must be 20 bytes",
|
|
||||||
))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.transpose()?;
|
|
||||||
let volume_limits = s
|
|
||||||
.volume_limits
|
|
||||||
.into_iter()
|
|
||||||
.map(parse_volume_rate_limit)
|
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
|
||||||
Ok(SpecificGrant::TokenTransfer(token_transfers::Settings {
|
|
||||||
token_contract: Address::from_slice(&s.token_contract),
|
|
||||||
target,
|
|
||||||
volume_limits,
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UserAgentRequestPayload::RevokeWalletAccess(SdkClientRevokeWalletAccess { accesses }) => {
|
||||||
|
match actor.ask(HandleRevokeEvmWalletAccess { entries: accesses }).await {
|
||||||
|
Ok(()) => {
|
||||||
|
info!("Successfully revoked wallet access");
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!(error = ?err, "Failed to revoke wallet access");
|
||||||
|
return Err(Status::internal("Failed to revoke wallet access"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UserAgentRequestPayload::ListWalletAccess(_) => {
|
||||||
|
let result = match actor.ask(HandleListWalletAccess {}).await {
|
||||||
|
Ok(accesses) => ListWalletAccessResponse {
|
||||||
|
accesses: accesses.into_iter().map(|a| a.convert()).collect(),
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
warn!(error = ?err, "Failed to list wallet access");
|
||||||
|
return Err(Status::internal("Failed to list wallet access"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
UserAgentResponsePayload::ListWalletAccessResponse(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
UserAgentRequestPayload::AuthChallengeRequest(..)
|
||||||
|
| UserAgentRequestPayload::AuthChallengeSolution(..) => {
|
||||||
|
warn!(?payload, "Unsupported post-auth user agent request");
|
||||||
|
return Err(Status::invalid_argument("Unsupported user-agent request"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Some(response))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_auth_pubkey(key_type: i32, pubkey: Vec<u8>) -> Result<AuthPublicKey, Status> {
|
pub async fn start(
|
||||||
match ProtoKeyType::try_from(key_type).unwrap_or(ProtoKeyType::Unspecified) {
|
mut conn: UserAgentConnection,
|
||||||
ProtoKeyType::Unspecified | ProtoKeyType::Ed25519 => {
|
mut bi: GrpcBi<UserAgentRequest, UserAgentResponse>,
|
||||||
let bytes: [u8; 32] = pubkey
|
) {
|
||||||
.as_slice()
|
let mut request_tracker = RequestTracker::default();
|
||||||
.try_into()
|
|
||||||
.map_err(|_| Status::invalid_argument("invalid Ed25519 public key length"))?;
|
|
||||||
let key = ed25519_dalek::VerifyingKey::from_bytes(&bytes)
|
|
||||||
.map_err(|_| Status::invalid_argument("invalid Ed25519 public key encoding"))?;
|
|
||||||
Ok(AuthPublicKey::Ed25519(key))
|
|
||||||
}
|
|
||||||
ProtoKeyType::EcdsaSecp256k1 => {
|
|
||||||
let key = k256::ecdsa::VerifyingKey::from_sec1_bytes(&pubkey)
|
|
||||||
.map_err(|_| Status::invalid_argument("invalid secp256k1 public key encoding"))?;
|
|
||||||
Ok(AuthPublicKey::EcdsaSecp256k1(key))
|
|
||||||
}
|
|
||||||
ProtoKeyType::Rsa => {
|
|
||||||
use rsa::pkcs8::DecodePublicKey as _;
|
|
||||||
|
|
||||||
let key = rsa::RsaPublicKey::from_public_key_der(&pubkey)
|
let pubkey = match auth::start(&mut conn, &mut bi, &mut request_tracker).await {
|
||||||
.map_err(|_| Status::invalid_argument("invalid RSA public key encoding"))?;
|
Ok(pubkey) => pubkey,
|
||||||
Ok(AuthPublicKey::Rsa(key))
|
Err(e) => {
|
||||||
}
|
warn!(error = ?e, "Authentication failed");
|
||||||
}
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
fn auth_error_status(value: &user_agent::auth::Error) -> Status {
|
|
||||||
use user_agent::auth::Error;
|
|
||||||
|
|
||||||
match value {
|
|
||||||
Error::UnexpectedMessagePayload | Error::InvalidClientPubkeyLength => {
|
|
||||||
Status::invalid_argument(value.to_string())
|
|
||||||
}
|
|
||||||
Error::InvalidAuthPubkeyEncoding => {
|
|
||||||
Status::invalid_argument("Failed to convert pubkey to VerifyingKey")
|
|
||||||
}
|
|
||||||
Error::PublicKeyNotRegistered | Error::InvalidChallengeSolution => {
|
|
||||||
Status::unauthenticated(value.to_string())
|
|
||||||
}
|
|
||||||
Error::InvalidBootstrapToken => Status::invalid_argument("Invalid bootstrap token"),
|
|
||||||
Error::Transport => Status::internal("Transport error"),
|
|
||||||
Error::BootstrapperActorUnreachable => {
|
|
||||||
Status::internal("Bootstrap token consumption failed")
|
|
||||||
}
|
|
||||||
Error::DatabasePoolUnavailable => Status::internal("Database pool error"),
|
|
||||||
Error::DatabaseOperationFailed => Status::internal("Database error"),
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let (oob_sender, oob_receiver) = mpsc::channel(16);
|
||||||
|
let oob_adapter = OutOfBandAdapter(oob_sender);
|
||||||
|
|
||||||
|
let actor = UserAgentSession::spawn(UserAgentSession::new(conn, Box::new(oob_adapter)));
|
||||||
|
let actor_for_cleanup = actor.clone();
|
||||||
|
|
||||||
|
info!(?pubkey, "User authenticated successfully");
|
||||||
|
dispatch_loop(bi, actor, oob_receiver, request_tracker).await;
|
||||||
|
actor_for_cleanup.kill();
|
||||||
}
|
}
|
||||||
|
|||||||
178
server/crates/arbiter-server/src/grpc/user_agent/auth.rs
Normal file
178
server/crates/arbiter-server/src/grpc/user_agent/auth.rs
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
use arbiter_proto::{
|
||||||
|
proto::user_agent::{
|
||||||
|
AuthChallenge as ProtoAuthChallenge, AuthChallengeRequest as ProtoAuthChallengeRequest,
|
||||||
|
AuthChallengeSolution as ProtoAuthChallengeSolution, AuthResult as ProtoAuthResult,
|
||||||
|
KeyType as ProtoKeyType, UserAgentRequest, UserAgentResponse,
|
||||||
|
user_agent_request::Payload as UserAgentRequestPayload,
|
||||||
|
user_agent_response::Payload as UserAgentResponsePayload,
|
||||||
|
},
|
||||||
|
transport::{Bi, Error as TransportError, Receiver, Sender, grpc::GrpcBi},
|
||||||
|
};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use tonic::Status;
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
actors::user_agent::{AuthPublicKey, UserAgentConnection, auth},
|
||||||
|
db::models::KeyType,
|
||||||
|
grpc::request_tracker::RequestTracker,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct AuthTransportAdapter<'a> {
|
||||||
|
bi: &'a mut GrpcBi<UserAgentRequest, UserAgentResponse>,
|
||||||
|
request_tracker: &'a mut RequestTracker,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> AuthTransportAdapter<'a> {
|
||||||
|
pub fn new(
|
||||||
|
bi: &'a mut GrpcBi<UserAgentRequest, UserAgentResponse>,
|
||||||
|
request_tracker: &'a mut RequestTracker,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
bi,
|
||||||
|
request_tracker,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_user_agent_response(
|
||||||
|
&mut self,
|
||||||
|
payload: UserAgentResponsePayload,
|
||||||
|
) -> Result<(), TransportError> {
|
||||||
|
self.bi
|
||||||
|
.send(Ok(UserAgentResponse {
|
||||||
|
id: Some(self.request_tracker.current_request_id()),
|
||||||
|
payload: Some(payload),
|
||||||
|
}))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Sender<Result<auth::Outbound, auth::Error>> for AuthTransportAdapter<'_> {
|
||||||
|
async fn send(
|
||||||
|
&mut self,
|
||||||
|
item: Result<auth::Outbound, auth::Error>,
|
||||||
|
) -> Result<(), TransportError> {
|
||||||
|
use auth::{Error, Outbound};
|
||||||
|
let payload = match item {
|
||||||
|
Ok(Outbound::AuthChallenge { nonce }) => {
|
||||||
|
UserAgentResponsePayload::AuthChallenge(ProtoAuthChallenge { nonce })
|
||||||
|
}
|
||||||
|
Ok(Outbound::AuthSuccess) => {
|
||||||
|
UserAgentResponsePayload::AuthResult(ProtoAuthResult::Success.into())
|
||||||
|
}
|
||||||
|
Err(Error::UnregisteredPublicKey) => {
|
||||||
|
UserAgentResponsePayload::AuthResult(ProtoAuthResult::InvalidKey.into())
|
||||||
|
}
|
||||||
|
Err(Error::InvalidChallengeSolution) => {
|
||||||
|
UserAgentResponsePayload::AuthResult(ProtoAuthResult::InvalidSignature.into())
|
||||||
|
}
|
||||||
|
Err(Error::InvalidBootstrapToken) => {
|
||||||
|
UserAgentResponsePayload::AuthResult(ProtoAuthResult::TokenInvalid.into())
|
||||||
|
}
|
||||||
|
Err(Error::Internal { details }) => {
|
||||||
|
return self.bi.send(Err(Status::internal(details))).await;
|
||||||
|
}
|
||||||
|
Err(Error::Transport) => {
|
||||||
|
return self
|
||||||
|
.bi
|
||||||
|
.send(Err(Status::unavailable("transport error")))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.send_user_agent_response(payload).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
|
||||||
|
async fn recv(&mut self) -> Option<auth::Inbound> {
|
||||||
|
let request = match self.bi.recv().await? {
|
||||||
|
Ok(request) => request,
|
||||||
|
Err(error) => {
|
||||||
|
warn!(error = ?error, "Failed to receive user agent auth request");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match self.request_tracker.request(request.id) {
|
||||||
|
Ok(request_id) => request_id,
|
||||||
|
Err(error) => {
|
||||||
|
let _ = self.bi.send(Err(error)).await;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(payload) = request.payload else {
|
||||||
|
warn!(
|
||||||
|
event = "received request with empty payload",
|
||||||
|
"grpc.useragent.auth_adapter"
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
match payload {
|
||||||
|
UserAgentRequestPayload::AuthChallengeRequest(ProtoAuthChallengeRequest {
|
||||||
|
pubkey,
|
||||||
|
bootstrap_token,
|
||||||
|
key_type,
|
||||||
|
}) => {
|
||||||
|
let Ok(key_type) = ProtoKeyType::try_from(key_type) else {
|
||||||
|
warn!(
|
||||||
|
event = "received request with invalid key type",
|
||||||
|
"grpc.useragent.auth_adapter"
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let key_type = match key_type {
|
||||||
|
ProtoKeyType::Ed25519 => KeyType::Ed25519,
|
||||||
|
ProtoKeyType::EcdsaSecp256k1 => KeyType::EcdsaSecp256k1,
|
||||||
|
ProtoKeyType::Rsa => KeyType::Rsa,
|
||||||
|
ProtoKeyType::Unspecified => {
|
||||||
|
warn!(
|
||||||
|
event = "received request with unspecified key type",
|
||||||
|
"grpc.useragent.auth_adapter"
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let Ok(pubkey) = AuthPublicKey::try_from((key_type, pubkey)) else {
|
||||||
|
warn!(
|
||||||
|
event = "received request with invalid public key",
|
||||||
|
"grpc.useragent.auth_adapter"
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(auth::Inbound::AuthChallengeRequest {
|
||||||
|
pubkey,
|
||||||
|
bootstrap_token,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
UserAgentRequestPayload::AuthChallengeSolution(ProtoAuthChallengeSolution {
|
||||||
|
signature,
|
||||||
|
}) => Some(auth::Inbound::AuthChallengeSolution { signature }),
|
||||||
|
_ => {
|
||||||
|
let _ = self
|
||||||
|
.bi
|
||||||
|
.send(Err(Status::invalid_argument(
|
||||||
|
"Unsupported user-agent auth request",
|
||||||
|
)))
|
||||||
|
.await;
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Bi<auth::Inbound, Result<auth::Outbound, auth::Error>> for AuthTransportAdapter<'_> {}
|
||||||
|
|
||||||
|
pub async fn start(
|
||||||
|
conn: &mut UserAgentConnection,
|
||||||
|
bi: &mut GrpcBi<UserAgentRequest, UserAgentResponse>,
|
||||||
|
request_tracker: &mut RequestTracker,
|
||||||
|
) -> Result<AuthPublicKey, auth::Error> {
|
||||||
|
let transport = AuthTransportAdapter::new(bi, request_tracker);
|
||||||
|
auth::authenticate(conn, transport).await
|
||||||
|
}
|
||||||
168
server/crates/arbiter-server/src/grpc/user_agent/inbound.rs
Normal file
168
server/crates/arbiter-server/src/grpc/user_agent/inbound.rs
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
use alloy::primitives::{Address, U256};
|
||||||
|
use arbiter_proto::proto::evm::{
|
||||||
|
EtherTransferSettings as ProtoEtherTransferSettings, SharedSettings as ProtoSharedSettings,
|
||||||
|
SpecificGrant as ProtoSpecificGrant, TokenTransferSettings as ProtoTokenTransferSettings,
|
||||||
|
TransactionRateLimit as ProtoTransactionRateLimit, VolumeRateLimit as ProtoVolumeRateLimit,
|
||||||
|
specific_grant::Grant as ProtoSpecificGrantType,
|
||||||
|
};
|
||||||
|
use arbiter_proto::proto::user_agent::{SdkClientWalletAccess, WalletAccess};
|
||||||
|
use chrono::{DateTime, TimeZone, Utc};
|
||||||
|
use prost_types::Timestamp as ProtoTimestamp;
|
||||||
|
use tonic::Status;
|
||||||
|
|
||||||
|
use crate::db::models::{CoreEvmWalletAccess, NewEvmWallet, NewEvmWalletAccess};
|
||||||
|
use crate::grpc::Convert;
|
||||||
|
use crate::{
|
||||||
|
evm::policies::{
|
||||||
|
SharedGrantSettings, SpecificGrant, TransactionRateLimit, VolumeRateLimit, ether_transfer,
|
||||||
|
token_transfers,
|
||||||
|
},
|
||||||
|
grpc::TryConvert,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn address_from_bytes(bytes: Vec<u8>) -> Result<Address, Status> {
|
||||||
|
if bytes.len() != 20 {
|
||||||
|
return Err(Status::invalid_argument("Invalid EVM address"));
|
||||||
|
}
|
||||||
|
Ok(Address::from_slice(&bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn u256_from_proto_bytes(bytes: &[u8]) -> Result<U256, Status> {
|
||||||
|
if bytes.len() > 32 {
|
||||||
|
return Err(Status::invalid_argument("Invalid U256 byte length"));
|
||||||
|
}
|
||||||
|
Ok(U256::from_be_slice(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryConvert for ProtoTimestamp {
|
||||||
|
type Output = DateTime<Utc>;
|
||||||
|
type Error = Status;
|
||||||
|
|
||||||
|
fn try_convert(self) -> Result<DateTime<Utc>, Status> {
|
||||||
|
Utc.timestamp_opt(self.seconds, self.nanos as u32)
|
||||||
|
.single()
|
||||||
|
.ok_or_else(|| Status::invalid_argument("Invalid timestamp"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryConvert for ProtoTransactionRateLimit {
|
||||||
|
type Output = TransactionRateLimit;
|
||||||
|
type Error = Status;
|
||||||
|
|
||||||
|
fn try_convert(self) -> Result<TransactionRateLimit, Status> {
|
||||||
|
Ok(TransactionRateLimit {
|
||||||
|
count: self.count,
|
||||||
|
window: chrono::Duration::seconds(self.window_secs),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryConvert for ProtoVolumeRateLimit {
|
||||||
|
type Output = VolumeRateLimit;
|
||||||
|
type Error = Status;
|
||||||
|
|
||||||
|
fn try_convert(self) -> Result<VolumeRateLimit, Status> {
|
||||||
|
Ok(VolumeRateLimit {
|
||||||
|
max_volume: u256_from_proto_bytes(&self.max_volume)?,
|
||||||
|
window: chrono::Duration::seconds(self.window_secs),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryConvert for ProtoSharedSettings {
|
||||||
|
type Output = SharedGrantSettings;
|
||||||
|
type Error = Status;
|
||||||
|
|
||||||
|
fn try_convert(self) -> Result<SharedGrantSettings, Status> {
|
||||||
|
Ok(SharedGrantSettings {
|
||||||
|
wallet_access_id: self.wallet_access_id,
|
||||||
|
chain: self.chain_id,
|
||||||
|
valid_from: self
|
||||||
|
.valid_from
|
||||||
|
.map(ProtoTimestamp::try_convert)
|
||||||
|
.transpose()?,
|
||||||
|
valid_until: self
|
||||||
|
.valid_until
|
||||||
|
.map(ProtoTimestamp::try_convert)
|
||||||
|
.transpose()?,
|
||||||
|
max_gas_fee_per_gas: self
|
||||||
|
.max_gas_fee_per_gas
|
||||||
|
.as_deref()
|
||||||
|
.map(u256_from_proto_bytes)
|
||||||
|
.transpose()?,
|
||||||
|
max_priority_fee_per_gas: self
|
||||||
|
.max_priority_fee_per_gas
|
||||||
|
.as_deref()
|
||||||
|
.map(u256_from_proto_bytes)
|
||||||
|
.transpose()?,
|
||||||
|
rate_limit: self
|
||||||
|
.rate_limit
|
||||||
|
.map(ProtoTransactionRateLimit::try_convert)
|
||||||
|
.transpose()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryConvert for ProtoSpecificGrant {
|
||||||
|
type Output = SpecificGrant;
|
||||||
|
type Error = Status;
|
||||||
|
|
||||||
|
fn try_convert(self) -> Result<SpecificGrant, Status> {
|
||||||
|
match self.grant {
|
||||||
|
Some(ProtoSpecificGrantType::EtherTransfer(ProtoEtherTransferSettings {
|
||||||
|
targets,
|
||||||
|
limit,
|
||||||
|
})) => Ok(SpecificGrant::EtherTransfer(ether_transfer::Settings {
|
||||||
|
target: targets
|
||||||
|
.into_iter()
|
||||||
|
.map(address_from_bytes)
|
||||||
|
.collect::<Result<_, _>>()?,
|
||||||
|
limit: limit
|
||||||
|
.ok_or_else(|| {
|
||||||
|
Status::invalid_argument("Missing ether transfer volume rate limit")
|
||||||
|
})?
|
||||||
|
.try_convert()?,
|
||||||
|
})),
|
||||||
|
Some(ProtoSpecificGrantType::TokenTransfer(ProtoTokenTransferSettings {
|
||||||
|
token_contract,
|
||||||
|
target,
|
||||||
|
volume_limits,
|
||||||
|
})) => Ok(SpecificGrant::TokenTransfer(token_transfers::Settings {
|
||||||
|
token_contract: address_from_bytes(token_contract)?,
|
||||||
|
target: target.map(address_from_bytes).transpose()?,
|
||||||
|
volume_limits: volume_limits
|
||||||
|
.into_iter()
|
||||||
|
.map(ProtoVolumeRateLimit::try_convert)
|
||||||
|
.collect::<Result<_, _>>()?,
|
||||||
|
})),
|
||||||
|
None => Err(Status::invalid_argument("Missing specific grant kind")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Convert for WalletAccess {
|
||||||
|
type Output = NewEvmWalletAccess;
|
||||||
|
|
||||||
|
fn convert(self) -> Self::Output {
|
||||||
|
NewEvmWalletAccess {
|
||||||
|
wallet_id: self.wallet_id,
|
||||||
|
client_id: self.sdk_client_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryConvert for SdkClientWalletAccess {
|
||||||
|
type Output = CoreEvmWalletAccess;
|
||||||
|
type Error = Status;
|
||||||
|
|
||||||
|
fn try_convert(self) -> Result<CoreEvmWalletAccess, Status> {
|
||||||
|
let Some(access) = self.access else {
|
||||||
|
return Err(Status::invalid_argument("Missing wallet access entry"));
|
||||||
|
};
|
||||||
|
Ok(CoreEvmWalletAccess {
|
||||||
|
wallet_id: access.wallet_id,
|
||||||
|
client_id: access.sdk_client_id,
|
||||||
|
id: self.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
111
server/crates/arbiter-server/src/grpc/user_agent/outbound.rs
Normal file
111
server/crates/arbiter-server/src/grpc/user_agent/outbound.rs
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
use arbiter_proto::proto::{
|
||||||
|
evm::{
|
||||||
|
EtherTransferSettings as ProtoEtherTransferSettings, SharedSettings as ProtoSharedSettings,
|
||||||
|
SpecificGrant as ProtoSpecificGrant, TokenTransferSettings as ProtoTokenTransferSettings,
|
||||||
|
TransactionRateLimit as ProtoTransactionRateLimit, VolumeRateLimit as ProtoVolumeRateLimit,
|
||||||
|
specific_grant::Grant as ProtoSpecificGrantType,
|
||||||
|
},
|
||||||
|
user_agent::{SdkClientWalletAccess as ProtoSdkClientWalletAccess, WalletAccess},
|
||||||
|
};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use prost_types::Timestamp as ProtoTimestamp;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
db::models::EvmWalletAccess,
|
||||||
|
evm::policies::{SharedGrantSettings, SpecificGrant, TransactionRateLimit, VolumeRateLimit},
|
||||||
|
grpc::Convert,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Convert for DateTime<Utc> {
|
||||||
|
type Output = ProtoTimestamp;
|
||||||
|
|
||||||
|
fn convert(self) -> ProtoTimestamp {
|
||||||
|
ProtoTimestamp {
|
||||||
|
seconds: self.timestamp(),
|
||||||
|
nanos: self.timestamp_subsec_nanos() as i32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Convert for TransactionRateLimit {
|
||||||
|
type Output = ProtoTransactionRateLimit;
|
||||||
|
|
||||||
|
fn convert(self) -> ProtoTransactionRateLimit {
|
||||||
|
ProtoTransactionRateLimit {
|
||||||
|
count: self.count,
|
||||||
|
window_secs: self.window.num_seconds(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Convert for VolumeRateLimit {
|
||||||
|
type Output = ProtoVolumeRateLimit;
|
||||||
|
|
||||||
|
fn convert(self) -> ProtoVolumeRateLimit {
|
||||||
|
ProtoVolumeRateLimit {
|
||||||
|
max_volume: self.max_volume.to_be_bytes::<32>().to_vec(),
|
||||||
|
window_secs: self.window.num_seconds(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Convert for SharedGrantSettings {
|
||||||
|
type Output = ProtoSharedSettings;
|
||||||
|
|
||||||
|
fn convert(self) -> ProtoSharedSettings {
|
||||||
|
ProtoSharedSettings {
|
||||||
|
wallet_access_id: self.wallet_access_id,
|
||||||
|
chain_id: self.chain,
|
||||||
|
valid_from: self.valid_from.map(DateTime::convert),
|
||||||
|
valid_until: self.valid_until.map(DateTime::convert),
|
||||||
|
max_gas_fee_per_gas: self
|
||||||
|
.max_gas_fee_per_gas
|
||||||
|
.map(|value| value.to_be_bytes::<32>().to_vec()),
|
||||||
|
max_priority_fee_per_gas: self
|
||||||
|
.max_priority_fee_per_gas
|
||||||
|
.map(|value| value.to_be_bytes::<32>().to_vec()),
|
||||||
|
rate_limit: self.rate_limit.map(TransactionRateLimit::convert),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Convert for SpecificGrant {
|
||||||
|
type Output = ProtoSpecificGrant;
|
||||||
|
|
||||||
|
fn convert(self) -> ProtoSpecificGrant {
|
||||||
|
let grant = match self {
|
||||||
|
SpecificGrant::EtherTransfer(s) => {
|
||||||
|
ProtoSpecificGrantType::EtherTransfer(ProtoEtherTransferSettings {
|
||||||
|
targets: s.target.into_iter().map(|a| a.to_vec()).collect(),
|
||||||
|
limit: Some(s.limit.convert()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
SpecificGrant::TokenTransfer(s) => {
|
||||||
|
ProtoSpecificGrantType::TokenTransfer(ProtoTokenTransferSettings {
|
||||||
|
token_contract: s.token_contract.to_vec(),
|
||||||
|
target: s.target.map(|a| a.to_vec()),
|
||||||
|
volume_limits: s
|
||||||
|
.volume_limits
|
||||||
|
.into_iter()
|
||||||
|
.map(VolumeRateLimit::convert)
|
||||||
|
.collect(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ProtoSpecificGrant { grant: Some(grant) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Convert for EvmWalletAccess {
|
||||||
|
type Output = ProtoSdkClientWalletAccess;
|
||||||
|
|
||||||
|
fn convert(self) -> Self::Output {
|
||||||
|
Self::Output {
|
||||||
|
id: self.id,
|
||||||
|
access: Some(WalletAccess {
|
||||||
|
wallet_id: self.wallet_id,
|
||||||
|
sdk_client_id: self.client_id,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![deny(
|
|
||||||
clippy::unwrap_used,
|
|
||||||
clippy::expect_used,
|
|
||||||
clippy::panic
|
|
||||||
)]
|
|
||||||
|
|
||||||
use crate::context::ServerContext;
|
use std::{net::SocketAddr, path::PathBuf};
|
||||||
|
|
||||||
|
use arbiter_proto::{proto::arbiter_service_server::ArbiterServiceServer, url::ArbiterUrl};
|
||||||
|
use miette::miette;
|
||||||
|
use tonic::transport::{Identity, ServerTlsConfig};
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
use crate::{actors::bootstrap::GetToken, context::ServerContext};
|
||||||
|
|
||||||
pub mod actors;
|
pub mod actors;
|
||||||
pub mod context;
|
pub mod context;
|
||||||
@@ -13,8 +15,7 @@ pub mod db;
|
|||||||
pub mod evm;
|
pub mod evm;
|
||||||
pub mod grpc;
|
pub mod grpc;
|
||||||
pub mod safe_cell;
|
pub mod safe_cell;
|
||||||
|
pub mod utils;
|
||||||
const DEFAULT_CHANNEL_SIZE: usize = 1000;
|
|
||||||
|
|
||||||
pub struct Server {
|
pub struct Server {
|
||||||
context: ServerContext,
|
context: ServerContext,
|
||||||
@@ -26,3 +27,63 @@ impl Server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct RunConfig {
|
||||||
|
pub addr: SocketAddr,
|
||||||
|
pub data_dir: Option<PathBuf>,
|
||||||
|
pub log_arbiter_url: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RunConfig {
|
||||||
|
pub fn new(addr: SocketAddr, data_dir: Option<PathBuf>) -> Self {
|
||||||
|
Self {
|
||||||
|
addr,
|
||||||
|
data_dir,
|
||||||
|
log_arbiter_url: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run_server_until_shutdown<F>(config: RunConfig, shutdown: F) -> miette::Result<()>
|
||||||
|
where
|
||||||
|
F: Future<Output = ()> + Send + 'static,
|
||||||
|
{
|
||||||
|
arbiter_proto::set_home_path_override(config.data_dir.clone())
|
||||||
|
.map_err(|err| miette!("failed to set home path override: {err}"))?;
|
||||||
|
|
||||||
|
let db = db::create_pool(None).await?;
|
||||||
|
info!(addr = %config.addr, "Database ready");
|
||||||
|
|
||||||
|
let context = ServerContext::new(db).await?;
|
||||||
|
info!(addr = %config.addr, "Server context ready");
|
||||||
|
|
||||||
|
if config.log_arbiter_url {
|
||||||
|
let url = ArbiterUrl {
|
||||||
|
host: config.addr.ip().to_string(),
|
||||||
|
port: config.addr.port(),
|
||||||
|
ca_cert: context.tls.ca_cert().clone().into_owned(),
|
||||||
|
bootstrap_token: context
|
||||||
|
.actors
|
||||||
|
.bootstrapper
|
||||||
|
.ask(GetToken)
|
||||||
|
.await
|
||||||
|
.map_err(|err| miette!("failed to get bootstrap token from actor: {err}"))?,
|
||||||
|
};
|
||||||
|
info!(%url, "Server URL");
|
||||||
|
}
|
||||||
|
|
||||||
|
let tls = ServerTlsConfig::new().identity(Identity::from_pem(
|
||||||
|
context.tls.cert_pem(),
|
||||||
|
context.tls.key_pem(),
|
||||||
|
));
|
||||||
|
|
||||||
|
tonic::transport::Server::builder()
|
||||||
|
.tls_config(tls)
|
||||||
|
.map_err(|err| miette!("Failed to setup TLS: {err}"))?
|
||||||
|
.add_service(ArbiterServiceServer::new(Server::new(context)))
|
||||||
|
.serve_with_shutdown(config.addr, shutdown)
|
||||||
|
.await
|
||||||
|
.map_err(|e| miette!("gRPC server error: {e}"))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,56 +1,42 @@
|
|||||||
use std::net::SocketAddr;
|
mod cli;
|
||||||
|
mod service;
|
||||||
|
|
||||||
use arbiter_proto::{proto::arbiter_service_server::ArbiterServiceServer, url::ArbiterUrl};
|
use clap::Parser;
|
||||||
use arbiter_server::{Server, actors::bootstrap::GetToken, context::ServerContext, db};
|
use cli::{Cli, Command, RunArgs, ServiceCommand};
|
||||||
use miette::miette;
|
|
||||||
use rustls::crypto::aws_lc_rs;
|
use rustls::crypto::aws_lc_rs;
|
||||||
use tonic::transport::{Identity, ServerTlsConfig};
|
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
const PORT: u16 = 50051;
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> miette::Result<()> {
|
async fn main() -> miette::Result<()> {
|
||||||
aws_lc_rs::default_provider().install_default().unwrap();
|
aws_lc_rs::default_provider().install_default().unwrap();
|
||||||
|
init_logging();
|
||||||
|
|
||||||
tracing_subscriber::fmt()
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
match cli.command {
|
||||||
|
None => run_foreground(RunArgs::default()).await,
|
||||||
|
Some(Command::Run(args)) => run_foreground(args).await,
|
||||||
|
Some(Command::Service { command }) => match command {
|
||||||
|
ServiceCommand::Install(args) => service::install_service(args),
|
||||||
|
ServiceCommand::Run(args) => service::run_service_dispatcher(args),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_foreground(args: RunArgs) -> miette::Result<()> {
|
||||||
|
info!(addr = %args.listen_addr, "Starting arbiter server");
|
||||||
|
arbiter_server::run_server_until_shutdown(
|
||||||
|
arbiter_server::RunConfig::new(args.listen_addr, args.data_dir),
|
||||||
|
std::future::pending::<()>(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_logging() {
|
||||||
|
let _ = tracing_subscriber::fmt()
|
||||||
.with_env_filter(
|
.with_env_filter(
|
||||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||||
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
|
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
|
||||||
)
|
)
|
||||||
.init();
|
.try_init();
|
||||||
|
|
||||||
info!("Starting arbiter server");
|
|
||||||
|
|
||||||
let db = db::create_pool(None).await?;
|
|
||||||
info!("Database ready");
|
|
||||||
|
|
||||||
let context = ServerContext::new(db).await?;
|
|
||||||
|
|
||||||
let addr: SocketAddr = format!("127.0.0.1:{PORT}").parse().expect("valid address");
|
|
||||||
info!(%addr, "Starting gRPC server");
|
|
||||||
|
|
||||||
let url = ArbiterUrl {
|
|
||||||
host: addr.ip().to_string(),
|
|
||||||
port: addr.port(),
|
|
||||||
ca_cert: context.tls.ca_cert().clone().into_owned(),
|
|
||||||
bootstrap_token: context.actors.bootstrapper.ask(GetToken).await.unwrap(),
|
|
||||||
};
|
|
||||||
|
|
||||||
info!(%url, "Server URL");
|
|
||||||
|
|
||||||
let tls = ServerTlsConfig::new().identity(Identity::from_pem(
|
|
||||||
context.tls.cert_pem(),
|
|
||||||
context.tls.key_pem(),
|
|
||||||
));
|
|
||||||
|
|
||||||
tonic::transport::Server::builder()
|
|
||||||
.tls_config(tls)
|
|
||||||
.map_err(|err| miette!("Faild to setup TLS: {err}"))?
|
|
||||||
.add_service(ArbiterServiceServer::new(Server::new(context)))
|
|
||||||
.serve(addr)
|
|
||||||
.await
|
|
||||||
.map_err(|e| miette::miette!("gRPC server error: {e}"))?;
|
|
||||||
|
|
||||||
unreachable!("gRPC server should run indefinitely");
|
|
||||||
}
|
}
|
||||||
|
|||||||
19
server/crates/arbiter-server/src/service/mod.rs
Normal file
19
server/crates/arbiter-server/src/service/mod.rs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#[cfg(windows)]
|
||||||
|
mod windows;
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub use windows::{install_service, run_service_dispatcher};
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
pub fn install_service(_: crate::cli::ServiceInstallArgs) -> miette::Result<()> {
|
||||||
|
Err(miette::miette!(
|
||||||
|
"service install is currently supported only on Windows"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
pub fn run_service_dispatcher(_: crate::cli::ServiceRunArgs) -> miette::Result<()> {
|
||||||
|
Err(miette::miette!(
|
||||||
|
"service run entrypoint is currently supported only on Windows"
|
||||||
|
))
|
||||||
|
}
|
||||||
230
server/crates/arbiter-server/src/service/windows.rs
Normal file
230
server/crates/arbiter-server/src/service/windows.rs
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
use std::{
|
||||||
|
ffi::OsString,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
process::Command,
|
||||||
|
sync::mpsc,
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use miette::{Context as _, IntoDiagnostic as _, miette};
|
||||||
|
use windows_service::{
|
||||||
|
define_windows_service,
|
||||||
|
service::{
|
||||||
|
ServiceAccess, ServiceControl, ServiceControlAccept, ServiceErrorControl, ServiceExitCode,
|
||||||
|
ServiceInfo, ServiceStartType, ServiceState, ServiceStatus, ServiceType,
|
||||||
|
},
|
||||||
|
service_control_handler::{self, ServiceControlHandlerResult},
|
||||||
|
service_dispatcher,
|
||||||
|
service_manager::{ServiceManager, ServiceManagerAccess},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::cli::{ServiceInstallArgs, ServiceRunArgs};
|
||||||
|
use arbiter_server::{RunConfig, run_server_until_shutdown};
|
||||||
|
|
||||||
|
const SERVICE_NAME: &str = "ArbiterServer";
|
||||||
|
const SERVICE_DISPLAY_NAME: &str = "Arbiter Server";
|
||||||
|
|
||||||
|
pub fn default_service_data_dir() -> PathBuf {
|
||||||
|
let base = std::env::var_os("PROGRAMDATA")
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.unwrap_or_else(|| PathBuf::from(r"C:\ProgramData"));
|
||||||
|
base.join("Arbiter")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn install_service(args: ServiceInstallArgs) -> miette::Result<()> {
|
||||||
|
ensure_admin_rights()?;
|
||||||
|
|
||||||
|
let executable = std::env::current_exe().into_diagnostic()?;
|
||||||
|
let data_dir = args.data_dir.unwrap_or_else(default_service_data_dir);
|
||||||
|
|
||||||
|
std::fs::create_dir_all(&data_dir)
|
||||||
|
.into_diagnostic()
|
||||||
|
.with_context(|| format!("failed to create service data dir: {}", data_dir.display()))?;
|
||||||
|
ensure_token_acl_contract(&data_dir)?;
|
||||||
|
|
||||||
|
let manager_access = ServiceManagerAccess::CONNECT | ServiceManagerAccess::CREATE_SERVICE;
|
||||||
|
let manager = ServiceManager::local_computer(None::<&str>, manager_access)
|
||||||
|
.into_diagnostic()
|
||||||
|
.wrap_err("failed to open Service Control Manager")?;
|
||||||
|
|
||||||
|
let launch_arguments = vec![
|
||||||
|
OsString::from("service"),
|
||||||
|
OsString::from("run"),
|
||||||
|
OsString::from("--data-dir"),
|
||||||
|
data_dir.as_os_str().to_os_string(),
|
||||||
|
];
|
||||||
|
|
||||||
|
let service_info = ServiceInfo {
|
||||||
|
name: OsString::from(SERVICE_NAME),
|
||||||
|
display_name: OsString::from(SERVICE_DISPLAY_NAME),
|
||||||
|
service_type: ServiceType::OWN_PROCESS,
|
||||||
|
start_type: ServiceStartType::AutoStart,
|
||||||
|
error_control: ServiceErrorControl::Normal,
|
||||||
|
executable_path: executable,
|
||||||
|
launch_arguments,
|
||||||
|
dependencies: vec![],
|
||||||
|
account_name: Some(OsString::from(r"NT AUTHORITY\LocalService")),
|
||||||
|
account_password: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let service = manager
|
||||||
|
.create_service(
|
||||||
|
&service_info,
|
||||||
|
ServiceAccess::QUERY_STATUS | ServiceAccess::START,
|
||||||
|
)
|
||||||
|
.into_diagnostic()
|
||||||
|
.wrap_err("failed to create Windows service in SCM")?;
|
||||||
|
|
||||||
|
if args.start {
|
||||||
|
service
|
||||||
|
.start::<&str>(&[])
|
||||||
|
.into_diagnostic()
|
||||||
|
.wrap_err("service created but failed to start")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_service_dispatcher(args: ServiceRunArgs) -> miette::Result<()> {
|
||||||
|
SERVICE_RUN_ARGS
|
||||||
|
.set(args)
|
||||||
|
.map_err(|_| miette!("service runtime args are already initialized"))?;
|
||||||
|
|
||||||
|
service_dispatcher::start(SERVICE_NAME, ffi_service_main)
|
||||||
|
.into_diagnostic()
|
||||||
|
.wrap_err("failed to start service dispatcher")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
define_windows_service!(ffi_service_main, service_main);
|
||||||
|
|
||||||
|
static SERVICE_RUN_ARGS: std::sync::OnceLock<ServiceRunArgs> = std::sync::OnceLock::new();
|
||||||
|
|
||||||
|
fn service_main(_arguments: Vec<OsString>) {
|
||||||
|
if let Err(error) = run_service_main() {
|
||||||
|
tracing::error!(error = ?error, "Windows service main failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_service_main() -> miette::Result<()> {
|
||||||
|
let args = SERVICE_RUN_ARGS
|
||||||
|
.get()
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| miette!("service run args are missing"))?;
|
||||||
|
|
||||||
|
let (shutdown_tx, shutdown_rx) = mpsc::channel::<()>();
|
||||||
|
|
||||||
|
let status_handle =
|
||||||
|
service_control_handler::register(SERVICE_NAME, move |control| match control {
|
||||||
|
ServiceControl::Stop => {
|
||||||
|
let _ = shutdown_tx.send(());
|
||||||
|
ServiceControlHandlerResult::NoError
|
||||||
|
}
|
||||||
|
ServiceControl::Interrogate => ServiceControlHandlerResult::NoError,
|
||||||
|
_ => ServiceControlHandlerResult::NotImplemented,
|
||||||
|
})
|
||||||
|
.into_diagnostic()
|
||||||
|
.wrap_err("failed to register service control handler")?;
|
||||||
|
|
||||||
|
set_status(
|
||||||
|
&status_handle,
|
||||||
|
ServiceState::StartPending,
|
||||||
|
ServiceControlAccept::empty(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.into_diagnostic()
|
||||||
|
.wrap_err("failed to build tokio runtime for service")?;
|
||||||
|
|
||||||
|
set_status(
|
||||||
|
&status_handle,
|
||||||
|
ServiceState::Running,
|
||||||
|
ServiceControlAccept::STOP,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let data_dir = args.data_dir.unwrap_or_else(default_service_data_dir);
|
||||||
|
let config = RunConfig {
|
||||||
|
addr: args.listen_addr,
|
||||||
|
data_dir: Some(data_dir),
|
||||||
|
log_arbiter_url: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = runtime.block_on(run_server_until_shutdown(config, async move {
|
||||||
|
let _ = tokio::task::spawn_blocking(move || shutdown_rx.recv()).await;
|
||||||
|
}));
|
||||||
|
|
||||||
|
set_status(
|
||||||
|
&status_handle,
|
||||||
|
ServiceState::Stopped,
|
||||||
|
ServiceControlAccept::empty(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_status(
|
||||||
|
status_handle: &service_control_handler::ServiceStatusHandle,
|
||||||
|
current_state: ServiceState,
|
||||||
|
controls_accepted: ServiceControlAccept,
|
||||||
|
) -> miette::Result<()> {
|
||||||
|
status_handle
|
||||||
|
.set_service_status(ServiceStatus {
|
||||||
|
service_type: ServiceType::OWN_PROCESS,
|
||||||
|
current_state,
|
||||||
|
controls_accepted,
|
||||||
|
exit_code: ServiceExitCode::Win32(0),
|
||||||
|
checkpoint: 0,
|
||||||
|
wait_hint: Duration::from_secs(10),
|
||||||
|
process_id: None,
|
||||||
|
})
|
||||||
|
.into_diagnostic()
|
||||||
|
.wrap_err("failed to update service state")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_admin_rights() -> miette::Result<()> {
|
||||||
|
let status = Command::new("net")
|
||||||
|
.arg("session")
|
||||||
|
.status()
|
||||||
|
.into_diagnostic()
|
||||||
|
.wrap_err("failed to check administrator rights")?;
|
||||||
|
|
||||||
|
if status.success() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(miette!(
|
||||||
|
"administrator privileges are required to install Windows service"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_token_acl_contract(data_dir: &Path) -> miette::Result<()> {
|
||||||
|
// IMPORTANT: Keep this ACL setup explicit.
|
||||||
|
// The service account needs write access, while the interactive user only needs read access
|
||||||
|
// to the bootstrap token and service data directory.
|
||||||
|
let target = data_dir.as_os_str();
|
||||||
|
|
||||||
|
let status = Command::new("icacls")
|
||||||
|
.arg(target)
|
||||||
|
.arg("/grant")
|
||||||
|
.arg("*S-1-5-19:(OI)(CI)M")
|
||||||
|
.arg("/grant")
|
||||||
|
.arg("*S-1-5-32-545:(OI)(CI)RX")
|
||||||
|
.arg("/T")
|
||||||
|
.arg("/C")
|
||||||
|
.status()
|
||||||
|
.into_diagnostic()
|
||||||
|
.wrap_err("failed to apply ACLs for service data directory")?;
|
||||||
|
|
||||||
|
if status.success() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(miette!(
|
||||||
|
"failed to ensure ACL contract for service data directory"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
16
server/crates/arbiter-server/src/utils.rs
Normal file
16
server/crates/arbiter-server/src/utils.rs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
struct DeferClosure<F: FnOnce()> {
|
||||||
|
f: Option<F>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<F: FnOnce()> Drop for DeferClosure<F> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Some(f) = self.f.take() {
|
||||||
|
f();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run some code when a scope is exited, similar to Go's defer statement
|
||||||
|
pub fn defer<F: FnOnce()>(f: F) -> impl Drop + Sized {
|
||||||
|
DeferClosure { f: Some(f) }
|
||||||
|
}
|
||||||
@@ -1,15 +1,52 @@
|
|||||||
use arbiter_proto::transport::Bi;
|
use arbiter_proto::ClientMetadata;
|
||||||
|
use arbiter_proto::transport::{Receiver, Sender};
|
||||||
use arbiter_server::actors::GlobalActors;
|
use arbiter_server::actors::GlobalActors;
|
||||||
use arbiter_server::{
|
use arbiter_server::{
|
||||||
actors::client::{ClientConnection, Request, Response, connect_client},
|
actors::client::{ClientConnection, auth, connect_client},
|
||||||
db::{self, schema},
|
db,
|
||||||
};
|
};
|
||||||
use diesel::{ExpressionMethods as _, insert_into};
|
use diesel::{ExpressionMethods as _, NullableExpressionMethods as _, QueryDsl as _, insert_into};
|
||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::RunQueryDsl;
|
||||||
use ed25519_dalek::Signer as _;
|
use ed25519_dalek::Signer as _;
|
||||||
|
|
||||||
use super::common::ChannelTransport;
|
use super::common::ChannelTransport;
|
||||||
|
|
||||||
|
fn metadata(name: &str, description: Option<&str>, version: Option<&str>) -> ClientMetadata {
|
||||||
|
ClientMetadata {
|
||||||
|
name: name.to_owned(),
|
||||||
|
description: description.map(str::to_owned),
|
||||||
|
version: version.map(str::to_owned),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn insert_registered_client(
|
||||||
|
db: &db::DatabasePool,
|
||||||
|
pubkey: Vec<u8>,
|
||||||
|
metadata: &ClientMetadata,
|
||||||
|
) {
|
||||||
|
use arbiter_server::db::schema::{client_metadata, program_client};
|
||||||
|
|
||||||
|
let mut conn = db.get().await.unwrap();
|
||||||
|
let metadata_id: i32 = insert_into(client_metadata::table)
|
||||||
|
.values((
|
||||||
|
client_metadata::name.eq(&metadata.name),
|
||||||
|
client_metadata::description.eq(&metadata.description),
|
||||||
|
client_metadata::version.eq(&metadata.version),
|
||||||
|
))
|
||||||
|
.returning(client_metadata::id)
|
||||||
|
.get_result(&mut conn)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
insert_into(program_client::table)
|
||||||
|
.values((
|
||||||
|
program_client::public_key.eq(pubkey),
|
||||||
|
program_client::metadata_id.eq(metadata_id),
|
||||||
|
))
|
||||||
|
.execute(&mut conn)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[test_log::test]
|
#[test_log::test]
|
||||||
pub async fn test_unregistered_pubkey_rejected() {
|
pub async fn test_unregistered_pubkey_rejected() {
|
||||||
@@ -17,15 +54,18 @@ pub async fn test_unregistered_pubkey_rejected() {
|
|||||||
|
|
||||||
let (server_transport, mut test_transport) = ChannelTransport::new();
|
let (server_transport, mut test_transport) = ChannelTransport::new();
|
||||||
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
|
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
|
||||||
let props = ClientConnection::new(db.clone(), Box::new(server_transport), actors);
|
let props = ClientConnection::new(db.clone(), actors);
|
||||||
let task = tokio::spawn(connect_client(props));
|
let task = tokio::spawn(async move {
|
||||||
|
let mut server_transport = server_transport;
|
||||||
|
connect_client(props, &mut server_transport).await;
|
||||||
|
});
|
||||||
|
|
||||||
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
||||||
let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec();
|
|
||||||
|
|
||||||
test_transport
|
test_transport
|
||||||
.send(Request::AuthChallengeRequest {
|
.send(auth::Inbound::AuthChallengeRequest {
|
||||||
pubkey: pubkey_bytes,
|
pubkey: new_key.verifying_key(),
|
||||||
|
metadata: metadata("client", Some("desc"), Some("1.0.0")),
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -42,25 +82,27 @@ pub async fn test_challenge_auth() {
|
|||||||
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
||||||
let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec();
|
let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec();
|
||||||
|
|
||||||
{
|
insert_registered_client(
|
||||||
let mut conn = db.get().await.unwrap();
|
&db,
|
||||||
insert_into(schema::program_client::table)
|
pubkey_bytes.clone(),
|
||||||
.values(schema::program_client::public_key.eq(pubkey_bytes.clone()))
|
&metadata("client", Some("desc"), Some("1.0.0")),
|
||||||
.execute(&mut conn)
|
)
|
||||||
.await
|
.await;
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
let (server_transport, mut test_transport) = ChannelTransport::new();
|
let (server_transport, mut test_transport) = ChannelTransport::new();
|
||||||
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
|
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
|
||||||
|
|
||||||
let props = ClientConnection::new(db.clone(), Box::new(server_transport), actors);
|
let props = ClientConnection::new(db.clone(), actors);
|
||||||
let task = tokio::spawn(connect_client(props));
|
let task = tokio::spawn(async move {
|
||||||
|
let mut server_transport = server_transport;
|
||||||
|
connect_client(props, &mut server_transport).await;
|
||||||
|
});
|
||||||
|
|
||||||
// Send challenge request
|
// Send challenge request
|
||||||
test_transport
|
test_transport
|
||||||
.send(Request::AuthChallengeRequest {
|
.send(auth::Inbound::AuthChallengeRequest {
|
||||||
pubkey: pubkey_bytes,
|
pubkey: new_key.verifying_key(),
|
||||||
|
metadata: metadata("client", Some("desc"), Some("1.0.0")),
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -72,23 +114,211 @@ pub async fn test_challenge_auth() {
|
|||||||
.expect("should receive challenge");
|
.expect("should receive challenge");
|
||||||
let challenge = match response {
|
let challenge = match response {
|
||||||
Ok(resp) => match resp {
|
Ok(resp) => match resp {
|
||||||
Response::AuthChallenge { pubkey, nonce } => (pubkey, nonce),
|
auth::Outbound::AuthChallenge { pubkey, nonce } => (pubkey, nonce),
|
||||||
other => panic!("Expected AuthChallenge, got {other:?}"),
|
other => panic!("Expected AuthChallenge, got {other:?}"),
|
||||||
},
|
},
|
||||||
Err(err) => panic!("Expected Ok response, got Err({err:?})"),
|
Err(err) => panic!("Expected Ok response, got Err({err:?})"),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sign the challenge and send solution
|
// Sign the challenge and send solution
|
||||||
let formatted_challenge = arbiter_proto::format_challenge(challenge.1, &challenge.0);
|
let formatted_challenge = arbiter_proto::format_challenge(challenge.1, challenge.0.as_bytes());
|
||||||
let signature = new_key.sign(&formatted_challenge);
|
let signature = new_key.sign(&formatted_challenge);
|
||||||
|
|
||||||
test_transport
|
test_transport
|
||||||
.send(Request::AuthChallengeSolution {
|
.send(auth::Inbound::AuthChallengeSolution { signature })
|
||||||
signature: signature.to_bytes().to_vec(),
|
|
||||||
})
|
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
let response = test_transport
|
||||||
|
.recv()
|
||||||
|
.await
|
||||||
|
.expect("should receive auth success");
|
||||||
|
match response {
|
||||||
|
Ok(auth::Outbound::AuthSuccess) => {}
|
||||||
|
Ok(other) => panic!("Expected AuthSuccess, got {other:?}"),
|
||||||
|
Err(err) => panic!("Expected Ok response, got Err({err:?})"),
|
||||||
|
}
|
||||||
|
|
||||||
// Auth completes, session spawned
|
// Auth completes, session spawned
|
||||||
task.await.unwrap();
|
task.await.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[test_log::test]
|
||||||
|
pub async fn test_metadata_unchanged_does_not_append_history() {
|
||||||
|
let db = db::create_test_pool().await;
|
||||||
|
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
|
||||||
|
let props = ClientConnection::new(db.clone(), actors);
|
||||||
|
|
||||||
|
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
||||||
|
let requested = metadata("client", Some("desc"), Some("1.0.0"));
|
||||||
|
|
||||||
|
{
|
||||||
|
use arbiter_server::db::schema::{client_metadata, program_client};
|
||||||
|
let mut conn = db.get().await.unwrap();
|
||||||
|
let metadata_id: i32 = insert_into(client_metadata::table)
|
||||||
|
.values((
|
||||||
|
client_metadata::name.eq(&requested.name),
|
||||||
|
client_metadata::description.eq(&requested.description),
|
||||||
|
client_metadata::version.eq(&requested.version),
|
||||||
|
))
|
||||||
|
.returning(client_metadata::id)
|
||||||
|
.get_result(&mut conn)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
insert_into(program_client::table)
|
||||||
|
.values((
|
||||||
|
program_client::public_key.eq(new_key.verifying_key().to_bytes().to_vec()),
|
||||||
|
program_client::metadata_id.eq(metadata_id),
|
||||||
|
))
|
||||||
|
.execute(&mut conn)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let (server_transport, mut test_transport) = ChannelTransport::new();
|
||||||
|
let task = tokio::spawn(async move {
|
||||||
|
let mut server_transport = server_transport;
|
||||||
|
connect_client(props, &mut server_transport).await;
|
||||||
|
});
|
||||||
|
|
||||||
|
test_transport
|
||||||
|
.send(auth::Inbound::AuthChallengeRequest {
|
||||||
|
pubkey: new_key.verifying_key(),
|
||||||
|
metadata: requested,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let response = test_transport.recv().await.unwrap().unwrap();
|
||||||
|
let (pubkey, nonce) = match response {
|
||||||
|
auth::Outbound::AuthChallenge { pubkey, nonce } => (pubkey, nonce),
|
||||||
|
other => panic!("Expected AuthChallenge, got {other:?}"),
|
||||||
|
};
|
||||||
|
let signature = new_key.sign(&arbiter_proto::format_challenge(nonce, pubkey.as_bytes()));
|
||||||
|
test_transport
|
||||||
|
.send(auth::Inbound::AuthChallengeSolution { signature })
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let _ = test_transport.recv().await.unwrap();
|
||||||
|
task.await.unwrap();
|
||||||
|
|
||||||
|
{
|
||||||
|
use arbiter_server::db::schema::{client_metadata, client_metadata_history};
|
||||||
|
let mut conn = db.get().await.unwrap();
|
||||||
|
let metadata_count: i64 = client_metadata::table
|
||||||
|
.count()
|
||||||
|
.get_result(&mut conn)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let history_count: i64 = client_metadata_history::table
|
||||||
|
.count()
|
||||||
|
.get_result(&mut conn)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(metadata_count, 1);
|
||||||
|
assert_eq!(history_count, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[test_log::test]
|
||||||
|
pub async fn test_metadata_change_appends_history_and_repoints_binding() {
|
||||||
|
let db = db::create_test_pool().await;
|
||||||
|
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
|
||||||
|
let props = ClientConnection::new(db.clone(), actors);
|
||||||
|
|
||||||
|
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
||||||
|
|
||||||
|
{
|
||||||
|
use arbiter_server::db::schema::{client_metadata, program_client};
|
||||||
|
let mut conn = db.get().await.unwrap();
|
||||||
|
let metadata_id: i32 = insert_into(client_metadata::table)
|
||||||
|
.values((
|
||||||
|
client_metadata::name.eq("client"),
|
||||||
|
client_metadata::description.eq(Some("old")),
|
||||||
|
client_metadata::version.eq(Some("1.0.0")),
|
||||||
|
))
|
||||||
|
.returning(client_metadata::id)
|
||||||
|
.get_result(&mut conn)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
insert_into(program_client::table)
|
||||||
|
.values((
|
||||||
|
program_client::public_key.eq(new_key.verifying_key().to_bytes().to_vec()),
|
||||||
|
program_client::metadata_id.eq(metadata_id),
|
||||||
|
))
|
||||||
|
.execute(&mut conn)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let (server_transport, mut test_transport) = ChannelTransport::new();
|
||||||
|
let task = tokio::spawn(async move {
|
||||||
|
let mut server_transport = server_transport;
|
||||||
|
connect_client(props, &mut server_transport).await;
|
||||||
|
});
|
||||||
|
|
||||||
|
test_transport
|
||||||
|
.send(auth::Inbound::AuthChallengeRequest {
|
||||||
|
pubkey: new_key.verifying_key(),
|
||||||
|
metadata: metadata("client", Some("new"), Some("2.0.0")),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let response = test_transport.recv().await.unwrap().unwrap();
|
||||||
|
let (pubkey, nonce) = match response {
|
||||||
|
auth::Outbound::AuthChallenge { pubkey, nonce } => (pubkey, nonce),
|
||||||
|
other => panic!("Expected AuthChallenge, got {other:?}"),
|
||||||
|
};
|
||||||
|
let signature = new_key.sign(&arbiter_proto::format_challenge(nonce, pubkey.as_bytes()));
|
||||||
|
test_transport
|
||||||
|
.send(auth::Inbound::AuthChallengeSolution { signature })
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let _ = test_transport.recv().await.unwrap();
|
||||||
|
task.await.unwrap();
|
||||||
|
|
||||||
|
{
|
||||||
|
use arbiter_server::db::schema::{
|
||||||
|
client_metadata, client_metadata_history, program_client,
|
||||||
|
};
|
||||||
|
let mut conn = db.get().await.unwrap();
|
||||||
|
let metadata_count: i64 = client_metadata::table
|
||||||
|
.count()
|
||||||
|
.get_result(&mut conn)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let history_count: i64 = client_metadata_history::table
|
||||||
|
.count()
|
||||||
|
.get_result(&mut conn)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let metadata_id = program_client::table
|
||||||
|
.select(program_client::metadata_id)
|
||||||
|
.first::<i32>(&mut conn)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let current = client_metadata::table
|
||||||
|
.find(metadata_id)
|
||||||
|
.select((
|
||||||
|
client_metadata::name,
|
||||||
|
client_metadata::description.nullable(),
|
||||||
|
client_metadata::version.nullable(),
|
||||||
|
))
|
||||||
|
.first::<(String, Option<String>, Option<String>)>(&mut conn)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(metadata_count, 2);
|
||||||
|
assert_eq!(history_count, 1);
|
||||||
|
assert_eq!(
|
||||||
|
current,
|
||||||
|
(
|
||||||
|
"client".to_owned(),
|
||||||
|
Some("new".to_owned()),
|
||||||
|
Some("2.0.0".to_owned())
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
use arbiter_proto::transport::{Bi, Error};
|
use arbiter_proto::transport::{Bi, Error, Receiver, Sender};
|
||||||
use arbiter_server::{
|
use arbiter_server::{
|
||||||
actors::keyholder::KeyHolder,
|
actors::keyholder::KeyHolder,
|
||||||
db::{self, schema}, safe_cell::{SafeCell, SafeCellHandle as _},
|
db::{self, schema},
|
||||||
|
safe_cell::{SafeCell, SafeCellHandle as _},
|
||||||
};
|
};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use diesel::QueryDsl;
|
use diesel::QueryDsl;
|
||||||
@@ -54,10 +55,10 @@ impl<T, Y> ChannelTransport<T, Y> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl<T, Y> Bi<T, Y> for ChannelTransport<T, Y>
|
impl<T, Y> Sender<Y> for ChannelTransport<T, Y>
|
||||||
where
|
where
|
||||||
T: Send + 'static,
|
T: Send + Sync + 'static,
|
||||||
Y: Send + 'static,
|
Y: Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
async fn send(&mut self, item: Y) -> Result<(), Error> {
|
async fn send(&mut self, item: Y) -> Result<(), Error> {
|
||||||
self.sender
|
self.sender
|
||||||
@@ -65,8 +66,22 @@ where
|
|||||||
.await
|
.await
|
||||||
.map_err(|_| Error::ChannelClosed)
|
.map_err(|_| Error::ChannelClosed)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<T, Y> Receiver<T> for ChannelTransport<T, Y>
|
||||||
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
Y: Send + Sync + 'static,
|
||||||
|
{
|
||||||
async fn recv(&mut self) -> Option<T> {
|
async fn recv(&mut self) -> Option<T> {
|
||||||
self.receiver.recv().await
|
self.receiver.recv().await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T, Y> Bi<T, Y> for ChannelTransport<T, Y>
|
||||||
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
Y: Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
use arbiter_proto::transport::Bi;
|
use arbiter_proto::transport::{Receiver, Sender};
|
||||||
use arbiter_server::{
|
use arbiter_server::{
|
||||||
actors::{
|
actors::{
|
||||||
GlobalActors,
|
GlobalActors,
|
||||||
bootstrap::GetToken,
|
bootstrap::GetToken,
|
||||||
user_agent::{AuthPublicKey, Request, Response, UserAgentConnection, connect_user_agent},
|
user_agent::{AuthPublicKey, UserAgentConnection, auth},
|
||||||
},
|
},
|
||||||
db::{self, schema},
|
db::{self, schema},
|
||||||
};
|
};
|
||||||
@@ -21,19 +21,31 @@ pub async fn test_bootstrap_token_auth() {
|
|||||||
let token = actors.bootstrapper.ask(GetToken).await.unwrap().unwrap();
|
let token = actors.bootstrapper.ask(GetToken).await.unwrap().unwrap();
|
||||||
|
|
||||||
let (server_transport, mut test_transport) = ChannelTransport::new();
|
let (server_transport, mut test_transport) = ChannelTransport::new();
|
||||||
let props = UserAgentConnection::new(db.clone(), actors, Box::new(server_transport));
|
let db_for_task = db.clone();
|
||||||
let task = tokio::spawn(connect_user_agent(props));
|
let task = tokio::spawn(async move {
|
||||||
|
let mut props = UserAgentConnection::new(db_for_task, actors);
|
||||||
|
auth::authenticate(&mut props, server_transport).await
|
||||||
|
});
|
||||||
|
|
||||||
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
||||||
test_transport
|
test_transport
|
||||||
.send(Request::AuthChallengeRequest {
|
.send(auth::Inbound::AuthChallengeRequest {
|
||||||
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
|
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
|
||||||
bootstrap_token: Some(token),
|
bootstrap_token: Some(token),
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
task.await.unwrap();
|
let response = test_transport
|
||||||
|
.recv()
|
||||||
|
.await
|
||||||
|
.expect("should receive auth result");
|
||||||
|
match response {
|
||||||
|
Ok(auth::Outbound::AuthSuccess) => {}
|
||||||
|
other => panic!("Expected AuthSuccess, got {other:?}"),
|
||||||
|
}
|
||||||
|
|
||||||
|
task.await.unwrap().unwrap();
|
||||||
|
|
||||||
let mut conn = db.get().await.unwrap();
|
let mut conn = db.get().await.unwrap();
|
||||||
let stored_pubkey: Vec<u8> = schema::useragent_client::table
|
let stored_pubkey: Vec<u8> = schema::useragent_client::table
|
||||||
@@ -51,20 +63,25 @@ pub async fn test_bootstrap_invalid_token_auth() {
|
|||||||
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
|
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
|
||||||
|
|
||||||
let (server_transport, mut test_transport) = ChannelTransport::new();
|
let (server_transport, mut test_transport) = ChannelTransport::new();
|
||||||
let props = UserAgentConnection::new(db.clone(), actors, Box::new(server_transport));
|
let db_for_task = db.clone();
|
||||||
let task = tokio::spawn(connect_user_agent(props));
|
let task = tokio::spawn(async move {
|
||||||
|
let mut props = UserAgentConnection::new(db_for_task, actors);
|
||||||
|
auth::authenticate(&mut props, server_transport).await
|
||||||
|
});
|
||||||
|
|
||||||
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
||||||
test_transport
|
test_transport
|
||||||
.send(Request::AuthChallengeRequest {
|
.send(auth::Inbound::AuthChallengeRequest {
|
||||||
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
|
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
|
||||||
bootstrap_token: Some("invalid_token".to_string()),
|
bootstrap_token: Some("invalid_token".to_string()),
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Auth fails, connect_user_agent returns, transport drops
|
assert!(matches!(
|
||||||
task.await.unwrap();
|
task.await.unwrap(),
|
||||||
|
Err(auth::Error::InvalidBootstrapToken)
|
||||||
|
));
|
||||||
|
|
||||||
// Verify no key was registered
|
// Verify no key was registered
|
||||||
let mut conn = db.get().await.unwrap();
|
let mut conn = db.get().await.unwrap();
|
||||||
@@ -99,12 +116,15 @@ pub async fn test_challenge_auth() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let (server_transport, mut test_transport) = ChannelTransport::new();
|
let (server_transport, mut test_transport) = ChannelTransport::new();
|
||||||
let props = UserAgentConnection::new(db.clone(), actors, Box::new(server_transport));
|
let db_for_task = db.clone();
|
||||||
let task = tokio::spawn(connect_user_agent(props));
|
let task = tokio::spawn(async move {
|
||||||
|
let mut props = UserAgentConnection::new(db_for_task, actors);
|
||||||
|
auth::authenticate(&mut props, server_transport).await
|
||||||
|
});
|
||||||
|
|
||||||
// Send challenge request
|
// Send challenge request
|
||||||
test_transport
|
test_transport
|
||||||
.send(Request::AuthChallengeRequest {
|
.send(auth::Inbound::AuthChallengeRequest {
|
||||||
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
|
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
|
||||||
bootstrap_token: None,
|
bootstrap_token: None,
|
||||||
})
|
})
|
||||||
@@ -118,7 +138,7 @@ pub async fn test_challenge_auth() {
|
|||||||
.expect("should receive challenge");
|
.expect("should receive challenge");
|
||||||
let challenge = match response {
|
let challenge = match response {
|
||||||
Ok(resp) => match resp {
|
Ok(resp) => match resp {
|
||||||
Response::AuthChallenge { nonce } => nonce,
|
auth::Outbound::AuthChallenge { nonce } => nonce,
|
||||||
other => panic!("Expected AuthChallenge, got {other:?}"),
|
other => panic!("Expected AuthChallenge, got {other:?}"),
|
||||||
},
|
},
|
||||||
Err(err) => panic!("Expected Ok response, got Err({err:?})"),
|
Err(err) => panic!("Expected Ok response, got Err({err:?})"),
|
||||||
@@ -128,12 +148,20 @@ pub async fn test_challenge_auth() {
|
|||||||
let signature = new_key.sign(&formatted_challenge);
|
let signature = new_key.sign(&formatted_challenge);
|
||||||
|
|
||||||
test_transport
|
test_transport
|
||||||
.send(Request::AuthChallengeSolution {
|
.send(auth::Inbound::AuthChallengeSolution {
|
||||||
signature: signature.to_bytes().to_vec(),
|
signature: signature.to_bytes().to_vec(),
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Auth completes, session spawned
|
let response = test_transport
|
||||||
task.await.unwrap();
|
.recv()
|
||||||
|
.await
|
||||||
|
.expect("should receive auth result");
|
||||||
|
match response {
|
||||||
|
Ok(auth::Outbound::AuthSuccess) => {}
|
||||||
|
other => panic!("Expected AuthSuccess, got {other:?}"),
|
||||||
|
}
|
||||||
|
|
||||||
|
task.await.unwrap().unwrap();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,20 @@ use arbiter_server::{
|
|||||||
actors::{
|
actors::{
|
||||||
GlobalActors,
|
GlobalActors,
|
||||||
keyholder::{Bootstrap, Seal},
|
keyholder::{Bootstrap, Seal},
|
||||||
user_agent::{Request, Response, UnsealError, session::UserAgentSession},
|
user_agent::{UserAgentSession, session::connection::{
|
||||||
|
HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError,
|
||||||
|
}},
|
||||||
},
|
},
|
||||||
db,
|
db,
|
||||||
safe_cell::{SafeCell, SafeCellHandle as _},
|
safe_cell::{SafeCell, SafeCellHandle as _},
|
||||||
};
|
};
|
||||||
use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
|
use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
|
||||||
|
use kameo::actor::Spawn as _;
|
||||||
use x25519_dalek::{EphemeralSecret, PublicKey};
|
use x25519_dalek::{EphemeralSecret, PublicKey};
|
||||||
|
|
||||||
async fn setup_sealed_user_agent(seal_key: &[u8]) -> (db::DatabasePool, UserAgentSession) {
|
async fn setup_sealed_user_agent(
|
||||||
|
seal_key: &[u8],
|
||||||
|
) -> (db::DatabasePool, kameo::actor::ActorRef<UserAgentSession>) {
|
||||||
let db = db::create_test_pool().await;
|
let db = db::create_test_pool().await;
|
||||||
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
|
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
|
||||||
|
|
||||||
@@ -23,26 +28,26 @@ async fn setup_sealed_user_agent(seal_key: &[u8]) -> (db::DatabasePool, UserAgen
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
actors.key_holder.ask(Seal).await.unwrap();
|
actors.key_holder.ask(Seal).await.unwrap();
|
||||||
|
|
||||||
let session = UserAgentSession::new_test(db.clone(), actors);
|
let session = UserAgentSession::spawn(UserAgentSession::new_test(db.clone(), actors));
|
||||||
|
|
||||||
(db, session)
|
(db, session)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn client_dh_encrypt(user_agent: &mut UserAgentSession, key_to_send: &[u8]) -> Request {
|
async fn client_dh_encrypt(
|
||||||
|
user_agent: &kameo::actor::ActorRef<UserAgentSession>,
|
||||||
|
key_to_send: &[u8],
|
||||||
|
) -> HandleUnsealEncryptedKey {
|
||||||
let client_secret = EphemeralSecret::random();
|
let client_secret = EphemeralSecret::random();
|
||||||
let client_public = PublicKey::from(&client_secret);
|
let client_public = PublicKey::from(&client_secret);
|
||||||
|
|
||||||
let response = user_agent
|
let response = user_agent
|
||||||
.process_transport_inbound(Request::UnsealStart {
|
.ask(HandleUnsealRequest {
|
||||||
client_pubkey: client_public,
|
client_pubkey: client_public,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let server_pubkey = match response {
|
let server_pubkey = response.server_pubkey;
|
||||||
Response::UnsealStartResponse { server_pubkey } => server_pubkey,
|
|
||||||
other => panic!("Expected UnsealStartResponse, got {other:?}"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let shared_secret = client_secret.diffie_hellman(&server_pubkey);
|
let shared_secret = client_secret.diffie_hellman(&server_pubkey);
|
||||||
let cipher = XChaCha20Poly1305::new(shared_secret.as_bytes().into());
|
let cipher = XChaCha20Poly1305::new(shared_secret.as_bytes().into());
|
||||||
@@ -53,7 +58,7 @@ async fn client_dh_encrypt(user_agent: &mut UserAgentSession, key_to_send: &[u8]
|
|||||||
.encrypt_in_place(&nonce, associated_data, &mut ciphertext)
|
.encrypt_in_place(&nonce, associated_data, &mut ciphertext)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
Request::UnsealEncryptedKey {
|
HandleUnsealEncryptedKey {
|
||||||
nonce: nonce.to_vec(),
|
nonce: nonce.to_vec(),
|
||||||
ciphertext,
|
ciphertext,
|
||||||
associated_data: associated_data.to_vec(),
|
associated_data: associated_data.to_vec(),
|
||||||
@@ -64,63 +69,58 @@ async fn client_dh_encrypt(user_agent: &mut UserAgentSession, key_to_send: &[u8]
|
|||||||
#[test_log::test]
|
#[test_log::test]
|
||||||
pub async fn test_unseal_success() {
|
pub async fn test_unseal_success() {
|
||||||
let seal_key = b"test-seal-key";
|
let seal_key = b"test-seal-key";
|
||||||
let (_db, mut user_agent) = setup_sealed_user_agent(seal_key).await;
|
let (_db, user_agent) = setup_sealed_user_agent(seal_key).await;
|
||||||
|
|
||||||
let encrypted_key = client_dh_encrypt(&mut user_agent, seal_key).await;
|
let encrypted_key = client_dh_encrypt(&user_agent, seal_key).await;
|
||||||
|
|
||||||
let response = user_agent
|
let response = user_agent.ask(encrypted_key).await;
|
||||||
.process_transport_inbound(encrypted_key)
|
assert!(matches!(response, Ok(())));
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(matches!(response, Response::UnsealResult(Ok(()))));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[test_log::test]
|
#[test_log::test]
|
||||||
pub async fn test_unseal_wrong_seal_key() {
|
pub async fn test_unseal_wrong_seal_key() {
|
||||||
let (_db, mut user_agent) = setup_sealed_user_agent(b"correct-key").await;
|
let (_db, user_agent) = setup_sealed_user_agent(b"correct-key").await;
|
||||||
|
|
||||||
let encrypted_key = client_dh_encrypt(&mut user_agent, b"wrong-key").await;
|
let encrypted_key = client_dh_encrypt(&user_agent, b"wrong-key").await;
|
||||||
|
|
||||||
let response = user_agent
|
|
||||||
.process_transport_inbound(encrypted_key)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
|
let response = user_agent.ask(encrypted_key).await;
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
response,
|
response,
|
||||||
Response::UnsealResult(Err(UnsealError::InvalidKey))
|
Err(kameo::error::SendError::HandlerError(
|
||||||
|
UnsealError::InvalidKey
|
||||||
|
))
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[test_log::test]
|
#[test_log::test]
|
||||||
pub async fn test_unseal_corrupted_ciphertext() {
|
pub async fn test_unseal_corrupted_ciphertext() {
|
||||||
let (_db, mut user_agent) = setup_sealed_user_agent(b"test-key").await;
|
let (_db, user_agent) = setup_sealed_user_agent(b"test-key").await;
|
||||||
|
|
||||||
let client_secret = EphemeralSecret::random();
|
let client_secret = EphemeralSecret::random();
|
||||||
let client_public = PublicKey::from(&client_secret);
|
let client_public = PublicKey::from(&client_secret);
|
||||||
|
|
||||||
user_agent
|
user_agent
|
||||||
.process_transport_inbound(Request::UnsealStart {
|
.ask(HandleUnsealRequest {
|
||||||
client_pubkey: client_public,
|
client_pubkey: client_public,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let response = user_agent
|
let response = user_agent
|
||||||
.process_transport_inbound(Request::UnsealEncryptedKey {
|
.ask(HandleUnsealEncryptedKey {
|
||||||
nonce: vec![0u8; 24],
|
nonce: vec![0u8; 24],
|
||||||
ciphertext: vec![0u8; 32],
|
ciphertext: vec![0u8; 32],
|
||||||
associated_data: vec![],
|
associated_data: vec![],
|
||||||
})
|
})
|
||||||
.await
|
.await;
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
response,
|
response,
|
||||||
Response::UnsealResult(Err(UnsealError::InvalidKey))
|
Err(kameo::error::SendError::HandlerError(
|
||||||
|
UnsealError::InvalidKey
|
||||||
|
))
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,30 +128,24 @@ pub async fn test_unseal_corrupted_ciphertext() {
|
|||||||
#[test_log::test]
|
#[test_log::test]
|
||||||
pub async fn test_unseal_retry_after_invalid_key() {
|
pub async fn test_unseal_retry_after_invalid_key() {
|
||||||
let seal_key = b"real-seal-key";
|
let seal_key = b"real-seal-key";
|
||||||
let (_db, mut user_agent) = setup_sealed_user_agent(seal_key).await;
|
let (_db, user_agent) = setup_sealed_user_agent(seal_key).await;
|
||||||
|
|
||||||
{
|
{
|
||||||
let encrypted_key = client_dh_encrypt(&mut user_agent, b"wrong-key").await;
|
let encrypted_key = client_dh_encrypt(&user_agent, b"wrong-key").await;
|
||||||
|
|
||||||
let response = user_agent
|
|
||||||
.process_transport_inbound(encrypted_key)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
|
let response = user_agent.ask(encrypted_key).await;
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
response,
|
response,
|
||||||
Response::UnsealResult(Err(UnsealError::InvalidKey))
|
Err(kameo::error::SendError::HandlerError(
|
||||||
|
UnsealError::InvalidKey
|
||||||
|
))
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let encrypted_key = client_dh_encrypt(&mut user_agent, seal_key).await;
|
let encrypted_key = client_dh_encrypt(&user_agent, seal_key).await;
|
||||||
|
|
||||||
let response = user_agent
|
let response = user_agent.ask(encrypted_key).await;
|
||||||
.process_transport_inbound(encrypted_key)
|
assert!(matches!(response, Ok(())));
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(matches!(response, Response::UnsealResult(Ok(()))));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
16
useragent/lib/features/callouts/active_callout.dart
Normal file
16
useragent/lib/features/callouts/active_callout.dart
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import 'package:arbiter/features/callouts/callout_event.dart';
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'active_callout.freezed.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class ActiveCallout with _$ActiveCallout {
|
||||||
|
const factory ActiveCallout({
|
||||||
|
required String id,
|
||||||
|
required String title,
|
||||||
|
required String description,
|
||||||
|
String? iconUrl,
|
||||||
|
required DateTime addedAt,
|
||||||
|
required CalloutData data,
|
||||||
|
}) = _ActiveCallout;
|
||||||
|
}
|
||||||
304
useragent/lib/features/callouts/active_callout.freezed.dart
Normal file
304
useragent/lib/features/callouts/active_callout.freezed.dart
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// coverage:ignore-file
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||||
|
|
||||||
|
part of 'active_callout.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// dart format off
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$ActiveCallout {
|
||||||
|
|
||||||
|
String get id; String get title; String get description; String? get iconUrl; DateTime get addedAt; CalloutData get data;
|
||||||
|
/// Create a copy of ActiveCallout
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$ActiveCalloutCopyWith<ActiveCallout> get copyWith => _$ActiveCalloutCopyWithImpl<ActiveCallout>(this as ActiveCallout, _$identity);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is ActiveCallout&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.iconUrl, iconUrl) || other.iconUrl == iconUrl)&&(identical(other.addedAt, addedAt) || other.addedAt == addedAt)&&(identical(other.data, data) || other.data == data));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,id,title,description,iconUrl,addedAt,data);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'ActiveCallout(id: $id, title: $title, description: $description, iconUrl: $iconUrl, addedAt: $addedAt, data: $data)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class $ActiveCalloutCopyWith<$Res> {
|
||||||
|
factory $ActiveCalloutCopyWith(ActiveCallout value, $Res Function(ActiveCallout) _then) = _$ActiveCalloutCopyWithImpl;
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
String id, String title, String description, String? iconUrl, DateTime addedAt, CalloutData data
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
$CalloutDataCopyWith<$Res> get data;
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class _$ActiveCalloutCopyWithImpl<$Res>
|
||||||
|
implements $ActiveCalloutCopyWith<$Res> {
|
||||||
|
_$ActiveCalloutCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final ActiveCallout _self;
|
||||||
|
final $Res Function(ActiveCallout) _then;
|
||||||
|
|
||||||
|
/// Create a copy of ActiveCallout
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? title = null,Object? description = null,Object? iconUrl = freezed,Object? addedAt = null,Object? data = null,}) {
|
||||||
|
return _then(_self.copyWith(
|
||||||
|
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,description: null == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,iconUrl: freezed == iconUrl ? _self.iconUrl : iconUrl // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,addedAt: null == addedAt ? _self.addedAt : addedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,data: null == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
|
||||||
|
as CalloutData,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
/// Create a copy of ActiveCallout
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$CalloutDataCopyWith<$Res> get data {
|
||||||
|
|
||||||
|
return $CalloutDataCopyWith<$Res>(_self.data, (value) {
|
||||||
|
return _then(_self.copyWith(data: value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Adds pattern-matching-related methods to [ActiveCallout].
|
||||||
|
extension ActiveCalloutPatterns on ActiveCallout {
|
||||||
|
/// A variant of `map` that fallback to returning `orElse`.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return orElse();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _ActiveCallout value)? $default,{required TResult orElse(),}){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _ActiveCallout() when $default != null:
|
||||||
|
return $default(_that);case _:
|
||||||
|
return orElse();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A `switch`-like method, using callbacks.
|
||||||
|
///
|
||||||
|
/// Callbacks receives the raw object, upcasted.
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case final Subclass2 value:
|
||||||
|
/// return ...;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _ActiveCallout value) $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _ActiveCallout():
|
||||||
|
return $default(_that);case _:
|
||||||
|
throw StateError('Unexpected subclass');
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A variant of `map` that fallback to returning `null`.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return null;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _ActiveCallout value)? $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _ActiveCallout() when $default != null:
|
||||||
|
return $default(_that);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A variant of `when` that fallback to an `orElse` callback.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return orElse();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String title, String description, String? iconUrl, DateTime addedAt, CalloutData data)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _ActiveCallout() when $default != null:
|
||||||
|
return $default(_that.id,_that.title,_that.description,_that.iconUrl,_that.addedAt,_that.data);case _:
|
||||||
|
return orElse();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A `switch`-like method, using callbacks.
|
||||||
|
///
|
||||||
|
/// As opposed to `map`, this offers destructuring.
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case Subclass2(:final field2):
|
||||||
|
/// return ...;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String title, String description, String? iconUrl, DateTime addedAt, CalloutData data) $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _ActiveCallout():
|
||||||
|
return $default(_that.id,_that.title,_that.description,_that.iconUrl,_that.addedAt,_that.data);case _:
|
||||||
|
throw StateError('Unexpected subclass');
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A variant of `when` that fallback to returning `null`
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return null;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String title, String description, String? iconUrl, DateTime addedAt, CalloutData data)? $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _ActiveCallout() when $default != null:
|
||||||
|
return $default(_that.id,_that.title,_that.description,_that.iconUrl,_that.addedAt,_that.data);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
|
||||||
|
|
||||||
|
class _ActiveCallout implements ActiveCallout {
|
||||||
|
const _ActiveCallout({required this.id, required this.title, required this.description, this.iconUrl, required this.addedAt, required this.data});
|
||||||
|
|
||||||
|
|
||||||
|
@override final String id;
|
||||||
|
@override final String title;
|
||||||
|
@override final String description;
|
||||||
|
@override final String? iconUrl;
|
||||||
|
@override final DateTime addedAt;
|
||||||
|
@override final CalloutData data;
|
||||||
|
|
||||||
|
/// Create a copy of ActiveCallout
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$ActiveCalloutCopyWith<_ActiveCallout> get copyWith => __$ActiveCalloutCopyWithImpl<_ActiveCallout>(this, _$identity);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ActiveCallout&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.iconUrl, iconUrl) || other.iconUrl == iconUrl)&&(identical(other.addedAt, addedAt) || other.addedAt == addedAt)&&(identical(other.data, data) || other.data == data));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,id,title,description,iconUrl,addedAt,data);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'ActiveCallout(id: $id, title: $title, description: $description, iconUrl: $iconUrl, addedAt: $addedAt, data: $data)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class _$ActiveCalloutCopyWith<$Res> implements $ActiveCalloutCopyWith<$Res> {
|
||||||
|
factory _$ActiveCalloutCopyWith(_ActiveCallout value, $Res Function(_ActiveCallout) _then) = __$ActiveCalloutCopyWithImpl;
|
||||||
|
@override @useResult
|
||||||
|
$Res call({
|
||||||
|
String id, String title, String description, String? iconUrl, DateTime addedAt, CalloutData data
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@override $CalloutDataCopyWith<$Res> get data;
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class __$ActiveCalloutCopyWithImpl<$Res>
|
||||||
|
implements _$ActiveCalloutCopyWith<$Res> {
|
||||||
|
__$ActiveCalloutCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final _ActiveCallout _self;
|
||||||
|
final $Res Function(_ActiveCallout) _then;
|
||||||
|
|
||||||
|
/// Create a copy of ActiveCallout
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? title = null,Object? description = null,Object? iconUrl = freezed,Object? addedAt = null,Object? data = null,}) {
|
||||||
|
return _then(_ActiveCallout(
|
||||||
|
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,description: null == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,iconUrl: freezed == iconUrl ? _self.iconUrl : iconUrl // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String?,addedAt: null == addedAt ? _self.addedAt : addedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,data: null == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
|
||||||
|
as CalloutData,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a copy of ActiveCallout
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$CalloutDataCopyWith<$Res> get data {
|
||||||
|
|
||||||
|
return $CalloutDataCopyWith<$Res>(_self.data, (value) {
|
||||||
|
return _then(_self.copyWith(data: value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// dart format on
|
||||||
25
useragent/lib/features/callouts/callout_event.dart
Normal file
25
useragent/lib/features/callouts/callout_event.dart
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import 'package:arbiter/proto/client.pb.dart';
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
import 'package:hooks_riverpod/experimental/mutation.dart';
|
||||||
|
|
||||||
|
part 'callout_event.freezed.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
sealed class CalloutData with _$CalloutData {
|
||||||
|
const factory CalloutData.connectApproval({
|
||||||
|
required String pubkey,
|
||||||
|
required ClientInfo clientInfo,
|
||||||
|
}) = ConnectApprovalData;
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
sealed class CalloutEvent with _$CalloutEvent {
|
||||||
|
const factory CalloutEvent.added({
|
||||||
|
required String id,
|
||||||
|
required CalloutData data,
|
||||||
|
}) = CalloutEventAdded;
|
||||||
|
|
||||||
|
const factory CalloutEvent.cancelled({
|
||||||
|
required String id,
|
||||||
|
}) = CalloutEventCancelled;
|
||||||
|
}
|
||||||
602
useragent/lib/features/callouts/callout_event.freezed.dart
Normal file
602
useragent/lib/features/callouts/callout_event.freezed.dart
Normal file
@@ -0,0 +1,602 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// coverage:ignore-file
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||||
|
|
||||||
|
part of 'callout_event.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// dart format off
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$CalloutData {
|
||||||
|
|
||||||
|
String get pubkey; ClientInfo get clientInfo;
|
||||||
|
/// Create a copy of CalloutData
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$CalloutDataCopyWith<CalloutData> get copyWith => _$CalloutDataCopyWithImpl<CalloutData>(this as CalloutData, _$identity);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is CalloutData&&(identical(other.pubkey, pubkey) || other.pubkey == pubkey)&&(identical(other.clientInfo, clientInfo) || other.clientInfo == clientInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,pubkey,clientInfo);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'CalloutData(pubkey: $pubkey, clientInfo: $clientInfo)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class $CalloutDataCopyWith<$Res> {
|
||||||
|
factory $CalloutDataCopyWith(CalloutData value, $Res Function(CalloutData) _then) = _$CalloutDataCopyWithImpl;
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
String pubkey, ClientInfo clientInfo
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class _$CalloutDataCopyWithImpl<$Res>
|
||||||
|
implements $CalloutDataCopyWith<$Res> {
|
||||||
|
_$CalloutDataCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final CalloutData _self;
|
||||||
|
final $Res Function(CalloutData) _then;
|
||||||
|
|
||||||
|
/// Create a copy of CalloutData
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline') @override $Res call({Object? pubkey = null,Object? clientInfo = null,}) {
|
||||||
|
return _then(_self.copyWith(
|
||||||
|
pubkey: null == pubkey ? _self.pubkey : pubkey // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,clientInfo: null == clientInfo ? _self.clientInfo : clientInfo // ignore: cast_nullable_to_non_nullable
|
||||||
|
as ClientInfo,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Adds pattern-matching-related methods to [CalloutData].
|
||||||
|
extension CalloutDataPatterns on CalloutData {
|
||||||
|
/// A variant of `map` that fallback to returning `orElse`.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return orElse();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult maybeMap<TResult extends Object?>({TResult Function( ConnectApprovalData value)? connectApproval,required TResult orElse(),}){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case ConnectApprovalData() when connectApproval != null:
|
||||||
|
return connectApproval(_that);case _:
|
||||||
|
return orElse();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A `switch`-like method, using callbacks.
|
||||||
|
///
|
||||||
|
/// Callbacks receives the raw object, upcasted.
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case final Subclass2 value:
|
||||||
|
/// return ...;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult map<TResult extends Object?>({required TResult Function( ConnectApprovalData value) connectApproval,}){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case ConnectApprovalData():
|
||||||
|
return connectApproval(_that);}
|
||||||
|
}
|
||||||
|
/// A variant of `map` that fallback to returning `null`.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return null;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>({TResult? Function( ConnectApprovalData value)? connectApproval,}){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case ConnectApprovalData() when connectApproval != null:
|
||||||
|
return connectApproval(_that);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A variant of `when` that fallback to an `orElse` callback.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return orElse();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>({TResult Function( String pubkey, ClientInfo clientInfo)? connectApproval,required TResult orElse(),}) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case ConnectApprovalData() when connectApproval != null:
|
||||||
|
return connectApproval(_that.pubkey,_that.clientInfo);case _:
|
||||||
|
return orElse();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A `switch`-like method, using callbacks.
|
||||||
|
///
|
||||||
|
/// As opposed to `map`, this offers destructuring.
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case Subclass2(:final field2):
|
||||||
|
/// return ...;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult when<TResult extends Object?>({required TResult Function( String pubkey, ClientInfo clientInfo) connectApproval,}) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case ConnectApprovalData():
|
||||||
|
return connectApproval(_that.pubkey,_that.clientInfo);}
|
||||||
|
}
|
||||||
|
/// A variant of `when` that fallback to returning `null`
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return null;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>({TResult? Function( String pubkey, ClientInfo clientInfo)? connectApproval,}) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case ConnectApprovalData() when connectApproval != null:
|
||||||
|
return connectApproval(_that.pubkey,_that.clientInfo);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectApprovalData implements CalloutData {
|
||||||
|
const ConnectApprovalData({required this.pubkey, required this.clientInfo});
|
||||||
|
|
||||||
|
|
||||||
|
@override final String pubkey;
|
||||||
|
@override final ClientInfo clientInfo;
|
||||||
|
|
||||||
|
/// Create a copy of CalloutData
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$ConnectApprovalDataCopyWith<ConnectApprovalData> get copyWith => _$ConnectApprovalDataCopyWithImpl<ConnectApprovalData>(this, _$identity);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is ConnectApprovalData&&(identical(other.pubkey, pubkey) || other.pubkey == pubkey)&&(identical(other.clientInfo, clientInfo) || other.clientInfo == clientInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,pubkey,clientInfo);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'CalloutData.connectApproval(pubkey: $pubkey, clientInfo: $clientInfo)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class $ConnectApprovalDataCopyWith<$Res> implements $CalloutDataCopyWith<$Res> {
|
||||||
|
factory $ConnectApprovalDataCopyWith(ConnectApprovalData value, $Res Function(ConnectApprovalData) _then) = _$ConnectApprovalDataCopyWithImpl;
|
||||||
|
@override @useResult
|
||||||
|
$Res call({
|
||||||
|
String pubkey, ClientInfo clientInfo
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class _$ConnectApprovalDataCopyWithImpl<$Res>
|
||||||
|
implements $ConnectApprovalDataCopyWith<$Res> {
|
||||||
|
_$ConnectApprovalDataCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final ConnectApprovalData _self;
|
||||||
|
final $Res Function(ConnectApprovalData) _then;
|
||||||
|
|
||||||
|
/// Create a copy of CalloutData
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @pragma('vm:prefer-inline') $Res call({Object? pubkey = null,Object? clientInfo = null,}) {
|
||||||
|
return _then(ConnectApprovalData(
|
||||||
|
pubkey: null == pubkey ? _self.pubkey : pubkey // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,clientInfo: null == clientInfo ? _self.clientInfo : clientInfo // ignore: cast_nullable_to_non_nullable
|
||||||
|
as ClientInfo,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$CalloutEvent {
|
||||||
|
|
||||||
|
String get id;
|
||||||
|
/// Create a copy of CalloutEvent
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$CalloutEventCopyWith<CalloutEvent> get copyWith => _$CalloutEventCopyWithImpl<CalloutEvent>(this as CalloutEvent, _$identity);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is CalloutEvent&&(identical(other.id, id) || other.id == id));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,id);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'CalloutEvent(id: $id)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class $CalloutEventCopyWith<$Res> {
|
||||||
|
factory $CalloutEventCopyWith(CalloutEvent value, $Res Function(CalloutEvent) _then) = _$CalloutEventCopyWithImpl;
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
String id
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class _$CalloutEventCopyWithImpl<$Res>
|
||||||
|
implements $CalloutEventCopyWith<$Res> {
|
||||||
|
_$CalloutEventCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final CalloutEvent _self;
|
||||||
|
final $Res Function(CalloutEvent) _then;
|
||||||
|
|
||||||
|
/// Create a copy of CalloutEvent
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,}) {
|
||||||
|
return _then(_self.copyWith(
|
||||||
|
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Adds pattern-matching-related methods to [CalloutEvent].
|
||||||
|
extension CalloutEventPatterns on CalloutEvent {
|
||||||
|
/// A variant of `map` that fallback to returning `orElse`.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return orElse();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult maybeMap<TResult extends Object?>({TResult Function( CalloutEventAdded value)? added,TResult Function( CalloutEventCancelled value)? cancelled,required TResult orElse(),}){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case CalloutEventAdded() when added != null:
|
||||||
|
return added(_that);case CalloutEventCancelled() when cancelled != null:
|
||||||
|
return cancelled(_that);case _:
|
||||||
|
return orElse();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A `switch`-like method, using callbacks.
|
||||||
|
///
|
||||||
|
/// Callbacks receives the raw object, upcasted.
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case final Subclass2 value:
|
||||||
|
/// return ...;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult map<TResult extends Object?>({required TResult Function( CalloutEventAdded value) added,required TResult Function( CalloutEventCancelled value) cancelled,}){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case CalloutEventAdded():
|
||||||
|
return added(_that);case CalloutEventCancelled():
|
||||||
|
return cancelled(_that);}
|
||||||
|
}
|
||||||
|
/// A variant of `map` that fallback to returning `null`.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case final Subclass value:
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return null;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>({TResult? Function( CalloutEventAdded value)? added,TResult? Function( CalloutEventCancelled value)? cancelled,}){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case CalloutEventAdded() when added != null:
|
||||||
|
return added(_that);case CalloutEventCancelled() when cancelled != null:
|
||||||
|
return cancelled(_that);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A variant of `when` that fallback to an `orElse` callback.
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return orElse();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>({TResult Function( String id, CalloutData data)? added,TResult Function( String id)? cancelled,required TResult orElse(),}) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case CalloutEventAdded() when added != null:
|
||||||
|
return added(_that.id,_that.data);case CalloutEventCancelled() when cancelled != null:
|
||||||
|
return cancelled(_that.id);case _:
|
||||||
|
return orElse();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// A `switch`-like method, using callbacks.
|
||||||
|
///
|
||||||
|
/// As opposed to `map`, this offers destructuring.
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case Subclass2(:final field2):
|
||||||
|
/// return ...;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult when<TResult extends Object?>({required TResult Function( String id, CalloutData data) added,required TResult Function( String id) cancelled,}) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case CalloutEventAdded():
|
||||||
|
return added(_that.id,_that.data);case CalloutEventCancelled():
|
||||||
|
return cancelled(_that.id);}
|
||||||
|
}
|
||||||
|
/// A variant of `when` that fallback to returning `null`
|
||||||
|
///
|
||||||
|
/// It is equivalent to doing:
|
||||||
|
/// ```dart
|
||||||
|
/// switch (sealedClass) {
|
||||||
|
/// case Subclass(:final field):
|
||||||
|
/// return ...;
|
||||||
|
/// case _:
|
||||||
|
/// return null;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
|
||||||
|
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>({TResult? Function( String id, CalloutData data)? added,TResult? Function( String id)? cancelled,}) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case CalloutEventAdded() when added != null:
|
||||||
|
return added(_that.id,_that.data);case CalloutEventCancelled() when cancelled != null:
|
||||||
|
return cancelled(_that.id);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
|
||||||
|
|
||||||
|
class CalloutEventAdded implements CalloutEvent {
|
||||||
|
const CalloutEventAdded({required this.id, required this.data});
|
||||||
|
|
||||||
|
|
||||||
|
@override final String id;
|
||||||
|
final CalloutData data;
|
||||||
|
|
||||||
|
/// Create a copy of CalloutEvent
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$CalloutEventAddedCopyWith<CalloutEventAdded> get copyWith => _$CalloutEventAddedCopyWithImpl<CalloutEventAdded>(this, _$identity);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is CalloutEventAdded&&(identical(other.id, id) || other.id == id)&&(identical(other.data, data) || other.data == data));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,id,data);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'CalloutEvent.added(id: $id, data: $data)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class $CalloutEventAddedCopyWith<$Res> implements $CalloutEventCopyWith<$Res> {
|
||||||
|
factory $CalloutEventAddedCopyWith(CalloutEventAdded value, $Res Function(CalloutEventAdded) _then) = _$CalloutEventAddedCopyWithImpl;
|
||||||
|
@override @useResult
|
||||||
|
$Res call({
|
||||||
|
String id, CalloutData data
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
$CalloutDataCopyWith<$Res> get data;
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class _$CalloutEventAddedCopyWithImpl<$Res>
|
||||||
|
implements $CalloutEventAddedCopyWith<$Res> {
|
||||||
|
_$CalloutEventAddedCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final CalloutEventAdded _self;
|
||||||
|
final $Res Function(CalloutEventAdded) _then;
|
||||||
|
|
||||||
|
/// Create a copy of CalloutEvent
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? data = null,}) {
|
||||||
|
return _then(CalloutEventAdded(
|
||||||
|
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,data: null == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
|
||||||
|
as CalloutData,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a copy of CalloutEvent
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$CalloutDataCopyWith<$Res> get data {
|
||||||
|
|
||||||
|
return $CalloutDataCopyWith<$Res>(_self.data, (value) {
|
||||||
|
return _then(_self.copyWith(data: value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
|
||||||
|
|
||||||
|
class CalloutEventCancelled implements CalloutEvent {
|
||||||
|
const CalloutEventCancelled({required this.id});
|
||||||
|
|
||||||
|
|
||||||
|
@override final String id;
|
||||||
|
|
||||||
|
/// Create a copy of CalloutEvent
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$CalloutEventCancelledCopyWith<CalloutEventCancelled> get copyWith => _$CalloutEventCancelledCopyWithImpl<CalloutEventCancelled>(this, _$identity);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is CalloutEventCancelled&&(identical(other.id, id) || other.id == id));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,id);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'CalloutEvent.cancelled(id: $id)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class $CalloutEventCancelledCopyWith<$Res> implements $CalloutEventCopyWith<$Res> {
|
||||||
|
factory $CalloutEventCancelledCopyWith(CalloutEventCancelled value, $Res Function(CalloutEventCancelled) _then) = _$CalloutEventCancelledCopyWithImpl;
|
||||||
|
@override @useResult
|
||||||
|
$Res call({
|
||||||
|
String id
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class _$CalloutEventCancelledCopyWithImpl<$Res>
|
||||||
|
implements $CalloutEventCancelledCopyWith<$Res> {
|
||||||
|
_$CalloutEventCancelledCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final CalloutEventCancelled _self;
|
||||||
|
final $Res Function(CalloutEventCancelled) _then;
|
||||||
|
|
||||||
|
/// Create a copy of CalloutEvent
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,}) {
|
||||||
|
return _then(CalloutEventCancelled(
|
||||||
|
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// dart format on
|
||||||
57
useragent/lib/features/callouts/callout_manager.dart
Normal file
57
useragent/lib/features/callouts/callout_manager.dart
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import 'package:arbiter/features/callouts/active_callout.dart';
|
||||||
|
import 'package:arbiter/features/callouts/callout_event.dart';
|
||||||
|
import 'package:arbiter/features/callouts/types/sdk_connect_approve.dart'
|
||||||
|
as connect_approve;
|
||||||
|
import 'package:arbiter/proto/client.pb.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
part 'callout_manager.g.dart';
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
class CalloutManager extends _$CalloutManager {
|
||||||
|
@override
|
||||||
|
Map<String, ActiveCallout> build() {
|
||||||
|
ref.listen(connect_approve.connectApproveEventsProvider, (_, next) {
|
||||||
|
next.whenData(_processEvent);
|
||||||
|
});
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void _processEvent(CalloutEvent event) {
|
||||||
|
switch (event) {
|
||||||
|
case CalloutEventAdded(:final id, :final data):
|
||||||
|
state = {...state, id: _toActiveCallout(id, data)};
|
||||||
|
case CalloutEventCancelled(:final id):
|
||||||
|
state = {...state}..remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> sendDecision(String id, bool approved) async {
|
||||||
|
final callout = state[id];
|
||||||
|
if (callout == null) return;
|
||||||
|
switch (callout.data) {
|
||||||
|
case ConnectApprovalData(:final pubkey):
|
||||||
|
await connect_approve.sendDecision(ref, pubkey, approved);
|
||||||
|
}
|
||||||
|
dismiss(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void dismiss(String id) {
|
||||||
|
state = {...state}..remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ActiveCallout _toActiveCallout(String id, CalloutData data) => switch (data) {
|
||||||
|
ConnectApprovalData(:final clientInfo) => ActiveCallout(
|
||||||
|
id: id,
|
||||||
|
title: 'Connection Request',
|
||||||
|
description: _clientDisplayName(clientInfo) != null
|
||||||
|
? '${_clientDisplayName(clientInfo)} is requesting a connection.'
|
||||||
|
: 'An SDK client is requesting a connection.',
|
||||||
|
addedAt: DateTime.now(),
|
||||||
|
data: data,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
String? _clientDisplayName(ClientInfo info) =>
|
||||||
|
info.hasName() && info.name.isNotEmpty ? info.name : null;
|
||||||
67
useragent/lib/features/callouts/callout_manager.g.dart
Normal file
67
useragent/lib/features/callouts/callout_manager.g.dart
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'callout_manager.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint, type=warning
|
||||||
|
|
||||||
|
@ProviderFor(CalloutManager)
|
||||||
|
final calloutManagerProvider = CalloutManagerProvider._();
|
||||||
|
|
||||||
|
final class CalloutManagerProvider
|
||||||
|
extends $NotifierProvider<CalloutManager, Map<String, ActiveCallout>> {
|
||||||
|
CalloutManagerProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'calloutManagerProvider',
|
||||||
|
isAutoDispose: false,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$calloutManagerHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
CalloutManager create() => CalloutManager();
|
||||||
|
|
||||||
|
/// {@macro riverpod.override_with_value}
|
||||||
|
Override overrideWithValue(Map<String, ActiveCallout> value) {
|
||||||
|
return $ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
providerOverride: $SyncValueProvider<Map<String, ActiveCallout>>(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$calloutManagerHash() => r'ff8c9a03a6bbbca822242eb497c503b18240a289';
|
||||||
|
|
||||||
|
abstract class _$CalloutManager extends $Notifier<Map<String, ActiveCallout>> {
|
||||||
|
Map<String, ActiveCallout> build();
|
||||||
|
@$mustCallSuper
|
||||||
|
@override
|
||||||
|
void runBuild() {
|
||||||
|
final ref =
|
||||||
|
this.ref
|
||||||
|
as $Ref<Map<String, ActiveCallout>, Map<String, ActiveCallout>>;
|
||||||
|
final element =
|
||||||
|
ref.element
|
||||||
|
as $ClassProviderElement<
|
||||||
|
AnyNotifier<
|
||||||
|
Map<String, ActiveCallout>,
|
||||||
|
Map<String, ActiveCallout>
|
||||||
|
>,
|
||||||
|
Map<String, ActiveCallout>,
|
||||||
|
Object?,
|
||||||
|
Object?
|
||||||
|
>;
|
||||||
|
element.handleCreate(ref, build);
|
||||||
|
}
|
||||||
|
}
|
||||||
99
useragent/lib/features/callouts/show_callout.dart
Normal file
99
useragent/lib/features/callouts/show_callout.dart
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import 'package:arbiter/features/callouts/callout_event.dart';
|
||||||
|
import 'package:arbiter/features/callouts/callout_manager.dart';
|
||||||
|
import 'package:arbiter/screens/callouts/sdk_connect.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
|
Future<void> showCallout(BuildContext context, WidgetRef ref, String id) async {
|
||||||
|
final data = ref.read(calloutManagerProvider)[id]?.data;
|
||||||
|
if (data == null) return;
|
||||||
|
|
||||||
|
await showGeneralDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
|
||||||
|
barrierColor: Colors.transparent,
|
||||||
|
transitionDuration: const Duration(milliseconds: 320),
|
||||||
|
pageBuilder: (_, animation, _) => _CalloutOverlay(
|
||||||
|
id: id,
|
||||||
|
data: data,
|
||||||
|
animation: animation,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CalloutOverlay extends ConsumerWidget {
|
||||||
|
const _CalloutOverlay({
|
||||||
|
required this.id,
|
||||||
|
required this.data,
|
||||||
|
required this.animation,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final CalloutData data;
|
||||||
|
final Animation<double> animation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
ref.listen(
|
||||||
|
calloutManagerProvider.select((map) => map.containsKey(id)),
|
||||||
|
(wasPresent, isPresent) {
|
||||||
|
if (wasPresent == true && !isPresent && context.mounted) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
final content = switch (data) {
|
||||||
|
ConnectApprovalData(:final pubkey, :final clientInfo) => SdkConnectCallout(
|
||||||
|
pubkey: pubkey,
|
||||||
|
clientInfo: clientInfo,
|
||||||
|
onAccept: () => ref.read(calloutManagerProvider.notifier).sendDecision(id, true),
|
||||||
|
onDecline: () => ref.read(calloutManagerProvider.notifier).sendDecision(id, false),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
final barrierAnim = CurvedAnimation(
|
||||||
|
parent: animation,
|
||||||
|
curve: const Interval(0, 0.3125, curve: Curves.easeOut),
|
||||||
|
);
|
||||||
|
final popupAnim = CurvedAnimation(
|
||||||
|
parent: animation,
|
||||||
|
curve: const Interval(0.3125, 1, curve: Curves.easeOutCubic),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Material(
|
||||||
|
type: MaterialType.transparency,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Positioned.fill(
|
||||||
|
child: AnimatedBuilder(
|
||||||
|
animation: barrierAnim,
|
||||||
|
builder: (_, __) => ColoredBox(
|
||||||
|
color: Colors.black.withValues(alpha: 0.35 * barrierAnim.value),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SafeArea(
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: FadeTransition(
|
||||||
|
opacity: popupAnim,
|
||||||
|
child: SlideTransition(
|
||||||
|
position: Tween<Offset>(
|
||||||
|
begin: const Offset(0, 0.08),
|
||||||
|
end: Offset.zero,
|
||||||
|
).animate(popupAnim),
|
||||||
|
child: content,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
218
useragent/lib/features/callouts/show_callout_list.dart
Normal file
218
useragent/lib/features/callouts/show_callout_list.dart
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
import 'package:arbiter/features/callouts/active_callout.dart';
|
||||||
|
import 'package:arbiter/features/callouts/callout_manager.dart';
|
||||||
|
import 'package:arbiter/features/callouts/show_callout.dart';
|
||||||
|
import 'package:arbiter/theme/palette.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:sizer/sizer.dart';
|
||||||
|
import 'package:timeago/timeago.dart' as timeago;
|
||||||
|
|
||||||
|
Future<void> showCalloutList(BuildContext context, WidgetRef ref) async {
|
||||||
|
final selectedId = await showGeneralDialog<String>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: true,
|
||||||
|
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
|
||||||
|
barrierColor: Colors.transparent,
|
||||||
|
transitionDuration: const Duration(milliseconds: 280),
|
||||||
|
pageBuilder: (_, animation, __) => _CalloutListOverlay(animation: animation),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (selectedId != null && context.mounted) {
|
||||||
|
await showCallout(context, ref, selectedId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CalloutListOverlay extends ConsumerWidget {
|
||||||
|
const _CalloutListOverlay({required this.animation});
|
||||||
|
|
||||||
|
final Animation<double> animation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final callouts = ref.watch(calloutManagerProvider);
|
||||||
|
|
||||||
|
final barrierAnim = CurvedAnimation(
|
||||||
|
parent: animation,
|
||||||
|
curve: const Interval(0, 0.3, curve: Curves.easeOut),
|
||||||
|
);
|
||||||
|
final panelAnim = CurvedAnimation(
|
||||||
|
parent: animation,
|
||||||
|
curve: const Interval(0.3, 1, curve: Curves.easeOutCubic),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Material(
|
||||||
|
type: MaterialType.transparency,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Positioned.fill(
|
||||||
|
child: GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onTap: () => Navigator.of(context).pop(),
|
||||||
|
child: AnimatedBuilder(
|
||||||
|
animation: barrierAnim,
|
||||||
|
builder: (_, __) => ColoredBox(
|
||||||
|
color: Colors.black.withValues(alpha: 0.35 * barrierAnim.value),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SafeArea(
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(1.6.h),
|
||||||
|
child: FadeTransition(
|
||||||
|
opacity: panelAnim,
|
||||||
|
child: SlideTransition(
|
||||||
|
position: Tween<Offset>(
|
||||||
|
begin: const Offset(0, 0.08),
|
||||||
|
end: Offset.zero,
|
||||||
|
).animate(panelAnim),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () {},
|
||||||
|
child: _CalloutListPanel(callouts: callouts),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CalloutListPanel extends StatelessWidget {
|
||||||
|
const _CalloutListPanel({required this.callouts});
|
||||||
|
|
||||||
|
final Map<String, ActiveCallout> callouts;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
constraints: BoxConstraints(maxHeight: 48.h),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Palette.cream,
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
border: Border.all(color: Palette.line),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(2.h, 2.h, 2.h, 1.2.h),
|
||||||
|
child: Text(
|
||||||
|
'Notifications',
|
||||||
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
|
color: Palette.ink,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (callouts.isEmpty)
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(2.h, 0, 2.h, 2.h),
|
||||||
|
child: Text(
|
||||||
|
'No pending notifications.',
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: Palette.ink.withValues(alpha: 0.50),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Flexible(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: EdgeInsets.fromLTRB(1.2.h, 0, 1.2.h, 1.2.h),
|
||||||
|
child: Column(
|
||||||
|
spacing: 0.5.h,
|
||||||
|
children: [
|
||||||
|
for (final entry in callouts.values)
|
||||||
|
_CalloutListEntry(
|
||||||
|
callout: entry,
|
||||||
|
onTap: () => Navigator.of(context).pop(entry.id),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CalloutListEntry extends StatelessWidget {
|
||||||
|
const _CalloutListEntry({required this.callout, required this.onTap});
|
||||||
|
|
||||||
|
final ActiveCallout callout;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 1.2.h, vertical: 1.2.h),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(color: Palette.line),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
spacing: 1.2.h,
|
||||||
|
children: [
|
||||||
|
if (callout.iconUrl != null)
|
||||||
|
CircleAvatar(
|
||||||
|
radius: 2.2.h,
|
||||||
|
backgroundColor: Palette.line,
|
||||||
|
backgroundImage: NetworkImage(callout.iconUrl!),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
spacing: 0.3.h,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
callout.title,
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: Palette.ink,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
timeago.format(callout.addedAt),
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: Palette.ink.withValues(alpha: 0.45),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
callout.description,
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: Palette.ink.withValues(alpha: 0.65),
|
||||||
|
height: 1.4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:arbiter/features/callouts/callout_event.dart';
|
||||||
|
import 'package:arbiter/proto/user_agent.pb.dart';
|
||||||
|
import 'package:arbiter/providers/connection/connection_manager.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
part 'sdk_connect_approve.g.dart';
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
Stream<CalloutEvent> connectApproveEvents(Ref ref) async* {
|
||||||
|
final connection = await ref.watch(connectionManagerProvider.future);
|
||||||
|
if (connection == null) return;
|
||||||
|
|
||||||
|
await for (final message in connection.outOfBandMessages) {
|
||||||
|
switch (message.whichPayload()) {
|
||||||
|
case UserAgentResponse_Payload.sdkClientConnectionRequest:
|
||||||
|
final body = message.sdkClientConnectionRequest;
|
||||||
|
final id = base64Encode(body.pubkey);
|
||||||
|
yield CalloutEvent.added(
|
||||||
|
id: 'connect_approve:$id',
|
||||||
|
data: CalloutData.connectApproval(
|
||||||
|
pubkey: id,
|
||||||
|
clientInfo: body.info,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
case UserAgentResponse_Payload.sdkClientConnectionCancel:
|
||||||
|
final id = base64Encode(message.sdkClientConnectionCancel.pubkey);
|
||||||
|
yield CalloutEvent.cancelled(id: 'connect_approve:$id');
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> sendDecision(Ref ref, String pubkey, bool approved) async {
|
||||||
|
final connection = await ref.watch(connectionManagerProvider.future);
|
||||||
|
if (connection == null) return;
|
||||||
|
|
||||||
|
final bytes = base64Decode(pubkey);
|
||||||
|
|
||||||
|
final req = UserAgentRequest(sdkClientConnectionResponse: SdkClientConnectionResponse(
|
||||||
|
approved: approved,
|
||||||
|
pubkey: bytes
|
||||||
|
));
|
||||||
|
|
||||||
|
await connection.tell(req);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'sdk_connect_approve.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint, type=warning
|
||||||
|
|
||||||
|
@ProviderFor(connectApproveEvents)
|
||||||
|
final connectApproveEventsProvider = ConnectApproveEventsProvider._();
|
||||||
|
|
||||||
|
final class ConnectApproveEventsProvider
|
||||||
|
extends
|
||||||
|
$FunctionalProvider<
|
||||||
|
AsyncValue<CalloutEvent>,
|
||||||
|
CalloutEvent,
|
||||||
|
Stream<CalloutEvent>
|
||||||
|
>
|
||||||
|
with $FutureModifier<CalloutEvent>, $StreamProvider<CalloutEvent> {
|
||||||
|
ConnectApproveEventsProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'connectApproveEventsProvider',
|
||||||
|
isAutoDispose: true,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$connectApproveEventsHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
$StreamProviderElement<CalloutEvent> $createElement(
|
||||||
|
$ProviderPointer pointer,
|
||||||
|
) => $StreamProviderElement(pointer);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<CalloutEvent> create(Ref ref) {
|
||||||
|
return connectApproveEvents(ref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$connectApproveEventsHash() =>
|
||||||
|
r'6a0998288afc0836a7c1701a983f64c33d318fd6';
|
||||||
@@ -9,13 +9,49 @@ import 'package:arbiter/proto/user_agent.pb.dart';
|
|||||||
import 'package:grpc/grpc.dart';
|
import 'package:grpc/grpc.dart';
|
||||||
import 'package:mtcore/markettakers.dart';
|
import 'package:mtcore/markettakers.dart';
|
||||||
|
|
||||||
|
class AuthorizationException implements Exception {
|
||||||
|
const AuthorizationException(this.result);
|
||||||
|
|
||||||
|
final AuthResult result;
|
||||||
|
|
||||||
|
String get message => switch (result) {
|
||||||
|
AuthResult.AUTH_RESULT_INVALID_KEY =>
|
||||||
|
'Authentication failed: this device key is not registered on the server.',
|
||||||
|
AuthResult.AUTH_RESULT_INVALID_SIGNATURE =>
|
||||||
|
'Authentication failed: the server rejected the signature for this device key.',
|
||||||
|
AuthResult.AUTH_RESULT_BOOTSTRAP_REQUIRED =>
|
||||||
|
'Authentication failed: the server requires bootstrap before this device can connect.',
|
||||||
|
AuthResult.AUTH_RESULT_TOKEN_INVALID =>
|
||||||
|
'Authentication failed: the bootstrap token is invalid.',
|
||||||
|
AuthResult.AUTH_RESULT_INTERNAL =>
|
||||||
|
'Authentication failed: the server hit an internal error.',
|
||||||
|
AuthResult.AUTH_RESULT_UNSPECIFIED =>
|
||||||
|
'Authentication failed: the server returned an unspecified auth error.',
|
||||||
|
AuthResult.AUTH_RESULT_SUCCESS => 'Authentication succeeded.',
|
||||||
|
_ => 'Authentication failed: ${result.name}.',
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => message;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConnectionException implements Exception {
|
||||||
|
const ConnectionException(this.message);
|
||||||
|
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => message;
|
||||||
|
}
|
||||||
|
|
||||||
Future<Connection> connectAndAuthorize(
|
Future<Connection> connectAndAuthorize(
|
||||||
StoredServerInfo serverInfo,
|
StoredServerInfo serverInfo,
|
||||||
KeyHandle key, {
|
KeyHandle key, {
|
||||||
String? bootstrapToken,
|
String? bootstrapToken,
|
||||||
}) async {
|
}) async {
|
||||||
|
Connection? connection;
|
||||||
try {
|
try {
|
||||||
final connection = await _connect(serverInfo);
|
connection = await _connect(serverInfo);
|
||||||
talker.info(
|
talker.info(
|
||||||
'Connected to server at ${serverInfo.address}:${serverInfo.port}',
|
'Connected to server at ${serverInfo.address}:${serverInfo.port}',
|
||||||
);
|
);
|
||||||
@@ -30,21 +66,24 @@ Future<Connection> connectAndAuthorize(
|
|||||||
KeyAlgorithm.ed25519 => KeyType.KEY_TYPE_ED25519,
|
KeyAlgorithm.ed25519 => KeyType.KEY_TYPE_ED25519,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
await connection.send(UserAgentRequest(authChallengeRequest: req));
|
final response = await connection.ask(
|
||||||
|
UserAgentRequest(authChallengeRequest: req),
|
||||||
|
);
|
||||||
talker.info(
|
talker.info(
|
||||||
"Sent auth challenge request with pubkey ${base64Encode(pubkey)}",
|
"Sent auth challenge request with pubkey ${base64Encode(pubkey)}",
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await connection.receive();
|
|
||||||
talker.info('Received response from server, checking auth flow...');
|
talker.info('Received response from server, checking auth flow...');
|
||||||
|
|
||||||
if (response.hasAuthOk()) {
|
if (response.hasAuthResult()) {
|
||||||
|
if (response.authResult != AuthResult.AUTH_RESULT_SUCCESS) {
|
||||||
|
throw AuthorizationException(response.authResult);
|
||||||
|
}
|
||||||
talker.info('Authentication successful, connection established');
|
talker.info('Authentication successful, connection established');
|
||||||
return connection;
|
return connection;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!response.hasAuthChallenge()) {
|
if (!response.hasAuthChallenge()) {
|
||||||
throw Exception(
|
throw ConnectionException(
|
||||||
'Expected AuthChallengeResponse, got ${response.whichPayload()}',
|
'Expected AuthChallengeResponse, got ${response.whichPayload()}',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -55,23 +94,35 @@ Future<Connection> connectAndAuthorize(
|
|||||||
);
|
);
|
||||||
|
|
||||||
final signature = await key.sign(challenge);
|
final signature = await key.sign(challenge);
|
||||||
await connection.send(
|
final solutionResponse = await connection.ask(
|
||||||
UserAgentRequest(authChallengeSolution: AuthChallengeSolution(signature: signature)),
|
UserAgentRequest(authChallengeSolution: AuthChallengeSolution(signature: signature)),
|
||||||
);
|
);
|
||||||
|
|
||||||
talker.info('Sent auth challenge solution, waiting for server response...');
|
talker.info('Sent auth challenge solution, waiting for server response...');
|
||||||
|
|
||||||
final solutionResponse = await connection.receive();
|
if (!solutionResponse.hasAuthResult()) {
|
||||||
if (!solutionResponse.hasAuthOk()) {
|
throw ConnectionException(
|
||||||
throw Exception(
|
|
||||||
'Expected AuthChallengeSolutionResponse, got ${solutionResponse.whichPayload()}',
|
'Expected AuthChallengeSolutionResponse, got ${solutionResponse.whichPayload()}',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (solutionResponse.authResult != AuthResult.AUTH_RESULT_SUCCESS) {
|
||||||
|
throw AuthorizationException(solutionResponse.authResult);
|
||||||
|
}
|
||||||
|
|
||||||
talker.info('Authentication successful, connection established');
|
talker.info('Authentication successful, connection established');
|
||||||
return connection;
|
return connection;
|
||||||
|
} on AuthorizationException {
|
||||||
|
await connection?.close();
|
||||||
|
rethrow;
|
||||||
|
} on GrpcError catch (error) {
|
||||||
|
await connection?.close();
|
||||||
|
throw ConnectionException('Failed to connect to server: ${error.message}');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw Exception('Failed to connect to server: $e');
|
await connection?.close();
|
||||||
|
if (e is ConnectionException) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
throw ConnectionException('Failed to connect to server: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,33 +5,130 @@ import 'package:grpc/grpc.dart';
|
|||||||
import 'package:mtcore/markettakers.dart';
|
import 'package:mtcore/markettakers.dart';
|
||||||
|
|
||||||
class Connection {
|
class Connection {
|
||||||
final ClientChannel channel;
|
|
||||||
final StreamController<UserAgentRequest> _tx;
|
|
||||||
final StreamIterator<UserAgentResponse> _rx;
|
|
||||||
|
|
||||||
Connection({
|
Connection({
|
||||||
required this.channel,
|
required this.channel,
|
||||||
required StreamController<UserAgentRequest> tx,
|
required StreamController<UserAgentRequest> tx,
|
||||||
required ResponseStream<UserAgentResponse> rx,
|
required ResponseStream<UserAgentResponse> rx,
|
||||||
}) : _tx = tx,
|
}) : _tx = tx {
|
||||||
_rx = StreamIterator(rx);
|
_rxSubscription = rx.listen(
|
||||||
|
_handleResponse,
|
||||||
Future<void> send(UserAgentRequest request) async {
|
onError: _handleError,
|
||||||
talker.debug('Sending request: ${request.toDebugString()}');
|
onDone: _handleDone,
|
||||||
_tx.add(request);
|
cancelOnError: true,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<UserAgentResponse> receive() async {
|
final ClientChannel channel;
|
||||||
final hasValue = await _rx.moveNext();
|
final StreamController<UserAgentRequest> _tx;
|
||||||
if (!hasValue) {
|
final Map<int, Completer<UserAgentResponse>> _pendingRequests = {};
|
||||||
throw Exception('Connection closed while waiting for server response.');
|
final StreamController<UserAgentResponse> _outOfBandMessages =
|
||||||
|
StreamController<UserAgentResponse>.broadcast();
|
||||||
|
|
||||||
|
StreamSubscription<UserAgentResponse>? _rxSubscription;
|
||||||
|
int _nextRequestId = 0;
|
||||||
|
|
||||||
|
Stream<UserAgentResponse> get outOfBandMessages => _outOfBandMessages.stream;
|
||||||
|
|
||||||
|
Future<UserAgentResponse> ask(UserAgentRequest message) async {
|
||||||
|
_ensureOpen();
|
||||||
|
|
||||||
|
final requestId = _nextRequestId++;
|
||||||
|
final completer = Completer<UserAgentResponse>();
|
||||||
|
_pendingRequests[requestId] = completer;
|
||||||
|
|
||||||
|
message.id = requestId;
|
||||||
|
talker.debug('Sending request: ${message.toDebugString()}');
|
||||||
|
|
||||||
|
try {
|
||||||
|
_tx.add(message);
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
_pendingRequests.remove(requestId);
|
||||||
|
completer.completeError(error, stackTrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> tell(UserAgentRequest message) async {
|
||||||
|
_ensureOpen();
|
||||||
|
|
||||||
|
final requestId = _nextRequestId++;
|
||||||
|
message.id = requestId;
|
||||||
|
|
||||||
|
talker.debug('Sending message: ${message.toDebugString()}');
|
||||||
|
|
||||||
|
try {
|
||||||
|
_tx.add(message);
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
talker.error('Failed to send message: $error', error, stackTrace);
|
||||||
}
|
}
|
||||||
talker.debug('Received response: ${_rx.current.toDebugString()}');
|
|
||||||
return _rx.current;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> close() async {
|
Future<void> close() async {
|
||||||
|
talker.debug('Closing connection...');
|
||||||
|
final rxSubscription = _rxSubscription;
|
||||||
|
if (rxSubscription == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_rxSubscription = null;
|
||||||
|
await rxSubscription.cancel();
|
||||||
|
_failPendingRequests(Exception('Connection closed.'));
|
||||||
|
await _outOfBandMessages.close();
|
||||||
await _tx.close();
|
await _tx.close();
|
||||||
await channel.shutdown();
|
await channel.shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handleResponse(UserAgentResponse response) {
|
||||||
|
talker.debug('Received response: ${response.toDebugString()}');
|
||||||
|
|
||||||
|
if (response.hasId()) {
|
||||||
|
final completer = _pendingRequests.remove(response.id);
|
||||||
|
if (completer == null) {
|
||||||
|
talker.warning('Received response for unknown request id ${response.id}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
completer.complete(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_outOfBandMessages.add(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleError(Object error, StackTrace stackTrace) {
|
||||||
|
_rxSubscription = null;
|
||||||
|
_failPendingRequests(error, stackTrace);
|
||||||
|
_outOfBandMessages.addError(error, stackTrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleDone() {
|
||||||
|
talker.debug('Connection closed by server.');
|
||||||
|
if (_rxSubscription == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_rxSubscription = null;
|
||||||
|
final error = Exception(
|
||||||
|
'Connection closed while waiting for server response.',
|
||||||
|
);
|
||||||
|
_failPendingRequests(error);
|
||||||
|
_outOfBandMessages.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _failPendingRequests(Object error, [StackTrace? stackTrace]) {
|
||||||
|
final pendingRequests = _pendingRequests.values.toList(growable: false);
|
||||||
|
_pendingRequests.clear();
|
||||||
|
|
||||||
|
for (final completer in pendingRequests) {
|
||||||
|
if (!completer.isCompleted) {
|
||||||
|
completer.completeError(error, stackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _ensureOpen() {
|
||||||
|
if (_rxSubscription == null) {
|
||||||
|
throw StateError('Connection is closed.');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import 'package:arbiter/proto/user_agent.pb.dart';
|
|||||||
import 'package:protobuf/well_known_types/google/protobuf/empty.pb.dart';
|
import 'package:protobuf/well_known_types/google/protobuf/empty.pb.dart';
|
||||||
|
|
||||||
Future<List<WalletEntry>> listEvmWallets(Connection connection) async {
|
Future<List<WalletEntry>> listEvmWallets(Connection connection) async {
|
||||||
await connection.send(UserAgentRequest(evmWalletList: Empty()));
|
final response = await connection.ask(
|
||||||
|
UserAgentRequest(evmWalletList: Empty()),
|
||||||
final response = await connection.receive();
|
);
|
||||||
if (!response.hasEvmWalletList()) {
|
if (!response.hasEvmWalletList()) {
|
||||||
throw Exception(
|
throw Exception(
|
||||||
'Expected EVM wallet list response, got ${response.whichPayload()}',
|
'Expected EVM wallet list response, got ${response.whichPayload()}',
|
||||||
@@ -25,9 +25,9 @@ Future<List<WalletEntry>> listEvmWallets(Connection connection) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> createEvmWallet(Connection connection) async {
|
Future<void> createEvmWallet(Connection connection) async {
|
||||||
await connection.send(UserAgentRequest(evmWalletCreate: Empty()));
|
final response = await connection.ask(
|
||||||
|
UserAgentRequest(evmWalletCreate: Empty()),
|
||||||
final response = await connection.receive();
|
);
|
||||||
if (!response.hasEvmWalletCreate()) {
|
if (!response.hasEvmWalletCreate()) {
|
||||||
throw Exception(
|
throw Exception(
|
||||||
'Expected EVM wallet create response, got ${response.whichPayload()}',
|
'Expected EVM wallet create response, got ${response.whichPayload()}',
|
||||||
|
|||||||
@@ -4,18 +4,12 @@ import 'package:arbiter/proto/user_agent.pb.dart';
|
|||||||
import 'package:fixnum/fixnum.dart';
|
import 'package:fixnum/fixnum.dart';
|
||||||
import 'package:protobuf/well_known_types/google/protobuf/timestamp.pb.dart';
|
import 'package:protobuf/well_known_types/google/protobuf/timestamp.pb.dart';
|
||||||
|
|
||||||
Future<List<GrantEntry>> listEvmGrants(
|
Future<List<GrantEntry>> listEvmGrants(Connection connection) async {
|
||||||
Connection connection, {
|
|
||||||
int? walletId,
|
|
||||||
}) async {
|
|
||||||
final request = EvmGrantListRequest();
|
final request = EvmGrantListRequest();
|
||||||
if (walletId != null) {
|
|
||||||
request.walletId = walletId;
|
|
||||||
}
|
|
||||||
|
|
||||||
await connection.send(UserAgentRequest(evmGrantList: request));
|
final response = await connection.ask(
|
||||||
|
UserAgentRequest(evmGrantList: request),
|
||||||
final response = await connection.receive();
|
);
|
||||||
if (!response.hasEvmGrantList()) {
|
if (!response.hasEvmGrantList()) {
|
||||||
throw Exception(
|
throw Exception(
|
||||||
'Expected EVM grant list response, got ${response.whichPayload()}',
|
'Expected EVM grant list response, got ${response.whichPayload()}',
|
||||||
@@ -35,58 +29,33 @@ Future<List<GrantEntry>> listEvmGrants(
|
|||||||
|
|
||||||
Future<int> createEvmGrant(
|
Future<int> createEvmGrant(
|
||||||
Connection connection, {
|
Connection connection, {
|
||||||
required int clientId,
|
required SharedSettings sharedSettings,
|
||||||
required int walletId,
|
|
||||||
required Int64 chainId,
|
|
||||||
DateTime? validFrom,
|
|
||||||
DateTime? validUntil,
|
|
||||||
List<int>? maxGasFeePerGas,
|
|
||||||
List<int>? maxPriorityFeePerGas,
|
|
||||||
TransactionRateLimit? rateLimit,
|
|
||||||
required SpecificGrant specific,
|
required SpecificGrant specific,
|
||||||
}) async {
|
}) async {
|
||||||
await connection.send(
|
final request = UserAgentRequest(
|
||||||
UserAgentRequest(
|
|
||||||
evmGrantCreate: EvmGrantCreateRequest(
|
evmGrantCreate: EvmGrantCreateRequest(
|
||||||
clientId: clientId,
|
shared: sharedSettings,
|
||||||
shared: SharedSettings(
|
|
||||||
walletId: walletId,
|
|
||||||
chainId: chainId,
|
|
||||||
validFrom: validFrom == null ? null : _toTimestamp(validFrom),
|
|
||||||
validUntil: validUntil == null ? null : _toTimestamp(validUntil),
|
|
||||||
maxGasFeePerGas: maxGasFeePerGas,
|
|
||||||
maxPriorityFeePerGas: maxPriorityFeePerGas,
|
|
||||||
rateLimit: rateLimit,
|
|
||||||
),
|
|
||||||
specific: specific,
|
specific: specific,
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await connection.receive();
|
final resp = await connection.ask(request);
|
||||||
if (!response.hasEvmGrantCreate()) {
|
|
||||||
|
if (!resp.hasEvmGrantCreate()) {
|
||||||
throw Exception(
|
throw Exception(
|
||||||
'Expected EVM grant create response, got ${response.whichPayload()}',
|
'Expected EVM grant create response, got ${resp.whichPayload()}',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final result = response.evmGrantCreate;
|
final result = resp.evmGrantCreate;
|
||||||
switch (result.whichResult()) {
|
|
||||||
case EvmGrantCreateResponse_Result.grantId:
|
|
||||||
return result.grantId;
|
return result.grantId;
|
||||||
case EvmGrantCreateResponse_Result.error:
|
|
||||||
throw Exception(_describeGrantError(result.error));
|
|
||||||
case EvmGrantCreateResponse_Result.notSet:
|
|
||||||
throw Exception('Grant creation returned no result.');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deleteEvmGrant(Connection connection, int grantId) async {
|
Future<void> deleteEvmGrant(Connection connection, int grantId) async {
|
||||||
await connection.send(
|
final response = await connection.ask(
|
||||||
UserAgentRequest(evmGrantDelete: EvmGrantDeleteRequest(grantId: grantId)),
|
UserAgentRequest(evmGrantDelete: EvmGrantDeleteRequest(grantId: grantId)),
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await connection.receive();
|
|
||||||
if (!response.hasEvmGrantDelete()) {
|
if (!response.hasEvmGrantDelete()) {
|
||||||
throw Exception(
|
throw Exception(
|
||||||
'Expected EVM grant delete response, got ${response.whichPayload()}',
|
'Expected EVM grant delete response, got ${response.whichPayload()}',
|
||||||
|
|||||||
72
useragent/lib/features/connection/evm/wallet_access.dart
Normal file
72
useragent/lib/features/connection/evm/wallet_access.dart
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import 'package:arbiter/features/connection/connection.dart';
|
||||||
|
import 'package:arbiter/proto/user_agent.pb.dart';
|
||||||
|
import 'package:protobuf/well_known_types/google/protobuf/empty.pb.dart';
|
||||||
|
|
||||||
|
Future<Set<int>> readClientWalletAccess(
|
||||||
|
Connection connection, {
|
||||||
|
required int clientId,
|
||||||
|
}) async {
|
||||||
|
final response = await connection.ask(
|
||||||
|
UserAgentRequest(listWalletAccess: Empty()),
|
||||||
|
);
|
||||||
|
if (!response.hasListWalletAccessResponse()) {
|
||||||
|
throw Exception(
|
||||||
|
'Expected list wallet access response, got ${response.whichPayload()}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
for (final entry in response.listWalletAccessResponse.accesses)
|
||||||
|
if (entry.access.sdkClientId == clientId) entry.access.walletId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<SdkClientWalletAccess>> listAllWalletAccesses(
|
||||||
|
Connection connection,
|
||||||
|
) async {
|
||||||
|
final response = await connection.ask(
|
||||||
|
UserAgentRequest(listWalletAccess: Empty()),
|
||||||
|
);
|
||||||
|
if (!response.hasListWalletAccessResponse()) {
|
||||||
|
throw Exception(
|
||||||
|
'Expected list wallet access response, got ${response.whichPayload()}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return response.listWalletAccessResponse.accesses.toList(growable: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> writeClientWalletAccess(
|
||||||
|
Connection connection, {
|
||||||
|
required int clientId,
|
||||||
|
required Set<int> walletIds,
|
||||||
|
}) async {
|
||||||
|
final current = await readClientWalletAccess(connection, clientId: clientId);
|
||||||
|
|
||||||
|
final toGrant = walletIds.difference(current);
|
||||||
|
final toRevoke = current.difference(walletIds);
|
||||||
|
|
||||||
|
if (toGrant.isNotEmpty) {
|
||||||
|
await connection.tell(
|
||||||
|
UserAgentRequest(
|
||||||
|
grantWalletAccess: SdkClientGrantWalletAccess(
|
||||||
|
accesses: [
|
||||||
|
for (final walletId in toGrant)
|
||||||
|
WalletAccess(sdkClientId: clientId, walletId: walletId),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toRevoke.isNotEmpty) {
|
||||||
|
await connection.tell(
|
||||||
|
UserAgentRequest(
|
||||||
|
revokeWalletAccess: SdkClientRevokeWalletAccess(
|
||||||
|
accesses: [
|
||||||
|
for (final walletId in toRevoke)
|
||||||
|
walletId
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ Future<BootstrapResult> bootstrapVault(
|
|||||||
) async {
|
) async {
|
||||||
final encryptedKey = await _encryptVaultKeyMaterial(connection, password);
|
final encryptedKey = await _encryptVaultKeyMaterial(connection, password);
|
||||||
|
|
||||||
await connection.send(
|
final response = await connection.ask(
|
||||||
UserAgentRequest(
|
UserAgentRequest(
|
||||||
bootstrapEncryptedKey: BootstrapEncryptedKey(
|
bootstrapEncryptedKey: BootstrapEncryptedKey(
|
||||||
nonce: encryptedKey.nonce,
|
nonce: encryptedKey.nonce,
|
||||||
@@ -19,8 +19,6 @@ Future<BootstrapResult> bootstrapVault(
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await connection.receive();
|
|
||||||
if (!response.hasBootstrapResult()) {
|
if (!response.hasBootstrapResult()) {
|
||||||
throw Exception(
|
throw Exception(
|
||||||
'Expected bootstrap result, got ${response.whichPayload()}',
|
'Expected bootstrap result, got ${response.whichPayload()}',
|
||||||
@@ -33,7 +31,7 @@ Future<BootstrapResult> bootstrapVault(
|
|||||||
Future<UnsealResult> unsealVault(Connection connection, String password) async {
|
Future<UnsealResult> unsealVault(Connection connection, String password) async {
|
||||||
final encryptedKey = await _encryptVaultKeyMaterial(connection, password);
|
final encryptedKey = await _encryptVaultKeyMaterial(connection, password);
|
||||||
|
|
||||||
await connection.send(
|
final response = await connection.ask(
|
||||||
UserAgentRequest(
|
UserAgentRequest(
|
||||||
unsealEncryptedKey: UnsealEncryptedKey(
|
unsealEncryptedKey: UnsealEncryptedKey(
|
||||||
nonce: encryptedKey.nonce,
|
nonce: encryptedKey.nonce,
|
||||||
@@ -42,8 +40,6 @@ Future<UnsealResult> unsealVault(Connection connection, String password) async {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final response = await connection.receive();
|
|
||||||
if (!response.hasUnsealResult()) {
|
if (!response.hasUnsealResult()) {
|
||||||
throw Exception('Expected unseal result, got ${response.whichPayload()}');
|
throw Exception('Expected unseal result, got ${response.whichPayload()}');
|
||||||
}
|
}
|
||||||
@@ -60,11 +56,9 @@ Future<_EncryptedVaultKey> _encryptVaultKeyMaterial(
|
|||||||
final clientKeyPair = await keyExchange.newKeyPair();
|
final clientKeyPair = await keyExchange.newKeyPair();
|
||||||
final clientPublicKey = await clientKeyPair.extractPublicKey();
|
final clientPublicKey = await clientKeyPair.extractPublicKey();
|
||||||
|
|
||||||
await connection.send(
|
final handshakeResponse = await connection.ask(
|
||||||
UserAgentRequest(unsealStart: UnsealStart(clientPubkey: clientPublicKey.bytes)),
|
UserAgentRequest(unsealStart: UnsealStart(clientPubkey: clientPublicKey.bytes)),
|
||||||
);
|
);
|
||||||
|
|
||||||
final handshakeResponse = await connection.receive();
|
|
||||||
if (!handshakeResponse.hasUnsealStartResponse()) {
|
if (!handshakeResponse.hasUnsealStartResponse()) {
|
||||||
throw Exception(
|
throw Exception(
|
||||||
'Expected unseal handshake response, got ${handshakeResponse.whichPayload()}',
|
'Expected unseal handshake response, got ${handshakeResponse.whichPayload()}',
|
||||||
|
|||||||
@@ -13,20 +13,100 @@
|
|||||||
import 'dart:core' as $core;
|
import 'dart:core' as $core;
|
||||||
|
|
||||||
import 'package:protobuf/protobuf.dart' as $pb;
|
import 'package:protobuf/protobuf.dart' as $pb;
|
||||||
|
import 'package:protobuf/well_known_types/google/protobuf/empty.pb.dart' as $0;
|
||||||
|
|
||||||
import 'client.pbenum.dart';
|
import 'client.pbenum.dart';
|
||||||
import 'evm.pb.dart' as $0;
|
import 'evm.pb.dart' as $1;
|
||||||
|
|
||||||
export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions;
|
export 'package:protobuf/protobuf.dart' show GeneratedMessageGenericExtensions;
|
||||||
|
|
||||||
export 'client.pbenum.dart';
|
export 'client.pbenum.dart';
|
||||||
|
|
||||||
|
class ClientInfo extends $pb.GeneratedMessage {
|
||||||
|
factory ClientInfo({
|
||||||
|
$core.String? name,
|
||||||
|
$core.String? description,
|
||||||
|
$core.String? version,
|
||||||
|
}) {
|
||||||
|
final result = create();
|
||||||
|
if (name != null) result.name = name;
|
||||||
|
if (description != null) result.description = description;
|
||||||
|
if (version != null) result.version = version;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientInfo._();
|
||||||
|
|
||||||
|
factory ClientInfo.fromBuffer($core.List<$core.int> data,
|
||||||
|
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||||
|
create()..mergeFromBuffer(data, registry);
|
||||||
|
factory ClientInfo.fromJson($core.String json,
|
||||||
|
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||||
|
create()..mergeFromJson(json, registry);
|
||||||
|
|
||||||
|
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||||
|
_omitMessageNames ? '' : 'ClientInfo',
|
||||||
|
package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.client'),
|
||||||
|
createEmptyInstance: create)
|
||||||
|
..aOS(1, _omitFieldNames ? '' : 'name')
|
||||||
|
..aOS(2, _omitFieldNames ? '' : 'description')
|
||||||
|
..aOS(3, _omitFieldNames ? '' : 'version')
|
||||||
|
..hasRequiredFields = false;
|
||||||
|
|
||||||
|
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||||
|
ClientInfo clone() => deepCopy();
|
||||||
|
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||||
|
ClientInfo copyWith(void Function(ClientInfo) updates) =>
|
||||||
|
super.copyWith((message) => updates(message as ClientInfo)) as ClientInfo;
|
||||||
|
|
||||||
|
@$core.override
|
||||||
|
$pb.BuilderInfo get info_ => _i;
|
||||||
|
|
||||||
|
@$core.pragma('dart2js:noInline')
|
||||||
|
static ClientInfo create() => ClientInfo._();
|
||||||
|
@$core.override
|
||||||
|
ClientInfo createEmptyInstance() => create();
|
||||||
|
@$core.pragma('dart2js:noInline')
|
||||||
|
static ClientInfo getDefault() => _defaultInstance ??=
|
||||||
|
$pb.GeneratedMessage.$_defaultFor<ClientInfo>(create);
|
||||||
|
static ClientInfo? _defaultInstance;
|
||||||
|
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
$core.String get name => $_getSZ(0);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
set name($core.String value) => $_setString(0, value);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
$core.bool hasName() => $_has(0);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
void clearName() => $_clearField(1);
|
||||||
|
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
$core.String get description => $_getSZ(1);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
set description($core.String value) => $_setString(1, value);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
$core.bool hasDescription() => $_has(1);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
void clearDescription() => $_clearField(2);
|
||||||
|
|
||||||
|
@$pb.TagNumber(3)
|
||||||
|
$core.String get version => $_getSZ(2);
|
||||||
|
@$pb.TagNumber(3)
|
||||||
|
set version($core.String value) => $_setString(2, value);
|
||||||
|
@$pb.TagNumber(3)
|
||||||
|
$core.bool hasVersion() => $_has(2);
|
||||||
|
@$pb.TagNumber(3)
|
||||||
|
void clearVersion() => $_clearField(3);
|
||||||
|
}
|
||||||
|
|
||||||
class AuthChallengeRequest extends $pb.GeneratedMessage {
|
class AuthChallengeRequest extends $pb.GeneratedMessage {
|
||||||
factory AuthChallengeRequest({
|
factory AuthChallengeRequest({
|
||||||
$core.List<$core.int>? pubkey,
|
$core.List<$core.int>? pubkey,
|
||||||
|
ClientInfo? clientInfo,
|
||||||
}) {
|
}) {
|
||||||
final result = create();
|
final result = create();
|
||||||
if (pubkey != null) result.pubkey = pubkey;
|
if (pubkey != null) result.pubkey = pubkey;
|
||||||
|
if (clientInfo != null) result.clientInfo = clientInfo;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,6 +125,8 @@ class AuthChallengeRequest extends $pb.GeneratedMessage {
|
|||||||
createEmptyInstance: create)
|
createEmptyInstance: create)
|
||||||
..a<$core.List<$core.int>>(
|
..a<$core.List<$core.int>>(
|
||||||
1, _omitFieldNames ? '' : 'pubkey', $pb.PbFieldType.OY)
|
1, _omitFieldNames ? '' : 'pubkey', $pb.PbFieldType.OY)
|
||||||
|
..aOM<ClientInfo>(2, _omitFieldNames ? '' : 'clientInfo',
|
||||||
|
subBuilder: ClientInfo.create)
|
||||||
..hasRequiredFields = false;
|
..hasRequiredFields = false;
|
||||||
|
|
||||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||||
@@ -74,6 +156,17 @@ class AuthChallengeRequest extends $pb.GeneratedMessage {
|
|||||||
$core.bool hasPubkey() => $_has(0);
|
$core.bool hasPubkey() => $_has(0);
|
||||||
@$pb.TagNumber(1)
|
@$pb.TagNumber(1)
|
||||||
void clearPubkey() => $_clearField(1);
|
void clearPubkey() => $_clearField(1);
|
||||||
|
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
ClientInfo get clientInfo => $_getN(1);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
set clientInfo(ClientInfo value) => $_setField(2, value);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
$core.bool hasClientInfo() => $_has(1);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
void clearClientInfo() => $_clearField(2);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
ClientInfo ensureClientInfo() => $_ensure(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
class AuthChallenge extends $pb.GeneratedMessage {
|
class AuthChallenge extends $pb.GeneratedMessage {
|
||||||
@@ -199,46 +292,10 @@ class AuthChallengeSolution extends $pb.GeneratedMessage {
|
|||||||
void clearSignature() => $_clearField(1);
|
void clearSignature() => $_clearField(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
class AuthOk extends $pb.GeneratedMessage {
|
|
||||||
factory AuthOk() => create();
|
|
||||||
|
|
||||||
AuthOk._();
|
|
||||||
|
|
||||||
factory AuthOk.fromBuffer($core.List<$core.int> data,
|
|
||||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
|
||||||
create()..mergeFromBuffer(data, registry);
|
|
||||||
factory AuthOk.fromJson($core.String json,
|
|
||||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
|
||||||
create()..mergeFromJson(json, registry);
|
|
||||||
|
|
||||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
|
||||||
_omitMessageNames ? '' : 'AuthOk',
|
|
||||||
package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.client'),
|
|
||||||
createEmptyInstance: create)
|
|
||||||
..hasRequiredFields = false;
|
|
||||||
|
|
||||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
|
||||||
AuthOk clone() => deepCopy();
|
|
||||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
|
||||||
AuthOk copyWith(void Function(AuthOk) updates) =>
|
|
||||||
super.copyWith((message) => updates(message as AuthOk)) as AuthOk;
|
|
||||||
|
|
||||||
@$core.override
|
|
||||||
$pb.BuilderInfo get info_ => _i;
|
|
||||||
|
|
||||||
@$core.pragma('dart2js:noInline')
|
|
||||||
static AuthOk create() => AuthOk._();
|
|
||||||
@$core.override
|
|
||||||
AuthOk createEmptyInstance() => create();
|
|
||||||
@$core.pragma('dart2js:noInline')
|
|
||||||
static AuthOk getDefault() =>
|
|
||||||
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<AuthOk>(create);
|
|
||||||
static AuthOk? _defaultInstance;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ClientRequest_Payload {
|
enum ClientRequest_Payload {
|
||||||
authChallengeRequest,
|
authChallengeRequest,
|
||||||
authChallengeSolution,
|
authChallengeSolution,
|
||||||
|
queryVaultState,
|
||||||
notSet
|
notSet
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,12 +303,16 @@ class ClientRequest extends $pb.GeneratedMessage {
|
|||||||
factory ClientRequest({
|
factory ClientRequest({
|
||||||
AuthChallengeRequest? authChallengeRequest,
|
AuthChallengeRequest? authChallengeRequest,
|
||||||
AuthChallengeSolution? authChallengeSolution,
|
AuthChallengeSolution? authChallengeSolution,
|
||||||
|
$0.Empty? queryVaultState,
|
||||||
|
$core.int? requestId,
|
||||||
}) {
|
}) {
|
||||||
final result = create();
|
final result = create();
|
||||||
if (authChallengeRequest != null)
|
if (authChallengeRequest != null)
|
||||||
result.authChallengeRequest = authChallengeRequest;
|
result.authChallengeRequest = authChallengeRequest;
|
||||||
if (authChallengeSolution != null)
|
if (authChallengeSolution != null)
|
||||||
result.authChallengeSolution = authChallengeSolution;
|
result.authChallengeSolution = authChallengeSolution;
|
||||||
|
if (queryVaultState != null) result.queryVaultState = queryVaultState;
|
||||||
|
if (requestId != null) result.requestId = requestId;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,19 +329,23 @@ class ClientRequest extends $pb.GeneratedMessage {
|
|||||||
_ClientRequest_PayloadByTag = {
|
_ClientRequest_PayloadByTag = {
|
||||||
1: ClientRequest_Payload.authChallengeRequest,
|
1: ClientRequest_Payload.authChallengeRequest,
|
||||||
2: ClientRequest_Payload.authChallengeSolution,
|
2: ClientRequest_Payload.authChallengeSolution,
|
||||||
|
3: ClientRequest_Payload.queryVaultState,
|
||||||
0: ClientRequest_Payload.notSet
|
0: ClientRequest_Payload.notSet
|
||||||
};
|
};
|
||||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||||
_omitMessageNames ? '' : 'ClientRequest',
|
_omitMessageNames ? '' : 'ClientRequest',
|
||||||
package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.client'),
|
package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.client'),
|
||||||
createEmptyInstance: create)
|
createEmptyInstance: create)
|
||||||
..oo(0, [1, 2])
|
..oo(0, [1, 2, 3])
|
||||||
..aOM<AuthChallengeRequest>(
|
..aOM<AuthChallengeRequest>(
|
||||||
1, _omitFieldNames ? '' : 'authChallengeRequest',
|
1, _omitFieldNames ? '' : 'authChallengeRequest',
|
||||||
subBuilder: AuthChallengeRequest.create)
|
subBuilder: AuthChallengeRequest.create)
|
||||||
..aOM<AuthChallengeSolution>(
|
..aOM<AuthChallengeSolution>(
|
||||||
2, _omitFieldNames ? '' : 'authChallengeSolution',
|
2, _omitFieldNames ? '' : 'authChallengeSolution',
|
||||||
subBuilder: AuthChallengeSolution.create)
|
subBuilder: AuthChallengeSolution.create)
|
||||||
|
..aOM<$0.Empty>(3, _omitFieldNames ? '' : 'queryVaultState',
|
||||||
|
subBuilder: $0.Empty.create)
|
||||||
|
..aI(4, _omitFieldNames ? '' : 'requestId')
|
||||||
..hasRequiredFields = false;
|
..hasRequiredFields = false;
|
||||||
|
|
||||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||||
@@ -304,10 +369,12 @@ class ClientRequest extends $pb.GeneratedMessage {
|
|||||||
|
|
||||||
@$pb.TagNumber(1)
|
@$pb.TagNumber(1)
|
||||||
@$pb.TagNumber(2)
|
@$pb.TagNumber(2)
|
||||||
|
@$pb.TagNumber(3)
|
||||||
ClientRequest_Payload whichPayload() =>
|
ClientRequest_Payload whichPayload() =>
|
||||||
_ClientRequest_PayloadByTag[$_whichOneof(0)]!;
|
_ClientRequest_PayloadByTag[$_whichOneof(0)]!;
|
||||||
@$pb.TagNumber(1)
|
@$pb.TagNumber(1)
|
||||||
@$pb.TagNumber(2)
|
@$pb.TagNumber(2)
|
||||||
|
@$pb.TagNumber(3)
|
||||||
void clearPayload() => $_clearField($_whichOneof(0));
|
void clearPayload() => $_clearField($_whichOneof(0));
|
||||||
|
|
||||||
@$pb.TagNumber(1)
|
@$pb.TagNumber(1)
|
||||||
@@ -332,89 +399,55 @@ class ClientRequest extends $pb.GeneratedMessage {
|
|||||||
void clearAuthChallengeSolution() => $_clearField(2);
|
void clearAuthChallengeSolution() => $_clearField(2);
|
||||||
@$pb.TagNumber(2)
|
@$pb.TagNumber(2)
|
||||||
AuthChallengeSolution ensureAuthChallengeSolution() => $_ensure(1);
|
AuthChallengeSolution ensureAuthChallengeSolution() => $_ensure(1);
|
||||||
}
|
|
||||||
|
|
||||||
class ClientConnectError extends $pb.GeneratedMessage {
|
@$pb.TagNumber(3)
|
||||||
factory ClientConnectError({
|
$0.Empty get queryVaultState => $_getN(2);
|
||||||
ClientConnectError_Code? code,
|
@$pb.TagNumber(3)
|
||||||
}) {
|
set queryVaultState($0.Empty value) => $_setField(3, value);
|
||||||
final result = create();
|
@$pb.TagNumber(3)
|
||||||
if (code != null) result.code = code;
|
$core.bool hasQueryVaultState() => $_has(2);
|
||||||
return result;
|
@$pb.TagNumber(3)
|
||||||
}
|
void clearQueryVaultState() => $_clearField(3);
|
||||||
|
@$pb.TagNumber(3)
|
||||||
|
$0.Empty ensureQueryVaultState() => $_ensure(2);
|
||||||
|
|
||||||
ClientConnectError._();
|
@$pb.TagNumber(4)
|
||||||
|
$core.int get requestId => $_getIZ(3);
|
||||||
factory ClientConnectError.fromBuffer($core.List<$core.int> data,
|
@$pb.TagNumber(4)
|
||||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
set requestId($core.int value) => $_setSignedInt32(3, value);
|
||||||
create()..mergeFromBuffer(data, registry);
|
@$pb.TagNumber(4)
|
||||||
factory ClientConnectError.fromJson($core.String json,
|
$core.bool hasRequestId() => $_has(3);
|
||||||
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
@$pb.TagNumber(4)
|
||||||
create()..mergeFromJson(json, registry);
|
void clearRequestId() => $_clearField(4);
|
||||||
|
|
||||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
|
||||||
_omitMessageNames ? '' : 'ClientConnectError',
|
|
||||||
package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.client'),
|
|
||||||
createEmptyInstance: create)
|
|
||||||
..aE<ClientConnectError_Code>(1, _omitFieldNames ? '' : 'code',
|
|
||||||
enumValues: ClientConnectError_Code.values)
|
|
||||||
..hasRequiredFields = false;
|
|
||||||
|
|
||||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
|
||||||
ClientConnectError clone() => deepCopy();
|
|
||||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
|
||||||
ClientConnectError copyWith(void Function(ClientConnectError) updates) =>
|
|
||||||
super.copyWith((message) => updates(message as ClientConnectError))
|
|
||||||
as ClientConnectError;
|
|
||||||
|
|
||||||
@$core.override
|
|
||||||
$pb.BuilderInfo get info_ => _i;
|
|
||||||
|
|
||||||
@$core.pragma('dart2js:noInline')
|
|
||||||
static ClientConnectError create() => ClientConnectError._();
|
|
||||||
@$core.override
|
|
||||||
ClientConnectError createEmptyInstance() => create();
|
|
||||||
@$core.pragma('dart2js:noInline')
|
|
||||||
static ClientConnectError getDefault() => _defaultInstance ??=
|
|
||||||
$pb.GeneratedMessage.$_defaultFor<ClientConnectError>(create);
|
|
||||||
static ClientConnectError? _defaultInstance;
|
|
||||||
|
|
||||||
@$pb.TagNumber(1)
|
|
||||||
ClientConnectError_Code get code => $_getN(0);
|
|
||||||
@$pb.TagNumber(1)
|
|
||||||
set code(ClientConnectError_Code value) => $_setField(1, value);
|
|
||||||
@$pb.TagNumber(1)
|
|
||||||
$core.bool hasCode() => $_has(0);
|
|
||||||
@$pb.TagNumber(1)
|
|
||||||
void clearCode() => $_clearField(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ClientResponse_Payload {
|
enum ClientResponse_Payload {
|
||||||
authChallenge,
|
authChallenge,
|
||||||
authOk,
|
authResult,
|
||||||
evmSignTransaction,
|
evmSignTransaction,
|
||||||
evmAnalyzeTransaction,
|
evmAnalyzeTransaction,
|
||||||
clientConnectError,
|
vaultState,
|
||||||
notSet
|
notSet
|
||||||
}
|
}
|
||||||
|
|
||||||
class ClientResponse extends $pb.GeneratedMessage {
|
class ClientResponse extends $pb.GeneratedMessage {
|
||||||
factory ClientResponse({
|
factory ClientResponse({
|
||||||
AuthChallenge? authChallenge,
|
AuthChallenge? authChallenge,
|
||||||
AuthOk? authOk,
|
AuthResult? authResult,
|
||||||
$0.EvmSignTransactionResponse? evmSignTransaction,
|
$1.EvmSignTransactionResponse? evmSignTransaction,
|
||||||
$0.EvmAnalyzeTransactionResponse? evmAnalyzeTransaction,
|
$1.EvmAnalyzeTransactionResponse? evmAnalyzeTransaction,
|
||||||
ClientConnectError? clientConnectError,
|
VaultState? vaultState,
|
||||||
|
$core.int? requestId,
|
||||||
}) {
|
}) {
|
||||||
final result = create();
|
final result = create();
|
||||||
if (authChallenge != null) result.authChallenge = authChallenge;
|
if (authChallenge != null) result.authChallenge = authChallenge;
|
||||||
if (authOk != null) result.authOk = authOk;
|
if (authResult != null) result.authResult = authResult;
|
||||||
if (evmSignTransaction != null)
|
if (evmSignTransaction != null)
|
||||||
result.evmSignTransaction = evmSignTransaction;
|
result.evmSignTransaction = evmSignTransaction;
|
||||||
if (evmAnalyzeTransaction != null)
|
if (evmAnalyzeTransaction != null)
|
||||||
result.evmAnalyzeTransaction = evmAnalyzeTransaction;
|
result.evmAnalyzeTransaction = evmAnalyzeTransaction;
|
||||||
if (clientConnectError != null)
|
if (vaultState != null) result.vaultState = vaultState;
|
||||||
result.clientConnectError = clientConnectError;
|
if (requestId != null) result.requestId = requestId;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -430,28 +463,30 @@ class ClientResponse extends $pb.GeneratedMessage {
|
|||||||
static const $core.Map<$core.int, ClientResponse_Payload>
|
static const $core.Map<$core.int, ClientResponse_Payload>
|
||||||
_ClientResponse_PayloadByTag = {
|
_ClientResponse_PayloadByTag = {
|
||||||
1: ClientResponse_Payload.authChallenge,
|
1: ClientResponse_Payload.authChallenge,
|
||||||
2: ClientResponse_Payload.authOk,
|
2: ClientResponse_Payload.authResult,
|
||||||
3: ClientResponse_Payload.evmSignTransaction,
|
3: ClientResponse_Payload.evmSignTransaction,
|
||||||
4: ClientResponse_Payload.evmAnalyzeTransaction,
|
4: ClientResponse_Payload.evmAnalyzeTransaction,
|
||||||
5: ClientResponse_Payload.clientConnectError,
|
6: ClientResponse_Payload.vaultState,
|
||||||
0: ClientResponse_Payload.notSet
|
0: ClientResponse_Payload.notSet
|
||||||
};
|
};
|
||||||
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||||
_omitMessageNames ? '' : 'ClientResponse',
|
_omitMessageNames ? '' : 'ClientResponse',
|
||||||
package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.client'),
|
package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.client'),
|
||||||
createEmptyInstance: create)
|
createEmptyInstance: create)
|
||||||
..oo(0, [1, 2, 3, 4, 5])
|
..oo(0, [1, 2, 3, 4, 6])
|
||||||
..aOM<AuthChallenge>(1, _omitFieldNames ? '' : 'authChallenge',
|
..aOM<AuthChallenge>(1, _omitFieldNames ? '' : 'authChallenge',
|
||||||
subBuilder: AuthChallenge.create)
|
subBuilder: AuthChallenge.create)
|
||||||
..aOM<AuthOk>(2, _omitFieldNames ? '' : 'authOk', subBuilder: AuthOk.create)
|
..aE<AuthResult>(2, _omitFieldNames ? '' : 'authResult',
|
||||||
..aOM<$0.EvmSignTransactionResponse>(
|
enumValues: AuthResult.values)
|
||||||
|
..aOM<$1.EvmSignTransactionResponse>(
|
||||||
3, _omitFieldNames ? '' : 'evmSignTransaction',
|
3, _omitFieldNames ? '' : 'evmSignTransaction',
|
||||||
subBuilder: $0.EvmSignTransactionResponse.create)
|
subBuilder: $1.EvmSignTransactionResponse.create)
|
||||||
..aOM<$0.EvmAnalyzeTransactionResponse>(
|
..aOM<$1.EvmAnalyzeTransactionResponse>(
|
||||||
4, _omitFieldNames ? '' : 'evmAnalyzeTransaction',
|
4, _omitFieldNames ? '' : 'evmAnalyzeTransaction',
|
||||||
subBuilder: $0.EvmAnalyzeTransactionResponse.create)
|
subBuilder: $1.EvmAnalyzeTransactionResponse.create)
|
||||||
..aOM<ClientConnectError>(5, _omitFieldNames ? '' : 'clientConnectError',
|
..aE<VaultState>(6, _omitFieldNames ? '' : 'vaultState',
|
||||||
subBuilder: ClientConnectError.create)
|
enumValues: VaultState.values)
|
||||||
|
..aI(7, _omitFieldNames ? '' : 'requestId')
|
||||||
..hasRequiredFields = false;
|
..hasRequiredFields = false;
|
||||||
|
|
||||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||||
@@ -477,14 +512,14 @@ class ClientResponse extends $pb.GeneratedMessage {
|
|||||||
@$pb.TagNumber(2)
|
@$pb.TagNumber(2)
|
||||||
@$pb.TagNumber(3)
|
@$pb.TagNumber(3)
|
||||||
@$pb.TagNumber(4)
|
@$pb.TagNumber(4)
|
||||||
@$pb.TagNumber(5)
|
@$pb.TagNumber(6)
|
||||||
ClientResponse_Payload whichPayload() =>
|
ClientResponse_Payload whichPayload() =>
|
||||||
_ClientResponse_PayloadByTag[$_whichOneof(0)]!;
|
_ClientResponse_PayloadByTag[$_whichOneof(0)]!;
|
||||||
@$pb.TagNumber(1)
|
@$pb.TagNumber(1)
|
||||||
@$pb.TagNumber(2)
|
@$pb.TagNumber(2)
|
||||||
@$pb.TagNumber(3)
|
@$pb.TagNumber(3)
|
||||||
@$pb.TagNumber(4)
|
@$pb.TagNumber(4)
|
||||||
@$pb.TagNumber(5)
|
@$pb.TagNumber(6)
|
||||||
void clearPayload() => $_clearField($_whichOneof(0));
|
void clearPayload() => $_clearField($_whichOneof(0));
|
||||||
|
|
||||||
@$pb.TagNumber(1)
|
@$pb.TagNumber(1)
|
||||||
@@ -499,50 +534,55 @@ class ClientResponse extends $pb.GeneratedMessage {
|
|||||||
AuthChallenge ensureAuthChallenge() => $_ensure(0);
|
AuthChallenge ensureAuthChallenge() => $_ensure(0);
|
||||||
|
|
||||||
@$pb.TagNumber(2)
|
@$pb.TagNumber(2)
|
||||||
AuthOk get authOk => $_getN(1);
|
AuthResult get authResult => $_getN(1);
|
||||||
@$pb.TagNumber(2)
|
@$pb.TagNumber(2)
|
||||||
set authOk(AuthOk value) => $_setField(2, value);
|
set authResult(AuthResult value) => $_setField(2, value);
|
||||||
@$pb.TagNumber(2)
|
@$pb.TagNumber(2)
|
||||||
$core.bool hasAuthOk() => $_has(1);
|
$core.bool hasAuthResult() => $_has(1);
|
||||||
@$pb.TagNumber(2)
|
@$pb.TagNumber(2)
|
||||||
void clearAuthOk() => $_clearField(2);
|
void clearAuthResult() => $_clearField(2);
|
||||||
@$pb.TagNumber(2)
|
|
||||||
AuthOk ensureAuthOk() => $_ensure(1);
|
|
||||||
|
|
||||||
@$pb.TagNumber(3)
|
@$pb.TagNumber(3)
|
||||||
$0.EvmSignTransactionResponse get evmSignTransaction => $_getN(2);
|
$1.EvmSignTransactionResponse get evmSignTransaction => $_getN(2);
|
||||||
@$pb.TagNumber(3)
|
@$pb.TagNumber(3)
|
||||||
set evmSignTransaction($0.EvmSignTransactionResponse value) =>
|
set evmSignTransaction($1.EvmSignTransactionResponse value) =>
|
||||||
$_setField(3, value);
|
$_setField(3, value);
|
||||||
@$pb.TagNumber(3)
|
@$pb.TagNumber(3)
|
||||||
$core.bool hasEvmSignTransaction() => $_has(2);
|
$core.bool hasEvmSignTransaction() => $_has(2);
|
||||||
@$pb.TagNumber(3)
|
@$pb.TagNumber(3)
|
||||||
void clearEvmSignTransaction() => $_clearField(3);
|
void clearEvmSignTransaction() => $_clearField(3);
|
||||||
@$pb.TagNumber(3)
|
@$pb.TagNumber(3)
|
||||||
$0.EvmSignTransactionResponse ensureEvmSignTransaction() => $_ensure(2);
|
$1.EvmSignTransactionResponse ensureEvmSignTransaction() => $_ensure(2);
|
||||||
|
|
||||||
@$pb.TagNumber(4)
|
@$pb.TagNumber(4)
|
||||||
$0.EvmAnalyzeTransactionResponse get evmAnalyzeTransaction => $_getN(3);
|
$1.EvmAnalyzeTransactionResponse get evmAnalyzeTransaction => $_getN(3);
|
||||||
@$pb.TagNumber(4)
|
@$pb.TagNumber(4)
|
||||||
set evmAnalyzeTransaction($0.EvmAnalyzeTransactionResponse value) =>
|
set evmAnalyzeTransaction($1.EvmAnalyzeTransactionResponse value) =>
|
||||||
$_setField(4, value);
|
$_setField(4, value);
|
||||||
@$pb.TagNumber(4)
|
@$pb.TagNumber(4)
|
||||||
$core.bool hasEvmAnalyzeTransaction() => $_has(3);
|
$core.bool hasEvmAnalyzeTransaction() => $_has(3);
|
||||||
@$pb.TagNumber(4)
|
@$pb.TagNumber(4)
|
||||||
void clearEvmAnalyzeTransaction() => $_clearField(4);
|
void clearEvmAnalyzeTransaction() => $_clearField(4);
|
||||||
@$pb.TagNumber(4)
|
@$pb.TagNumber(4)
|
||||||
$0.EvmAnalyzeTransactionResponse ensureEvmAnalyzeTransaction() => $_ensure(3);
|
$1.EvmAnalyzeTransactionResponse ensureEvmAnalyzeTransaction() => $_ensure(3);
|
||||||
|
|
||||||
@$pb.TagNumber(5)
|
@$pb.TagNumber(6)
|
||||||
ClientConnectError get clientConnectError => $_getN(4);
|
VaultState get vaultState => $_getN(4);
|
||||||
@$pb.TagNumber(5)
|
@$pb.TagNumber(6)
|
||||||
set clientConnectError(ClientConnectError value) => $_setField(5, value);
|
set vaultState(VaultState value) => $_setField(6, value);
|
||||||
@$pb.TagNumber(5)
|
@$pb.TagNumber(6)
|
||||||
$core.bool hasClientConnectError() => $_has(4);
|
$core.bool hasVaultState() => $_has(4);
|
||||||
@$pb.TagNumber(5)
|
@$pb.TagNumber(6)
|
||||||
void clearClientConnectError() => $_clearField(5);
|
void clearVaultState() => $_clearField(6);
|
||||||
@$pb.TagNumber(5)
|
|
||||||
ClientConnectError ensureClientConnectError() => $_ensure(4);
|
@$pb.TagNumber(7)
|
||||||
|
$core.int get requestId => $_getIZ(5);
|
||||||
|
@$pb.TagNumber(7)
|
||||||
|
set requestId($core.int value) => $_setSignedInt32(5, value);
|
||||||
|
@$pb.TagNumber(7)
|
||||||
|
$core.bool hasRequestId() => $_has(5);
|
||||||
|
@$pb.TagNumber(7)
|
||||||
|
void clearRequestId() => $_clearField(7);
|
||||||
}
|
}
|
||||||
|
|
||||||
const $core.bool _omitFieldNames =
|
const $core.bool _omitFieldNames =
|
||||||
|
|||||||
@@ -14,28 +14,66 @@ import 'dart:core' as $core;
|
|||||||
|
|
||||||
import 'package:protobuf/protobuf.dart' as $pb;
|
import 'package:protobuf/protobuf.dart' as $pb;
|
||||||
|
|
||||||
class ClientConnectError_Code extends $pb.ProtobufEnum {
|
class AuthResult extends $pb.ProtobufEnum {
|
||||||
static const ClientConnectError_Code UNKNOWN =
|
static const AuthResult AUTH_RESULT_UNSPECIFIED =
|
||||||
ClientConnectError_Code._(0, _omitEnumNames ? '' : 'UNKNOWN');
|
AuthResult._(0, _omitEnumNames ? '' : 'AUTH_RESULT_UNSPECIFIED');
|
||||||
static const ClientConnectError_Code APPROVAL_DENIED =
|
static const AuthResult AUTH_RESULT_SUCCESS =
|
||||||
ClientConnectError_Code._(1, _omitEnumNames ? '' : 'APPROVAL_DENIED');
|
AuthResult._(1, _omitEnumNames ? '' : 'AUTH_RESULT_SUCCESS');
|
||||||
static const ClientConnectError_Code NO_USER_AGENTS_ONLINE =
|
static const AuthResult AUTH_RESULT_INVALID_KEY =
|
||||||
ClientConnectError_Code._(
|
AuthResult._(2, _omitEnumNames ? '' : 'AUTH_RESULT_INVALID_KEY');
|
||||||
2, _omitEnumNames ? '' : 'NO_USER_AGENTS_ONLINE');
|
static const AuthResult AUTH_RESULT_INVALID_SIGNATURE =
|
||||||
|
AuthResult._(3, _omitEnumNames ? '' : 'AUTH_RESULT_INVALID_SIGNATURE');
|
||||||
|
static const AuthResult AUTH_RESULT_APPROVAL_DENIED =
|
||||||
|
AuthResult._(4, _omitEnumNames ? '' : 'AUTH_RESULT_APPROVAL_DENIED');
|
||||||
|
static const AuthResult AUTH_RESULT_NO_USER_AGENTS_ONLINE = AuthResult._(
|
||||||
|
5, _omitEnumNames ? '' : 'AUTH_RESULT_NO_USER_AGENTS_ONLINE');
|
||||||
|
static const AuthResult AUTH_RESULT_INTERNAL =
|
||||||
|
AuthResult._(6, _omitEnumNames ? '' : 'AUTH_RESULT_INTERNAL');
|
||||||
|
|
||||||
static const $core.List<ClientConnectError_Code> values =
|
static const $core.List<AuthResult> values = <AuthResult>[
|
||||||
<ClientConnectError_Code>[
|
AUTH_RESULT_UNSPECIFIED,
|
||||||
UNKNOWN,
|
AUTH_RESULT_SUCCESS,
|
||||||
APPROVAL_DENIED,
|
AUTH_RESULT_INVALID_KEY,
|
||||||
NO_USER_AGENTS_ONLINE,
|
AUTH_RESULT_INVALID_SIGNATURE,
|
||||||
|
AUTH_RESULT_APPROVAL_DENIED,
|
||||||
|
AUTH_RESULT_NO_USER_AGENTS_ONLINE,
|
||||||
|
AUTH_RESULT_INTERNAL,
|
||||||
];
|
];
|
||||||
|
|
||||||
static final $core.List<ClientConnectError_Code?> _byValue =
|
static final $core.List<AuthResult?> _byValue =
|
||||||
$pb.ProtobufEnum.$_initByValueList(values, 2);
|
$pb.ProtobufEnum.$_initByValueList(values, 6);
|
||||||
static ClientConnectError_Code? valueOf($core.int value) =>
|
static AuthResult? valueOf($core.int value) =>
|
||||||
value < 0 || value >= _byValue.length ? null : _byValue[value];
|
value < 0 || value >= _byValue.length ? null : _byValue[value];
|
||||||
|
|
||||||
const ClientConnectError_Code._(super.value, super.name);
|
const AuthResult._(super.value, super.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
class VaultState extends $pb.ProtobufEnum {
|
||||||
|
static const VaultState VAULT_STATE_UNSPECIFIED =
|
||||||
|
VaultState._(0, _omitEnumNames ? '' : 'VAULT_STATE_UNSPECIFIED');
|
||||||
|
static const VaultState VAULT_STATE_UNBOOTSTRAPPED =
|
||||||
|
VaultState._(1, _omitEnumNames ? '' : 'VAULT_STATE_UNBOOTSTRAPPED');
|
||||||
|
static const VaultState VAULT_STATE_SEALED =
|
||||||
|
VaultState._(2, _omitEnumNames ? '' : 'VAULT_STATE_SEALED');
|
||||||
|
static const VaultState VAULT_STATE_UNSEALED =
|
||||||
|
VaultState._(3, _omitEnumNames ? '' : 'VAULT_STATE_UNSEALED');
|
||||||
|
static const VaultState VAULT_STATE_ERROR =
|
||||||
|
VaultState._(4, _omitEnumNames ? '' : 'VAULT_STATE_ERROR');
|
||||||
|
|
||||||
|
static const $core.List<VaultState> values = <VaultState>[
|
||||||
|
VAULT_STATE_UNSPECIFIED,
|
||||||
|
VAULT_STATE_UNBOOTSTRAPPED,
|
||||||
|
VAULT_STATE_SEALED,
|
||||||
|
VAULT_STATE_UNSEALED,
|
||||||
|
VAULT_STATE_ERROR,
|
||||||
|
];
|
||||||
|
|
||||||
|
static final $core.List<VaultState?> _byValue =
|
||||||
|
$pb.ProtobufEnum.$_initByValueList(values, 4);
|
||||||
|
static VaultState? valueOf($core.int value) =>
|
||||||
|
value < 0 || value >= _byValue.length ? null : _byValue[value];
|
||||||
|
|
||||||
|
const VaultState._(super.value, super.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
const $core.bool _omitEnumNames =
|
const $core.bool _omitEnumNames =
|
||||||
|
|||||||
@@ -15,18 +15,102 @@ import 'dart:convert' as $convert;
|
|||||||
import 'dart:core' as $core;
|
import 'dart:core' as $core;
|
||||||
import 'dart:typed_data' as $typed_data;
|
import 'dart:typed_data' as $typed_data;
|
||||||
|
|
||||||
|
@$core.Deprecated('Use authResultDescriptor instead')
|
||||||
|
const AuthResult$json = {
|
||||||
|
'1': 'AuthResult',
|
||||||
|
'2': [
|
||||||
|
{'1': 'AUTH_RESULT_UNSPECIFIED', '2': 0},
|
||||||
|
{'1': 'AUTH_RESULT_SUCCESS', '2': 1},
|
||||||
|
{'1': 'AUTH_RESULT_INVALID_KEY', '2': 2},
|
||||||
|
{'1': 'AUTH_RESULT_INVALID_SIGNATURE', '2': 3},
|
||||||
|
{'1': 'AUTH_RESULT_APPROVAL_DENIED', '2': 4},
|
||||||
|
{'1': 'AUTH_RESULT_NO_USER_AGENTS_ONLINE', '2': 5},
|
||||||
|
{'1': 'AUTH_RESULT_INTERNAL', '2': 6},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Descriptor for `AuthResult`. Decode as a `google.protobuf.EnumDescriptorProto`.
|
||||||
|
final $typed_data.Uint8List authResultDescriptor = $convert.base64Decode(
|
||||||
|
'CgpBdXRoUmVzdWx0EhsKF0FVVEhfUkVTVUxUX1VOU1BFQ0lGSUVEEAASFwoTQVVUSF9SRVNVTF'
|
||||||
|
'RfU1VDQ0VTUxABEhsKF0FVVEhfUkVTVUxUX0lOVkFMSURfS0VZEAISIQodQVVUSF9SRVNVTFRf'
|
||||||
|
'SU5WQUxJRF9TSUdOQVRVUkUQAxIfChtBVVRIX1JFU1VMVF9BUFBST1ZBTF9ERU5JRUQQBBIlCi'
|
||||||
|
'FBVVRIX1JFU1VMVF9OT19VU0VSX0FHRU5UU19PTkxJTkUQBRIYChRBVVRIX1JFU1VMVF9JTlRF'
|
||||||
|
'Uk5BTBAG');
|
||||||
|
|
||||||
|
@$core.Deprecated('Use vaultStateDescriptor instead')
|
||||||
|
const VaultState$json = {
|
||||||
|
'1': 'VaultState',
|
||||||
|
'2': [
|
||||||
|
{'1': 'VAULT_STATE_UNSPECIFIED', '2': 0},
|
||||||
|
{'1': 'VAULT_STATE_UNBOOTSTRAPPED', '2': 1},
|
||||||
|
{'1': 'VAULT_STATE_SEALED', '2': 2},
|
||||||
|
{'1': 'VAULT_STATE_UNSEALED', '2': 3},
|
||||||
|
{'1': 'VAULT_STATE_ERROR', '2': 4},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Descriptor for `VaultState`. Decode as a `google.protobuf.EnumDescriptorProto`.
|
||||||
|
final $typed_data.Uint8List vaultStateDescriptor = $convert.base64Decode(
|
||||||
|
'CgpWYXVsdFN0YXRlEhsKF1ZBVUxUX1NUQVRFX1VOU1BFQ0lGSUVEEAASHgoaVkFVTFRfU1RBVE'
|
||||||
|
'VfVU5CT09UU1RSQVBQRUQQARIWChJWQVVMVF9TVEFURV9TRUFMRUQQAhIYChRWQVVMVF9TVEFU'
|
||||||
|
'RV9VTlNFQUxFRBADEhUKEVZBVUxUX1NUQVRFX0VSUk9SEAQ=');
|
||||||
|
|
||||||
|
@$core.Deprecated('Use clientInfoDescriptor instead')
|
||||||
|
const ClientInfo$json = {
|
||||||
|
'1': 'ClientInfo',
|
||||||
|
'2': [
|
||||||
|
{'1': 'name', '3': 1, '4': 1, '5': 9, '10': 'name'},
|
||||||
|
{
|
||||||
|
'1': 'description',
|
||||||
|
'3': 2,
|
||||||
|
'4': 1,
|
||||||
|
'5': 9,
|
||||||
|
'9': 0,
|
||||||
|
'10': 'description',
|
||||||
|
'17': true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'1': 'version',
|
||||||
|
'3': 3,
|
||||||
|
'4': 1,
|
||||||
|
'5': 9,
|
||||||
|
'9': 1,
|
||||||
|
'10': 'version',
|
||||||
|
'17': true
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'8': [
|
||||||
|
{'1': '_description'},
|
||||||
|
{'1': '_version'},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Descriptor for `ClientInfo`. Decode as a `google.protobuf.DescriptorProto`.
|
||||||
|
final $typed_data.Uint8List clientInfoDescriptor = $convert.base64Decode(
|
||||||
|
'CgpDbGllbnRJbmZvEhIKBG5hbWUYASABKAlSBG5hbWUSJQoLZGVzY3JpcHRpb24YAiABKAlIAF'
|
||||||
|
'ILZGVzY3JpcHRpb26IAQESHQoHdmVyc2lvbhgDIAEoCUgBUgd2ZXJzaW9uiAEBQg4KDF9kZXNj'
|
||||||
|
'cmlwdGlvbkIKCghfdmVyc2lvbg==');
|
||||||
|
|
||||||
@$core.Deprecated('Use authChallengeRequestDescriptor instead')
|
@$core.Deprecated('Use authChallengeRequestDescriptor instead')
|
||||||
const AuthChallengeRequest$json = {
|
const AuthChallengeRequest$json = {
|
||||||
'1': 'AuthChallengeRequest',
|
'1': 'AuthChallengeRequest',
|
||||||
'2': [
|
'2': [
|
||||||
{'1': 'pubkey', '3': 1, '4': 1, '5': 12, '10': 'pubkey'},
|
{'1': 'pubkey', '3': 1, '4': 1, '5': 12, '10': 'pubkey'},
|
||||||
|
{
|
||||||
|
'1': 'client_info',
|
||||||
|
'3': 2,
|
||||||
|
'4': 1,
|
||||||
|
'5': 11,
|
||||||
|
'6': '.arbiter.client.ClientInfo',
|
||||||
|
'10': 'clientInfo'
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Descriptor for `AuthChallengeRequest`. Decode as a `google.protobuf.DescriptorProto`.
|
/// Descriptor for `AuthChallengeRequest`. Decode as a `google.protobuf.DescriptorProto`.
|
||||||
final $typed_data.Uint8List authChallengeRequestDescriptor =
|
final $typed_data.Uint8List authChallengeRequestDescriptor = $convert.base64Decode(
|
||||||
$convert.base64Decode(
|
'ChRBdXRoQ2hhbGxlbmdlUmVxdWVzdBIWCgZwdWJrZXkYASABKAxSBnB1YmtleRI7CgtjbGllbn'
|
||||||
'ChRBdXRoQ2hhbGxlbmdlUmVxdWVzdBIWCgZwdWJrZXkYASABKAxSBnB1YmtleQ==');
|
'RfaW5mbxgCIAEoCzIaLmFyYml0ZXIuY2xpZW50LkNsaWVudEluZm9SCmNsaWVudEluZm8=');
|
||||||
|
|
||||||
@$core.Deprecated('Use authChallengeDescriptor instead')
|
@$core.Deprecated('Use authChallengeDescriptor instead')
|
||||||
const AuthChallenge$json = {
|
const AuthChallenge$json = {
|
||||||
@@ -54,19 +138,11 @@ const AuthChallengeSolution$json = {
|
|||||||
final $typed_data.Uint8List authChallengeSolutionDescriptor = $convert.base64Decode(
|
final $typed_data.Uint8List authChallengeSolutionDescriptor = $convert.base64Decode(
|
||||||
'ChVBdXRoQ2hhbGxlbmdlU29sdXRpb24SHAoJc2lnbmF0dXJlGAEgASgMUglzaWduYXR1cmU=');
|
'ChVBdXRoQ2hhbGxlbmdlU29sdXRpb24SHAoJc2lnbmF0dXJlGAEgASgMUglzaWduYXR1cmU=');
|
||||||
|
|
||||||
@$core.Deprecated('Use authOkDescriptor instead')
|
|
||||||
const AuthOk$json = {
|
|
||||||
'1': 'AuthOk',
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Descriptor for `AuthOk`. Decode as a `google.protobuf.DescriptorProto`.
|
|
||||||
final $typed_data.Uint8List authOkDescriptor =
|
|
||||||
$convert.base64Decode('CgZBdXRoT2s=');
|
|
||||||
|
|
||||||
@$core.Deprecated('Use clientRequestDescriptor instead')
|
@$core.Deprecated('Use clientRequestDescriptor instead')
|
||||||
const ClientRequest$json = {
|
const ClientRequest$json = {
|
||||||
'1': 'ClientRequest',
|
'1': 'ClientRequest',
|
||||||
'2': [
|
'2': [
|
||||||
|
{'1': 'request_id', '3': 4, '4': 1, '5': 5, '10': 'requestId'},
|
||||||
{
|
{
|
||||||
'1': 'auth_challenge_request',
|
'1': 'auth_challenge_request',
|
||||||
'3': 1,
|
'3': 1,
|
||||||
@@ -85,6 +161,15 @@ const ClientRequest$json = {
|
|||||||
'9': 0,
|
'9': 0,
|
||||||
'10': 'authChallengeSolution'
|
'10': 'authChallengeSolution'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'1': 'query_vault_state',
|
||||||
|
'3': 3,
|
||||||
|
'4': 1,
|
||||||
|
'5': 11,
|
||||||
|
'6': '.google.protobuf.Empty',
|
||||||
|
'9': 0,
|
||||||
|
'10': 'queryVaultState'
|
||||||
|
},
|
||||||
],
|
],
|
||||||
'8': [
|
'8': [
|
||||||
{'1': 'payload'},
|
{'1': 'payload'},
|
||||||
@@ -93,47 +178,26 @@ const ClientRequest$json = {
|
|||||||
|
|
||||||
/// Descriptor for `ClientRequest`. Decode as a `google.protobuf.DescriptorProto`.
|
/// Descriptor for `ClientRequest`. Decode as a `google.protobuf.DescriptorProto`.
|
||||||
final $typed_data.Uint8List clientRequestDescriptor = $convert.base64Decode(
|
final $typed_data.Uint8List clientRequestDescriptor = $convert.base64Decode(
|
||||||
'Cg1DbGllbnRSZXF1ZXN0ElwKFmF1dGhfY2hhbGxlbmdlX3JlcXVlc3QYASABKAsyJC5hcmJpdG'
|
'Cg1DbGllbnRSZXF1ZXN0Eh0KCnJlcXVlc3RfaWQYBCABKAVSCXJlcXVlc3RJZBJcChZhdXRoX2'
|
||||||
'VyLmNsaWVudC5BdXRoQ2hhbGxlbmdlUmVxdWVzdEgAUhRhdXRoQ2hhbGxlbmdlUmVxdWVzdBJf'
|
'NoYWxsZW5nZV9yZXF1ZXN0GAEgASgLMiQuYXJiaXRlci5jbGllbnQuQXV0aENoYWxsZW5nZVJl'
|
||||||
'ChdhdXRoX2NoYWxsZW5nZV9zb2x1dGlvbhgCIAEoCzIlLmFyYml0ZXIuY2xpZW50LkF1dGhDaG'
|
'cXVlc3RIAFIUYXV0aENoYWxsZW5nZVJlcXVlc3QSXwoXYXV0aF9jaGFsbGVuZ2Vfc29sdXRpb2'
|
||||||
'FsbGVuZ2VTb2x1dGlvbkgAUhVhdXRoQ2hhbGxlbmdlU29sdXRpb25CCQoHcGF5bG9hZA==');
|
'4YAiABKAsyJS5hcmJpdGVyLmNsaWVudC5BdXRoQ2hhbGxlbmdlU29sdXRpb25IAFIVYXV0aENo'
|
||||||
|
'YWxsZW5nZVNvbHV0aW9uEkQKEXF1ZXJ5X3ZhdWx0X3N0YXRlGAMgASgLMhYuZ29vZ2xlLnByb3'
|
||||||
@$core.Deprecated('Use clientConnectErrorDescriptor instead')
|
'RvYnVmLkVtcHR5SABSD3F1ZXJ5VmF1bHRTdGF0ZUIJCgdwYXlsb2Fk');
|
||||||
const ClientConnectError$json = {
|
|
||||||
'1': 'ClientConnectError',
|
|
||||||
'2': [
|
|
||||||
{
|
|
||||||
'1': 'code',
|
|
||||||
'3': 1,
|
|
||||||
'4': 1,
|
|
||||||
'5': 14,
|
|
||||||
'6': '.arbiter.client.ClientConnectError.Code',
|
|
||||||
'10': 'code'
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'4': [ClientConnectError_Code$json],
|
|
||||||
};
|
|
||||||
|
|
||||||
@$core.Deprecated('Use clientConnectErrorDescriptor instead')
|
|
||||||
const ClientConnectError_Code$json = {
|
|
||||||
'1': 'Code',
|
|
||||||
'2': [
|
|
||||||
{'1': 'UNKNOWN', '2': 0},
|
|
||||||
{'1': 'APPROVAL_DENIED', '2': 1},
|
|
||||||
{'1': 'NO_USER_AGENTS_ONLINE', '2': 2},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Descriptor for `ClientConnectError`. Decode as a `google.protobuf.DescriptorProto`.
|
|
||||||
final $typed_data.Uint8List clientConnectErrorDescriptor = $convert.base64Decode(
|
|
||||||
'ChJDbGllbnRDb25uZWN0RXJyb3ISOwoEY29kZRgBIAEoDjInLmFyYml0ZXIuY2xpZW50LkNsaW'
|
|
||||||
'VudENvbm5lY3RFcnJvci5Db2RlUgRjb2RlIkMKBENvZGUSCwoHVU5LTk9XThAAEhMKD0FQUFJP'
|
|
||||||
'VkFMX0RFTklFRBABEhkKFU5PX1VTRVJfQUdFTlRTX09OTElORRAC');
|
|
||||||
|
|
||||||
@$core.Deprecated('Use clientResponseDescriptor instead')
|
@$core.Deprecated('Use clientResponseDescriptor instead')
|
||||||
const ClientResponse$json = {
|
const ClientResponse$json = {
|
||||||
'1': 'ClientResponse',
|
'1': 'ClientResponse',
|
||||||
'2': [
|
'2': [
|
||||||
|
{
|
||||||
|
'1': 'request_id',
|
||||||
|
'3': 7,
|
||||||
|
'4': 1,
|
||||||
|
'5': 5,
|
||||||
|
'9': 1,
|
||||||
|
'10': 'requestId',
|
||||||
|
'17': true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
'1': 'auth_challenge',
|
'1': 'auth_challenge',
|
||||||
'3': 1,
|
'3': 1,
|
||||||
@@ -144,22 +208,13 @@ const ClientResponse$json = {
|
|||||||
'10': 'authChallenge'
|
'10': 'authChallenge'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'1': 'auth_ok',
|
'1': 'auth_result',
|
||||||
'3': 2,
|
'3': 2,
|
||||||
'4': 1,
|
'4': 1,
|
||||||
'5': 11,
|
'5': 14,
|
||||||
'6': '.arbiter.client.AuthOk',
|
'6': '.arbiter.client.AuthResult',
|
||||||
'9': 0,
|
'9': 0,
|
||||||
'10': 'authOk'
|
'10': 'authResult'
|
||||||
},
|
|
||||||
{
|
|
||||||
'1': 'client_connect_error',
|
|
||||||
'3': 5,
|
|
||||||
'4': 1,
|
|
||||||
'5': 11,
|
|
||||||
'6': '.arbiter.client.ClientConnectError',
|
|
||||||
'9': 0,
|
|
||||||
'10': 'clientConnectError'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'1': 'evm_sign_transaction',
|
'1': 'evm_sign_transaction',
|
||||||
@@ -179,19 +234,30 @@ const ClientResponse$json = {
|
|||||||
'9': 0,
|
'9': 0,
|
||||||
'10': 'evmAnalyzeTransaction'
|
'10': 'evmAnalyzeTransaction'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'1': 'vault_state',
|
||||||
|
'3': 6,
|
||||||
|
'4': 1,
|
||||||
|
'5': 14,
|
||||||
|
'6': '.arbiter.client.VaultState',
|
||||||
|
'9': 0,
|
||||||
|
'10': 'vaultState'
|
||||||
|
},
|
||||||
],
|
],
|
||||||
'8': [
|
'8': [
|
||||||
{'1': 'payload'},
|
{'1': 'payload'},
|
||||||
|
{'1': '_request_id'},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Descriptor for `ClientResponse`. Decode as a `google.protobuf.DescriptorProto`.
|
/// Descriptor for `ClientResponse`. Decode as a `google.protobuf.DescriptorProto`.
|
||||||
final $typed_data.Uint8List clientResponseDescriptor = $convert.base64Decode(
|
final $typed_data.Uint8List clientResponseDescriptor = $convert.base64Decode(
|
||||||
'Cg5DbGllbnRSZXNwb25zZRJGCg5hdXRoX2NoYWxsZW5nZRgBIAEoCzIdLmFyYml0ZXIuY2xpZW'
|
'Cg5DbGllbnRSZXNwb25zZRIiCgpyZXF1ZXN0X2lkGAcgASgFSAFSCXJlcXVlc3RJZIgBARJGCg'
|
||||||
'50LkF1dGhDaGFsbGVuZ2VIAFINYXV0aENoYWxsZW5nZRIxCgdhdXRoX29rGAIgASgLMhYuYXJi'
|
'5hdXRoX2NoYWxsZW5nZRgBIAEoCzIdLmFyYml0ZXIuY2xpZW50LkF1dGhDaGFsbGVuZ2VIAFIN'
|
||||||
'aXRlci5jbGllbnQuQXV0aE9rSABSBmF1dGhPaxJWChRjbGllbnRfY29ubmVjdF9lcnJvchgFIA'
|
'YXV0aENoYWxsZW5nZRI9CgthdXRoX3Jlc3VsdBgCIAEoDjIaLmFyYml0ZXIuY2xpZW50LkF1dG'
|
||||||
'EoCzIiLmFyYml0ZXIuY2xpZW50LkNsaWVudENvbm5lY3RFcnJvckgAUhJjbGllbnRDb25uZWN0'
|
'hSZXN1bHRIAFIKYXV0aFJlc3VsdBJbChRldm1fc2lnbl90cmFuc2FjdGlvbhgDIAEoCzInLmFy'
|
||||||
'RXJyb3ISWwoUZXZtX3NpZ25fdHJhbnNhY3Rpb24YAyABKAsyJy5hcmJpdGVyLmV2bS5Fdm1TaW'
|
'Yml0ZXIuZXZtLkV2bVNpZ25UcmFuc2FjdGlvblJlc3BvbnNlSABSEmV2bVNpZ25UcmFuc2FjdG'
|
||||||
'duVHJhbnNhY3Rpb25SZXNwb25zZUgAUhJldm1TaWduVHJhbnNhY3Rpb24SZAoXZXZtX2FuYWx5'
|
'lvbhJkChdldm1fYW5hbHl6ZV90cmFuc2FjdGlvbhgEIAEoCzIqLmFyYml0ZXIuZXZtLkV2bUFu'
|
||||||
'emVfdHJhbnNhY3Rpb24YBCABKAsyKi5hcmJpdGVyLmV2bS5Fdm1BbmFseXplVHJhbnNhY3Rpb2'
|
'YWx5emVUcmFuc2FjdGlvblJlc3BvbnNlSABSFWV2bUFuYWx5emVUcmFuc2FjdGlvbhI9Cgt2YX'
|
||||||
'5SZXNwb25zZUgAUhVldm1BbmFseXplVHJhbnNhY3Rpb25CCQoHcGF5bG9hZA==');
|
'VsdF9zdGF0ZRgGIAEoDjIaLmFyYml0ZXIuY2xpZW50LlZhdWx0U3RhdGVIAFIKdmF1bHRTdGF0'
|
||||||
|
'ZUIJCgdwYXlsb2FkQg0KC19yZXF1ZXN0X2lk');
|
||||||
|
|||||||
@@ -26,9 +26,11 @@ export 'evm.pbenum.dart';
|
|||||||
|
|
||||||
class WalletEntry extends $pb.GeneratedMessage {
|
class WalletEntry extends $pb.GeneratedMessage {
|
||||||
factory WalletEntry({
|
factory WalletEntry({
|
||||||
|
$core.int? id,
|
||||||
$core.List<$core.int>? address,
|
$core.List<$core.int>? address,
|
||||||
}) {
|
}) {
|
||||||
final result = create();
|
final result = create();
|
||||||
|
if (id != null) result.id = id;
|
||||||
if (address != null) result.address = address;
|
if (address != null) result.address = address;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -46,8 +48,9 @@ class WalletEntry extends $pb.GeneratedMessage {
|
|||||||
_omitMessageNames ? '' : 'WalletEntry',
|
_omitMessageNames ? '' : 'WalletEntry',
|
||||||
package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.evm'),
|
package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.evm'),
|
||||||
createEmptyInstance: create)
|
createEmptyInstance: create)
|
||||||
|
..aI(1, _omitFieldNames ? '' : 'id')
|
||||||
..a<$core.List<$core.int>>(
|
..a<$core.List<$core.int>>(
|
||||||
1, _omitFieldNames ? '' : 'address', $pb.PbFieldType.OY)
|
2, _omitFieldNames ? '' : 'address', $pb.PbFieldType.OY)
|
||||||
..hasRequiredFields = false;
|
..hasRequiredFields = false;
|
||||||
|
|
||||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||||
@@ -70,13 +73,22 @@ class WalletEntry extends $pb.GeneratedMessage {
|
|||||||
static WalletEntry? _defaultInstance;
|
static WalletEntry? _defaultInstance;
|
||||||
|
|
||||||
@$pb.TagNumber(1)
|
@$pb.TagNumber(1)
|
||||||
$core.List<$core.int> get address => $_getN(0);
|
$core.int get id => $_getIZ(0);
|
||||||
@$pb.TagNumber(1)
|
@$pb.TagNumber(1)
|
||||||
set address($core.List<$core.int> value) => $_setBytes(0, value);
|
set id($core.int value) => $_setSignedInt32(0, value);
|
||||||
@$pb.TagNumber(1)
|
@$pb.TagNumber(1)
|
||||||
$core.bool hasAddress() => $_has(0);
|
$core.bool hasId() => $_has(0);
|
||||||
@$pb.TagNumber(1)
|
@$pb.TagNumber(1)
|
||||||
void clearAddress() => $_clearField(1);
|
void clearId() => $_clearField(1);
|
||||||
|
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
$core.List<$core.int> get address => $_getN(1);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
set address($core.List<$core.int> value) => $_setBytes(1, value);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
$core.bool hasAddress() => $_has(1);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
void clearAddress() => $_clearField(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
class WalletList extends $pb.GeneratedMessage {
|
class WalletList extends $pb.GeneratedMessage {
|
||||||
@@ -436,7 +448,7 @@ class VolumeRateLimit extends $pb.GeneratedMessage {
|
|||||||
|
|
||||||
class SharedSettings extends $pb.GeneratedMessage {
|
class SharedSettings extends $pb.GeneratedMessage {
|
||||||
factory SharedSettings({
|
factory SharedSettings({
|
||||||
$core.int? walletId,
|
$core.int? walletAccessId,
|
||||||
$fixnum.Int64? chainId,
|
$fixnum.Int64? chainId,
|
||||||
$0.Timestamp? validFrom,
|
$0.Timestamp? validFrom,
|
||||||
$0.Timestamp? validUntil,
|
$0.Timestamp? validUntil,
|
||||||
@@ -445,7 +457,7 @@ class SharedSettings extends $pb.GeneratedMessage {
|
|||||||
TransactionRateLimit? rateLimit,
|
TransactionRateLimit? rateLimit,
|
||||||
}) {
|
}) {
|
||||||
final result = create();
|
final result = create();
|
||||||
if (walletId != null) result.walletId = walletId;
|
if (walletAccessId != null) result.walletAccessId = walletAccessId;
|
||||||
if (chainId != null) result.chainId = chainId;
|
if (chainId != null) result.chainId = chainId;
|
||||||
if (validFrom != null) result.validFrom = validFrom;
|
if (validFrom != null) result.validFrom = validFrom;
|
||||||
if (validUntil != null) result.validUntil = validUntil;
|
if (validUntil != null) result.validUntil = validUntil;
|
||||||
@@ -469,7 +481,7 @@ class SharedSettings extends $pb.GeneratedMessage {
|
|||||||
_omitMessageNames ? '' : 'SharedSettings',
|
_omitMessageNames ? '' : 'SharedSettings',
|
||||||
package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.evm'),
|
package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.evm'),
|
||||||
createEmptyInstance: create)
|
createEmptyInstance: create)
|
||||||
..aI(1, _omitFieldNames ? '' : 'walletId')
|
..aI(1, _omitFieldNames ? '' : 'walletAccessId')
|
||||||
..a<$fixnum.Int64>(2, _omitFieldNames ? '' : 'chainId', $pb.PbFieldType.OU6,
|
..a<$fixnum.Int64>(2, _omitFieldNames ? '' : 'chainId', $pb.PbFieldType.OU6,
|
||||||
defaultOrMaker: $fixnum.Int64.ZERO)
|
defaultOrMaker: $fixnum.Int64.ZERO)
|
||||||
..aOM<$0.Timestamp>(3, _omitFieldNames ? '' : 'validFrom',
|
..aOM<$0.Timestamp>(3, _omitFieldNames ? '' : 'validFrom',
|
||||||
@@ -504,13 +516,13 @@ class SharedSettings extends $pb.GeneratedMessage {
|
|||||||
static SharedSettings? _defaultInstance;
|
static SharedSettings? _defaultInstance;
|
||||||
|
|
||||||
@$pb.TagNumber(1)
|
@$pb.TagNumber(1)
|
||||||
$core.int get walletId => $_getIZ(0);
|
$core.int get walletAccessId => $_getIZ(0);
|
||||||
@$pb.TagNumber(1)
|
@$pb.TagNumber(1)
|
||||||
set walletId($core.int value) => $_setSignedInt32(0, value);
|
set walletAccessId($core.int value) => $_setSignedInt32(0, value);
|
||||||
@$pb.TagNumber(1)
|
@$pb.TagNumber(1)
|
||||||
$core.bool hasWalletId() => $_has(0);
|
$core.bool hasWalletAccessId() => $_has(0);
|
||||||
@$pb.TagNumber(1)
|
@$pb.TagNumber(1)
|
||||||
void clearWalletId() => $_clearField(1);
|
void clearWalletAccessId() => $_clearField(1);
|
||||||
|
|
||||||
@$pb.TagNumber(2)
|
@$pb.TagNumber(2)
|
||||||
$fixnum.Int64 get chainId => $_getI64(1);
|
$fixnum.Int64 get chainId => $_getI64(1);
|
||||||
@@ -1625,12 +1637,10 @@ class TransactionEvalError extends $pb.GeneratedMessage {
|
|||||||
/// --- UserAgent grant management ---
|
/// --- UserAgent grant management ---
|
||||||
class EvmGrantCreateRequest extends $pb.GeneratedMessage {
|
class EvmGrantCreateRequest extends $pb.GeneratedMessage {
|
||||||
factory EvmGrantCreateRequest({
|
factory EvmGrantCreateRequest({
|
||||||
$core.int? clientId,
|
|
||||||
SharedSettings? shared,
|
SharedSettings? shared,
|
||||||
SpecificGrant? specific,
|
SpecificGrant? specific,
|
||||||
}) {
|
}) {
|
||||||
final result = create();
|
final result = create();
|
||||||
if (clientId != null) result.clientId = clientId;
|
|
||||||
if (shared != null) result.shared = shared;
|
if (shared != null) result.shared = shared;
|
||||||
if (specific != null) result.specific = specific;
|
if (specific != null) result.specific = specific;
|
||||||
return result;
|
return result;
|
||||||
@@ -1649,10 +1659,9 @@ class EvmGrantCreateRequest extends $pb.GeneratedMessage {
|
|||||||
_omitMessageNames ? '' : 'EvmGrantCreateRequest',
|
_omitMessageNames ? '' : 'EvmGrantCreateRequest',
|
||||||
package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.evm'),
|
package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.evm'),
|
||||||
createEmptyInstance: create)
|
createEmptyInstance: create)
|
||||||
..aI(1, _omitFieldNames ? '' : 'clientId')
|
..aOM<SharedSettings>(1, _omitFieldNames ? '' : 'shared',
|
||||||
..aOM<SharedSettings>(2, _omitFieldNames ? '' : 'shared',
|
|
||||||
subBuilder: SharedSettings.create)
|
subBuilder: SharedSettings.create)
|
||||||
..aOM<SpecificGrant>(3, _omitFieldNames ? '' : 'specific',
|
..aOM<SpecificGrant>(2, _omitFieldNames ? '' : 'specific',
|
||||||
subBuilder: SpecificGrant.create)
|
subBuilder: SpecificGrant.create)
|
||||||
..hasRequiredFields = false;
|
..hasRequiredFields = false;
|
||||||
|
|
||||||
@@ -1677,35 +1686,26 @@ class EvmGrantCreateRequest extends $pb.GeneratedMessage {
|
|||||||
static EvmGrantCreateRequest? _defaultInstance;
|
static EvmGrantCreateRequest? _defaultInstance;
|
||||||
|
|
||||||
@$pb.TagNumber(1)
|
@$pb.TagNumber(1)
|
||||||
$core.int get clientId => $_getIZ(0);
|
SharedSettings get shared => $_getN(0);
|
||||||
@$pb.TagNumber(1)
|
@$pb.TagNumber(1)
|
||||||
set clientId($core.int value) => $_setSignedInt32(0, value);
|
set shared(SharedSettings value) => $_setField(1, value);
|
||||||
@$pb.TagNumber(1)
|
@$pb.TagNumber(1)
|
||||||
$core.bool hasClientId() => $_has(0);
|
$core.bool hasShared() => $_has(0);
|
||||||
@$pb.TagNumber(1)
|
@$pb.TagNumber(1)
|
||||||
void clearClientId() => $_clearField(1);
|
void clearShared() => $_clearField(1);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
SharedSettings ensureShared() => $_ensure(0);
|
||||||
|
|
||||||
@$pb.TagNumber(2)
|
@$pb.TagNumber(2)
|
||||||
SharedSettings get shared => $_getN(1);
|
SpecificGrant get specific => $_getN(1);
|
||||||
@$pb.TagNumber(2)
|
@$pb.TagNumber(2)
|
||||||
set shared(SharedSettings value) => $_setField(2, value);
|
set specific(SpecificGrant value) => $_setField(2, value);
|
||||||
@$pb.TagNumber(2)
|
@$pb.TagNumber(2)
|
||||||
$core.bool hasShared() => $_has(1);
|
$core.bool hasSpecific() => $_has(1);
|
||||||
@$pb.TagNumber(2)
|
@$pb.TagNumber(2)
|
||||||
void clearShared() => $_clearField(2);
|
void clearSpecific() => $_clearField(2);
|
||||||
@$pb.TagNumber(2)
|
@$pb.TagNumber(2)
|
||||||
SharedSettings ensureShared() => $_ensure(1);
|
SpecificGrant ensureSpecific() => $_ensure(1);
|
||||||
|
|
||||||
@$pb.TagNumber(3)
|
|
||||||
SpecificGrant get specific => $_getN(2);
|
|
||||||
@$pb.TagNumber(3)
|
|
||||||
set specific(SpecificGrant value) => $_setField(3, value);
|
|
||||||
@$pb.TagNumber(3)
|
|
||||||
$core.bool hasSpecific() => $_has(2);
|
|
||||||
@$pb.TagNumber(3)
|
|
||||||
void clearSpecific() => $_clearField(3);
|
|
||||||
@$pb.TagNumber(3)
|
|
||||||
SpecificGrant ensureSpecific() => $_ensure(2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum EvmGrantCreateResponse_Result { grantId, error, notSet }
|
enum EvmGrantCreateResponse_Result { grantId, error, notSet }
|
||||||
@@ -1939,13 +1939,13 @@ class EvmGrantDeleteResponse extends $pb.GeneratedMessage {
|
|||||||
class GrantEntry extends $pb.GeneratedMessage {
|
class GrantEntry extends $pb.GeneratedMessage {
|
||||||
factory GrantEntry({
|
factory GrantEntry({
|
||||||
$core.int? id,
|
$core.int? id,
|
||||||
$core.int? clientId,
|
$core.int? walletAccessId,
|
||||||
SharedSettings? shared,
|
SharedSettings? shared,
|
||||||
SpecificGrant? specific,
|
SpecificGrant? specific,
|
||||||
}) {
|
}) {
|
||||||
final result = create();
|
final result = create();
|
||||||
if (id != null) result.id = id;
|
if (id != null) result.id = id;
|
||||||
if (clientId != null) result.clientId = clientId;
|
if (walletAccessId != null) result.walletAccessId = walletAccessId;
|
||||||
if (shared != null) result.shared = shared;
|
if (shared != null) result.shared = shared;
|
||||||
if (specific != null) result.specific = specific;
|
if (specific != null) result.specific = specific;
|
||||||
return result;
|
return result;
|
||||||
@@ -1965,7 +1965,7 @@ class GrantEntry extends $pb.GeneratedMessage {
|
|||||||
package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.evm'),
|
package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.evm'),
|
||||||
createEmptyInstance: create)
|
createEmptyInstance: create)
|
||||||
..aI(1, _omitFieldNames ? '' : 'id')
|
..aI(1, _omitFieldNames ? '' : 'id')
|
||||||
..aI(2, _omitFieldNames ? '' : 'clientId')
|
..aI(2, _omitFieldNames ? '' : 'walletAccessId')
|
||||||
..aOM<SharedSettings>(3, _omitFieldNames ? '' : 'shared',
|
..aOM<SharedSettings>(3, _omitFieldNames ? '' : 'shared',
|
||||||
subBuilder: SharedSettings.create)
|
subBuilder: SharedSettings.create)
|
||||||
..aOM<SpecificGrant>(4, _omitFieldNames ? '' : 'specific',
|
..aOM<SpecificGrant>(4, _omitFieldNames ? '' : 'specific',
|
||||||
@@ -2000,13 +2000,13 @@ class GrantEntry extends $pb.GeneratedMessage {
|
|||||||
void clearId() => $_clearField(1);
|
void clearId() => $_clearField(1);
|
||||||
|
|
||||||
@$pb.TagNumber(2)
|
@$pb.TagNumber(2)
|
||||||
$core.int get clientId => $_getIZ(1);
|
$core.int get walletAccessId => $_getIZ(1);
|
||||||
@$pb.TagNumber(2)
|
@$pb.TagNumber(2)
|
||||||
set clientId($core.int value) => $_setSignedInt32(1, value);
|
set walletAccessId($core.int value) => $_setSignedInt32(1, value);
|
||||||
@$pb.TagNumber(2)
|
@$pb.TagNumber(2)
|
||||||
$core.bool hasClientId() => $_has(1);
|
$core.bool hasWalletAccessId() => $_has(1);
|
||||||
@$pb.TagNumber(2)
|
@$pb.TagNumber(2)
|
||||||
void clearClientId() => $_clearField(2);
|
void clearWalletAccessId() => $_clearField(2);
|
||||||
|
|
||||||
@$pb.TagNumber(3)
|
@$pb.TagNumber(3)
|
||||||
SharedSettings get shared => $_getN(2);
|
SharedSettings get shared => $_getN(2);
|
||||||
@@ -2033,10 +2033,10 @@ class GrantEntry extends $pb.GeneratedMessage {
|
|||||||
|
|
||||||
class EvmGrantListRequest extends $pb.GeneratedMessage {
|
class EvmGrantListRequest extends $pb.GeneratedMessage {
|
||||||
factory EvmGrantListRequest({
|
factory EvmGrantListRequest({
|
||||||
$core.int? walletId,
|
$core.int? walletAccessId,
|
||||||
}) {
|
}) {
|
||||||
final result = create();
|
final result = create();
|
||||||
if (walletId != null) result.walletId = walletId;
|
if (walletAccessId != null) result.walletAccessId = walletAccessId;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2053,7 +2053,7 @@ class EvmGrantListRequest extends $pb.GeneratedMessage {
|
|||||||
_omitMessageNames ? '' : 'EvmGrantListRequest',
|
_omitMessageNames ? '' : 'EvmGrantListRequest',
|
||||||
package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.evm'),
|
package: const $pb.PackageName(_omitMessageNames ? '' : 'arbiter.evm'),
|
||||||
createEmptyInstance: create)
|
createEmptyInstance: create)
|
||||||
..aI(1, _omitFieldNames ? '' : 'walletId')
|
..aI(1, _omitFieldNames ? '' : 'walletAccessId')
|
||||||
..hasRequiredFields = false;
|
..hasRequiredFields = false;
|
||||||
|
|
||||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||||
@@ -2076,13 +2076,13 @@ class EvmGrantListRequest extends $pb.GeneratedMessage {
|
|||||||
static EvmGrantListRequest? _defaultInstance;
|
static EvmGrantListRequest? _defaultInstance;
|
||||||
|
|
||||||
@$pb.TagNumber(1)
|
@$pb.TagNumber(1)
|
||||||
$core.int get walletId => $_getIZ(0);
|
$core.int get walletAccessId => $_getIZ(0);
|
||||||
@$pb.TagNumber(1)
|
@$pb.TagNumber(1)
|
||||||
set walletId($core.int value) => $_setSignedInt32(0, value);
|
set walletAccessId($core.int value) => $_setSignedInt32(0, value);
|
||||||
@$pb.TagNumber(1)
|
@$pb.TagNumber(1)
|
||||||
$core.bool hasWalletId() => $_has(0);
|
$core.bool hasWalletAccessId() => $_has(0);
|
||||||
@$pb.TagNumber(1)
|
@$pb.TagNumber(1)
|
||||||
void clearWalletId() => $_clearField(1);
|
void clearWalletAccessId() => $_clearField(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
enum EvmGrantListResponse_Result { grants, error, notSet }
|
enum EvmGrantListResponse_Result { grants, error, notSet }
|
||||||
|
|||||||
@@ -34,13 +34,15 @@ final $typed_data.Uint8List evmErrorDescriptor = $convert.base64Decode(
|
|||||||
const WalletEntry$json = {
|
const WalletEntry$json = {
|
||||||
'1': 'WalletEntry',
|
'1': 'WalletEntry',
|
||||||
'2': [
|
'2': [
|
||||||
{'1': 'address', '3': 1, '4': 1, '5': 12, '10': 'address'},
|
{'1': 'id', '3': 1, '4': 1, '5': 5, '10': 'id'},
|
||||||
|
{'1': 'address', '3': 2, '4': 1, '5': 12, '10': 'address'},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Descriptor for `WalletEntry`. Decode as a `google.protobuf.DescriptorProto`.
|
/// Descriptor for `WalletEntry`. Decode as a `google.protobuf.DescriptorProto`.
|
||||||
final $typed_data.Uint8List walletEntryDescriptor = $convert
|
final $typed_data.Uint8List walletEntryDescriptor = $convert.base64Decode(
|
||||||
.base64Decode('CgtXYWxsZXRFbnRyeRIYCgdhZGRyZXNzGAEgASgMUgdhZGRyZXNz');
|
'CgtXYWxsZXRFbnRyeRIOCgJpZBgBIAEoBVICaWQSGAoHYWRkcmVzcxgCIAEoDFIHYWRkcmVzcw'
|
||||||
|
'==');
|
||||||
|
|
||||||
@$core.Deprecated('Use walletListDescriptor instead')
|
@$core.Deprecated('Use walletListDescriptor instead')
|
||||||
const WalletList$json = {
|
const WalletList$json = {
|
||||||
@@ -162,7 +164,7 @@ final $typed_data.Uint8List volumeRateLimitDescriptor = $convert.base64Decode(
|
|||||||
const SharedSettings$json = {
|
const SharedSettings$json = {
|
||||||
'1': 'SharedSettings',
|
'1': 'SharedSettings',
|
||||||
'2': [
|
'2': [
|
||||||
{'1': 'wallet_id', '3': 1, '4': 1, '5': 5, '10': 'walletId'},
|
{'1': 'wallet_access_id', '3': 1, '4': 1, '5': 5, '10': 'walletAccessId'},
|
||||||
{'1': 'chain_id', '3': 2, '4': 1, '5': 4, '10': 'chainId'},
|
{'1': 'chain_id', '3': 2, '4': 1, '5': 4, '10': 'chainId'},
|
||||||
{
|
{
|
||||||
'1': 'valid_from',
|
'1': 'valid_from',
|
||||||
@@ -224,15 +226,15 @@ const SharedSettings$json = {
|
|||||||
|
|
||||||
/// Descriptor for `SharedSettings`. Decode as a `google.protobuf.DescriptorProto`.
|
/// Descriptor for `SharedSettings`. Decode as a `google.protobuf.DescriptorProto`.
|
||||||
final $typed_data.Uint8List sharedSettingsDescriptor = $convert.base64Decode(
|
final $typed_data.Uint8List sharedSettingsDescriptor = $convert.base64Decode(
|
||||||
'Cg5TaGFyZWRTZXR0aW5ncxIbCgl3YWxsZXRfaWQYASABKAVSCHdhbGxldElkEhkKCGNoYWluX2'
|
'Cg5TaGFyZWRTZXR0aW5ncxIoChB3YWxsZXRfYWNjZXNzX2lkGAEgASgFUg53YWxsZXRBY2Nlc3'
|
||||||
'lkGAIgASgEUgdjaGFpbklkEj4KCnZhbGlkX2Zyb20YAyABKAsyGi5nb29nbGUucHJvdG9idWYu'
|
'NJZBIZCghjaGFpbl9pZBgCIAEoBFIHY2hhaW5JZBI+Cgp2YWxpZF9mcm9tGAMgASgLMhouZ29v'
|
||||||
'VGltZXN0YW1wSABSCXZhbGlkRnJvbYgBARJACgt2YWxpZF91bnRpbBgEIAEoCzIaLmdvb2dsZS'
|
'Z2xlLnByb3RvYnVmLlRpbWVzdGFtcEgAUgl2YWxpZEZyb22IAQESQAoLdmFsaWRfdW50aWwYBC'
|
||||||
'5wcm90b2J1Zi5UaW1lc3RhbXBIAVIKdmFsaWRVbnRpbIgBARIxChNtYXhfZ2FzX2ZlZV9wZXJf'
|
'ABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wSAFSCnZhbGlkVW50aWyIAQESMQoTbWF4'
|
||||||
'Z2FzGAUgASgMSAJSD21heEdhc0ZlZVBlckdhc4gBARI7ChhtYXhfcHJpb3JpdHlfZmVlX3Blcl'
|
'X2dhc19mZWVfcGVyX2dhcxgFIAEoDEgCUg9tYXhHYXNGZWVQZXJHYXOIAQESOwoYbWF4X3ByaW'
|
||||||
'9nYXMYBiABKAxIA1IUbWF4UHJpb3JpdHlGZWVQZXJHYXOIAQESRQoKcmF0ZV9saW1pdBgHIAEo'
|
'9yaXR5X2ZlZV9wZXJfZ2FzGAYgASgMSANSFG1heFByaW9yaXR5RmVlUGVyR2FziAEBEkUKCnJh'
|
||||||
'CzIhLmFyYml0ZXIuZXZtLlRyYW5zYWN0aW9uUmF0ZUxpbWl0SARSCXJhdGVMaW1pdIgBAUINCg'
|
'dGVfbGltaXQYByABKAsyIS5hcmJpdGVyLmV2bS5UcmFuc2FjdGlvblJhdGVMaW1pdEgEUglyYX'
|
||||||
'tfdmFsaWRfZnJvbUIOCgxfdmFsaWRfdW50aWxCFgoUX21heF9nYXNfZmVlX3Blcl9nYXNCGwoZ'
|
'RlTGltaXSIAQFCDQoLX3ZhbGlkX2Zyb21CDgoMX3ZhbGlkX3VudGlsQhYKFF9tYXhfZ2FzX2Zl'
|
||||||
'X21heF9wcmlvcml0eV9mZWVfcGVyX2dhc0INCgtfcmF0ZV9saW1pdA==');
|
'ZV9wZXJfZ2FzQhsKGV9tYXhfcHJpb3JpdHlfZmVlX3Blcl9nYXNCDQoLX3JhdGVfbGltaXQ=');
|
||||||
|
|
||||||
@$core.Deprecated('Use etherTransferSettingsDescriptor instead')
|
@$core.Deprecated('Use etherTransferSettingsDescriptor instead')
|
||||||
const EtherTransferSettings$json = {
|
const EtherTransferSettings$json = {
|
||||||
@@ -631,10 +633,9 @@ final $typed_data.Uint8List transactionEvalErrorDescriptor = $convert.base64Deco
|
|||||||
const EvmGrantCreateRequest$json = {
|
const EvmGrantCreateRequest$json = {
|
||||||
'1': 'EvmGrantCreateRequest',
|
'1': 'EvmGrantCreateRequest',
|
||||||
'2': [
|
'2': [
|
||||||
{'1': 'client_id', '3': 1, '4': 1, '5': 5, '10': 'clientId'},
|
|
||||||
{
|
{
|
||||||
'1': 'shared',
|
'1': 'shared',
|
||||||
'3': 2,
|
'3': 1,
|
||||||
'4': 1,
|
'4': 1,
|
||||||
'5': 11,
|
'5': 11,
|
||||||
'6': '.arbiter.evm.SharedSettings',
|
'6': '.arbiter.evm.SharedSettings',
|
||||||
@@ -642,7 +643,7 @@ const EvmGrantCreateRequest$json = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
'1': 'specific',
|
'1': 'specific',
|
||||||
'3': 3,
|
'3': 2,
|
||||||
'4': 1,
|
'4': 1,
|
||||||
'5': 11,
|
'5': 11,
|
||||||
'6': '.arbiter.evm.SpecificGrant',
|
'6': '.arbiter.evm.SpecificGrant',
|
||||||
@@ -653,9 +654,9 @@ const EvmGrantCreateRequest$json = {
|
|||||||
|
|
||||||
/// Descriptor for `EvmGrantCreateRequest`. Decode as a `google.protobuf.DescriptorProto`.
|
/// Descriptor for `EvmGrantCreateRequest`. Decode as a `google.protobuf.DescriptorProto`.
|
||||||
final $typed_data.Uint8List evmGrantCreateRequestDescriptor = $convert.base64Decode(
|
final $typed_data.Uint8List evmGrantCreateRequestDescriptor = $convert.base64Decode(
|
||||||
'ChVFdm1HcmFudENyZWF0ZVJlcXVlc3QSGwoJY2xpZW50X2lkGAEgASgFUghjbGllbnRJZBIzCg'
|
'ChVFdm1HcmFudENyZWF0ZVJlcXVlc3QSMwoGc2hhcmVkGAEgASgLMhsuYXJiaXRlci5ldm0uU2'
|
||||||
'ZzaGFyZWQYAiABKAsyGy5hcmJpdGVyLmV2bS5TaGFyZWRTZXR0aW5nc1IGc2hhcmVkEjYKCHNw'
|
'hhcmVkU2V0dGluZ3NSBnNoYXJlZBI2CghzcGVjaWZpYxgCIAEoCzIaLmFyYml0ZXIuZXZtLlNw'
|
||||||
'ZWNpZmljGAMgASgLMhouYXJiaXRlci5ldm0uU3BlY2lmaWNHcmFudFIIc3BlY2lmaWM=');
|
'ZWNpZmljR3JhbnRSCHNwZWNpZmlj');
|
||||||
|
|
||||||
@$core.Deprecated('Use evmGrantCreateResponseDescriptor instead')
|
@$core.Deprecated('Use evmGrantCreateResponseDescriptor instead')
|
||||||
const EvmGrantCreateResponse$json = {
|
const EvmGrantCreateResponse$json = {
|
||||||
@@ -734,7 +735,7 @@ const GrantEntry$json = {
|
|||||||
'1': 'GrantEntry',
|
'1': 'GrantEntry',
|
||||||
'2': [
|
'2': [
|
||||||
{'1': 'id', '3': 1, '4': 1, '5': 5, '10': 'id'},
|
{'1': 'id', '3': 1, '4': 1, '5': 5, '10': 'id'},
|
||||||
{'1': 'client_id', '3': 2, '4': 1, '5': 5, '10': 'clientId'},
|
{'1': 'wallet_access_id', '3': 2, '4': 1, '5': 5, '10': 'walletAccessId'},
|
||||||
{
|
{
|
||||||
'1': 'shared',
|
'1': 'shared',
|
||||||
'3': 3,
|
'3': 3,
|
||||||
@@ -756,34 +757,34 @@ const GrantEntry$json = {
|
|||||||
|
|
||||||
/// Descriptor for `GrantEntry`. Decode as a `google.protobuf.DescriptorProto`.
|
/// Descriptor for `GrantEntry`. Decode as a `google.protobuf.DescriptorProto`.
|
||||||
final $typed_data.Uint8List grantEntryDescriptor = $convert.base64Decode(
|
final $typed_data.Uint8List grantEntryDescriptor = $convert.base64Decode(
|
||||||
'CgpHcmFudEVudHJ5Eg4KAmlkGAEgASgFUgJpZBIbCgljbGllbnRfaWQYAiABKAVSCGNsaWVudE'
|
'CgpHcmFudEVudHJ5Eg4KAmlkGAEgASgFUgJpZBIoChB3YWxsZXRfYWNjZXNzX2lkGAIgASgFUg'
|
||||||
'lkEjMKBnNoYXJlZBgDIAEoCzIbLmFyYml0ZXIuZXZtLlNoYXJlZFNldHRpbmdzUgZzaGFyZWQS'
|
'53YWxsZXRBY2Nlc3NJZBIzCgZzaGFyZWQYAyABKAsyGy5hcmJpdGVyLmV2bS5TaGFyZWRTZXR0'
|
||||||
'NgoIc3BlY2lmaWMYBCABKAsyGi5hcmJpdGVyLmV2bS5TcGVjaWZpY0dyYW50UghzcGVjaWZpYw'
|
'aW5nc1IGc2hhcmVkEjYKCHNwZWNpZmljGAQgASgLMhouYXJiaXRlci5ldm0uU3BlY2lmaWNHcm'
|
||||||
'==');
|
'FudFIIc3BlY2lmaWM=');
|
||||||
|
|
||||||
@$core.Deprecated('Use evmGrantListRequestDescriptor instead')
|
@$core.Deprecated('Use evmGrantListRequestDescriptor instead')
|
||||||
const EvmGrantListRequest$json = {
|
const EvmGrantListRequest$json = {
|
||||||
'1': 'EvmGrantListRequest',
|
'1': 'EvmGrantListRequest',
|
||||||
'2': [
|
'2': [
|
||||||
{
|
{
|
||||||
'1': 'wallet_id',
|
'1': 'wallet_access_id',
|
||||||
'3': 1,
|
'3': 1,
|
||||||
'4': 1,
|
'4': 1,
|
||||||
'5': 5,
|
'5': 5,
|
||||||
'9': 0,
|
'9': 0,
|
||||||
'10': 'walletId',
|
'10': 'walletAccessId',
|
||||||
'17': true
|
'17': true
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
'8': [
|
'8': [
|
||||||
{'1': '_wallet_id'},
|
{'1': '_wallet_access_id'},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Descriptor for `EvmGrantListRequest`. Decode as a `google.protobuf.DescriptorProto`.
|
/// Descriptor for `EvmGrantListRequest`. Decode as a `google.protobuf.DescriptorProto`.
|
||||||
final $typed_data.Uint8List evmGrantListRequestDescriptor = $convert.base64Decode(
|
final $typed_data.Uint8List evmGrantListRequestDescriptor = $convert.base64Decode(
|
||||||
'ChNFdm1HcmFudExpc3RSZXF1ZXN0EiAKCXdhbGxldF9pZBgBIAEoBUgAUgh3YWxsZXRJZIgBAU'
|
'ChNFdm1HcmFudExpc3RSZXF1ZXN0Ei0KEHdhbGxldF9hY2Nlc3NfaWQYASABKAVIAFIOd2FsbG'
|
||||||
'IMCgpfd2FsbGV0X2lk');
|
'V0QWNjZXNzSWSIAQFCEwoRX3dhbGxldF9hY2Nlc3NfaWQ=');
|
||||||
|
|
||||||
@$core.Deprecated('Use evmGrantListResponseDescriptor instead')
|
@$core.Deprecated('Use evmGrantListResponseDescriptor instead')
|
||||||
const EvmGrantListResponse$json = {
|
const EvmGrantListResponse$json = {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user