1 Commits

Author SHA1 Message Date
CleverWild
efb11d2271 refactor(arbiter-client): rewrite errors to terrros 2026-03-24 17:25:45 +01:00
159 changed files with 4205 additions and 13285 deletions

View File

@@ -1,11 +0,0 @@
---
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).

1
.gitignore vendored
View File

@@ -3,4 +3,3 @@ scripts/__pycache__/
.DS_Store .DS_Store
.cargo/config.toml .cargo/config.toml
.vscode/ .vscode/
docs/

View File

@@ -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.
- **`FlowCoordinator`** — Coordinates cross-connection flow between user agents and SDK clients. - **`MessageRouter`** — Coordinates streaming messages 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.

View File

@@ -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.
- **`FlowCoordinator`** — Coordinates cross-connection flow between user agents and SDK clients. - **`MessageRouter`** — Coordinates streaming messages 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.

File diff suppressed because it is too large Load Diff

View File

@@ -1,821 +0,0 @@
# 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
```

View File

@@ -1,170 +0,0 @@
# 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 |

View File

@@ -5,15 +5,8 @@ package arbiter.client;
import "evm.proto"; import "evm.proto";
import "google/protobuf/empty.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 {

View File

@@ -12,8 +12,7 @@ enum EvmError {
} }
message WalletEntry { message WalletEntry {
int32 id = 1; bytes address = 1; // 20-byte Ethereum address
bytes address = 2; // 20-byte Ethereum address
} }
message WalletList { message WalletList {
@@ -47,7 +46,7 @@ message VolumeRateLimit {
} }
message SharedSettings { message SharedSettings {
int32 wallet_access_id = 1; int32 wallet_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;
@@ -140,8 +139,9 @@ message TransactionEvalError {
// --- UserAgent grant management --- // --- UserAgent grant management ---
message EvmGrantCreateRequest { message EvmGrantCreateRequest {
SharedSettings shared = 1; int32 client_id = 1;
SpecificGrant specific = 2; SharedSettings shared = 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 wallet_access_id = 2; int32 client_id = 2;
SharedSettings shared = 3; SharedSettings shared = 3;
SpecificGrant specific = 4; SpecificGrant specific = 4;
} }
message EvmGrantListRequest { message EvmGrantListRequest {
optional int32 wallet_access_id = 1; optional int32 wallet_id = 1;
} }
message EvmGrantListResponse { message EvmGrantListResponse {

View File

@@ -2,7 +2,6 @@ syntax = "proto3";
package arbiter.user_agent; package arbiter.user_agent;
import "client.proto";
import "evm.proto"; import "evm.proto";
import "google/protobuf/empty.proto"; import "google/protobuf/empty.proto";
@@ -23,6 +22,10 @@ enum SdkClientError {
SDK_CLIENT_ERROR_INTERNAL = 4; SDK_CLIENT_ERROR_INTERNAL = 4;
} }
message SdkClientApproveRequest {
bytes pubkey = 1; // 32-byte ed25519 public key
}
message SdkClientRevokeRequest { message SdkClientRevokeRequest {
int32 client_id = 1; int32 client_id = 1;
} }
@@ -30,14 +33,20 @@ message SdkClientRevokeRequest {
message SdkClientEntry { message SdkClientEntry {
int32 id = 1; int32 id = 1;
bytes pubkey = 2; bytes pubkey = 2;
arbiter.client.ClientInfo info = 3; int32 created_at = 3;
int32 created_at = 4;
} }
message SdkClientList { message SdkClientList {
repeated SdkClientEntry clients = 1; repeated SdkClientEntry clients = 1;
} }
message SdkClientApproveResponse {
oneof result {
SdkClientEntry client = 1;
SdkClientError error = 2;
}
}
message SdkClientRevokeResponse { message SdkClientRevokeResponse {
oneof result { oneof result {
google.protobuf.Empty ok = 1; google.protobuf.Empty ok = 1;
@@ -120,39 +129,13 @@ enum VaultState {
message SdkClientConnectionRequest { message SdkClientConnectionRequest {
bytes pubkey = 1; bytes pubkey = 1;
arbiter.client.ClientInfo info = 2;
} }
message SdkClientConnectionResponse { message SdkClientConnectionResponse {
bool approved = 1; bool approved = 1;
bytes pubkey = 2;
} }
message SdkClientConnectionCancel { message SdkClientConnectionCancel {}
bytes pubkey = 1;
}
message WalletAccess {
int32 wallet_id = 1;
int32 sdk_client_id = 2;
}
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; int32 id = 16;
@@ -168,12 +151,10 @@ message UserAgentRequest {
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;
SdkClientConnectionResponse sdk_client_connection_response = 11; SdkClientConnectionResponse sdk_client_connection_response = 11;
SdkClientRevokeRequest sdk_client_revoke = 12; SdkClientApproveRequest sdk_client_approve = 12;
google.protobuf.Empty sdk_client_list = 13; SdkClientRevokeRequest sdk_client_revoke = 13;
BootstrapEncryptedKey bootstrap_encrypted_key = 14; google.protobuf.Empty sdk_client_list = 14;
SdkClientGrantWalletAccess grant_wallet_access = 15; BootstrapEncryptedKey bootstrap_encrypted_key = 15;
SdkClientRevokeWalletAccess revoke_wallet_access = 17;
google.protobuf.Empty list_wallet_access = 18;
} }
} }
message UserAgentResponse { message UserAgentResponse {
@@ -189,11 +170,10 @@ 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;
SdkClientConnectionRequest sdk_client_connection_request = 11; SdkClientConnectionResponse sdk_client_connection_response = 11;
SdkClientConnectionCancel sdk_client_connection_cancel = 12; SdkClientApproveResponse sdk_client_approve_response = 12;
SdkClientRevokeResponse sdk_client_revoke_response = 13; SdkClientRevokeResponse sdk_client_revoke_response = 13;
SdkClientListResponse sdk_client_list_response = 14; SdkClientListResponse sdk_client_list_response = 14;
BootstrapResult bootstrap_result = 15; BootstrapResult bootstrap_result = 15;
ListWalletAccessResponse list_wallet_access_response = 17;
} }
} }

7
server/Cargo.lock generated
View File

@@ -686,6 +686,7 @@ dependencies = [
"http", "http",
"rand 0.10.0", "rand 0.10.0",
"rustls-webpki", "rustls-webpki",
"terrors",
"thiserror 2.0.18", "thiserror 2.0.18",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
@@ -737,7 +738,6 @@ dependencies = [
"ed25519-dalek", "ed25519-dalek",
"fatality", "fatality",
"futures", "futures",
"hmac",
"insta", "insta",
"k256", "k256",
"kameo", "kameo",
@@ -4909,6 +4909,11 @@ dependencies = [
"windows-sys 0.60.2", "windows-sys 0.60.2",
] ]
[[package]]
name = "terrors"
version = "0.5.1"
source = "git+https://github.com/CleverWild/terrors#a0867fd9ca3fbb44c32e92113a917f1577b5716a"
[[package]] [[package]]
name = "test-log" name = "test-log"
version = "0.2.19" version = "0.2.19"

View File

@@ -1,7 +1,5 @@
[workspace] [workspace]
members = [ members = ["crates/*"]
"crates/*",
]
resolver = "3" resolver = "3"
[workspace.lints.clippy] [workspace.lints.clippy]
@@ -43,3 +41,4 @@ k256 = { version = "0.13.4", features = ["ecdsa", "pkcs8"] }
rsa = { version = "0.9", features = ["sha2"] } rsa = { version = "0.9", features = ["sha2"] }
sha2 = "0.10" sha2 = "0.10"
spki = "0.7" spki = "0.7"
terrors = { version = "0.5", git = "https://github.com/CleverWild/terrors" }

View File

@@ -24,3 +24,4 @@ http = "1.4.0"
rustls-webpki = { version = "0.103.10", features = ["aws-lc-rs"] } rustls-webpki = { version = "0.103.10", features = ["aws-lc-rs"] }
async-trait.workspace = true async-trait.workspace = true
rand.workspace = true rand.workspace = true
terrors.workspace = true

View File

@@ -1,84 +1,53 @@
use arbiter_proto::{ use arbiter_proto::{
ClientMetadata, format_challenge, format_challenge,
proto::client::{ proto::client::{
AuthChallengeRequest, AuthChallengeSolution, AuthResult, ClientInfo as ProtoClientInfo, AuthChallengeRequest, AuthChallengeSolution, AuthResult, ClientRequest,
ClientRequest, client_request::Payload as ClientRequestPayload, client_request::Payload as ClientRequestPayload,
client_response::Payload as ClientResponsePayload, client_response::Payload as ClientResponsePayload,
}, },
}; };
use ed25519_dalek::Signer as _; use ed25519_dalek::Signer as _;
use terrors::OneOf;
use crate::{ use crate::{
storage::StorageError, errors::{
ConnectError, MissingAuthChallengeError, UnexpectedAuthResponseError, map_auth_code_error,
},
transport::{ClientTransport, next_request_id}, 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( async fn send_auth_challenge_request(
transport: &mut ClientTransport, transport: &mut ClientTransport,
metadata: ClientMetadata,
key: &ed25519_dalek::SigningKey, key: &ed25519_dalek::SigningKey,
) -> std::result::Result<(), AuthError> { ) -> std::result::Result<(), ConnectError> {
transport transport
.send(ClientRequest { .send(ClientRequest {
request_id: next_request_id(), request_id: next_request_id(),
payload: Some(ClientRequestPayload::AuthChallengeRequest( payload: Some(ClientRequestPayload::AuthChallengeRequest(
AuthChallengeRequest { AuthChallengeRequest {
pubkey: key.verifying_key().to_bytes().to_vec(), pubkey: key.verifying_key().to_bytes().to_vec(),
client_info: Some(ProtoClientInfo {
name: metadata.name,
description: metadata.description,
version: metadata.version,
}),
}, },
)), )),
}) })
.await .await
.map_err(|_| AuthError::UnexpectedAuthResponse) .map_err(|_| OneOf::new(UnexpectedAuthResponseError))
} }
async fn receive_auth_challenge( async fn receive_auth_challenge(
transport: &mut ClientTransport, transport: &mut ClientTransport,
) -> std::result::Result<arbiter_proto::proto::client::AuthChallenge, AuthError> { ) -> std::result::Result<arbiter_proto::proto::client::AuthChallenge, ConnectError> {
let response = transport let response = transport
.recv() .recv()
.await .await
.map_err(|_| AuthError::MissingAuthChallenge)?; .map_err(|_| OneOf::new(MissingAuthChallengeError))?;
let payload = response.payload.ok_or(AuthError::MissingAuthChallenge)?; let payload = response
.payload
.ok_or_else(|| OneOf::new(MissingAuthChallengeError))?;
match payload { match payload {
ClientResponsePayload::AuthChallenge(challenge) => Ok(challenge), ClientResponsePayload::AuthChallenge(challenge) => Ok(challenge),
ClientResponsePayload::AuthResult(result) => Err(map_auth_result(result)), ClientResponsePayload::AuthResult(result) => Err(map_auth_code_error(result)),
_ => Err(AuthError::UnexpectedAuthResponse), _ => Err(OneOf::new(UnexpectedAuthResponseError)),
} }
} }
@@ -86,7 +55,7 @@ async fn send_auth_challenge_solution(
transport: &mut ClientTransport, transport: &mut ClientTransport,
key: &ed25519_dalek::SigningKey, key: &ed25519_dalek::SigningKey,
challenge: arbiter_proto::proto::client::AuthChallenge, challenge: arbiter_proto::proto::client::AuthChallenge,
) -> std::result::Result<(), AuthError> { ) -> std::result::Result<(), ConnectError> {
let challenge_payload = format_challenge(challenge.nonce, &challenge.pubkey); let challenge_payload = format_challenge(challenge.nonce, &challenge.pubkey);
let signature = key.sign(&challenge_payload).to_bytes().to_vec(); let signature = key.sign(&challenge_payload).to_bytes().to_vec();
@@ -98,37 +67,36 @@ async fn send_auth_challenge_solution(
)), )),
}) })
.await .await
.map_err(|_| AuthError::UnexpectedAuthResponse) .map_err(|_| OneOf::new(UnexpectedAuthResponseError))
} }
async fn receive_auth_confirmation( async fn receive_auth_confirmation(
transport: &mut ClientTransport, transport: &mut ClientTransport,
) -> std::result::Result<(), AuthError> { ) -> std::result::Result<(), ConnectError> {
let response = transport let response = transport
.recv() .recv()
.await .await
.map_err(|_| AuthError::UnexpectedAuthResponse)?; .map_err(|_| OneOf::new(UnexpectedAuthResponseError))?;
let payload = response let payload = response
.payload .payload
.ok_or(AuthError::UnexpectedAuthResponse)?; .ok_or_else(|| OneOf::new(UnexpectedAuthResponseError))?;
match payload { match payload {
ClientResponsePayload::AuthResult(result) ClientResponsePayload::AuthResult(result)
if AuthResult::try_from(result).ok() == Some(AuthResult::Success) => if AuthResult::try_from(result).ok() == Some(AuthResult::Success) =>
{ {
Ok(()) Ok(())
} }
ClientResponsePayload::AuthResult(result) => Err(map_auth_result(result)), ClientResponsePayload::AuthResult(result) => Err(map_auth_code_error(result)),
_ => Err(AuthError::UnexpectedAuthResponse), _ => Err(OneOf::new(UnexpectedAuthResponseError)),
} }
} }
pub(crate) async fn authenticate( pub(crate) async fn authenticate(
transport: &mut ClientTransport, transport: &mut ClientTransport,
metadata: ClientMetadata,
key: &ed25519_dalek::SigningKey, key: &ed25519_dalek::SigningKey,
) -> std::result::Result<(), AuthError> { ) -> std::result::Result<(), ConnectError> {
send_auth_challenge_request(transport, metadata, key).await?; send_auth_challenge_request(transport, key).await?;
let challenge = receive_auth_challenge(transport).await?; let challenge = receive_auth_challenge(transport).await?;
send_auth_challenge_solution(transport, key, challenge).await?; send_auth_challenge_solution(transport, key, challenge).await?;
receive_auth_confirmation(transport).await receive_auth_confirmation(transport).await

View File

@@ -1,48 +0,0 @@
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),
}
}

View File

@@ -1,81 +1,73 @@
use arbiter_proto::{ClientMetadata, proto::arbiter_service_client::ArbiterServiceClient, url::ArbiterUrl}; use arbiter_proto::{proto::arbiter_service_client::ArbiterServiceClient, url::ArbiterUrl};
use std::sync::Arc; use std::sync::Arc;
use terrors::{Broaden as _, OneOf};
use tokio::sync::{Mutex, mpsc}; use tokio::sync::{Mutex, mpsc};
use tokio_stream::wrappers::ReceiverStream; use tokio_stream::wrappers::ReceiverStream;
use tonic::transport::ClientTlsConfig; use tonic::transport::ClientTlsConfig;
use crate::{ use crate::{
StorageError, auth::{AuthError, authenticate}, storage::{FileSigningKeyStorage, SigningKeyStorage}, transport::{BUFFER_LENGTH, ClientTransport} auth::authenticate,
errors::ConnectError,
storage::{FileSigningKeyStorage, SigningKeyStorage},
transport::{BUFFER_LENGTH, ClientTransport},
}; };
#[cfg(feature = "evm")]
use crate::errors::{ClientConnectionClosedError, ClientError};
#[cfg(feature = "evm")] #[cfg(feature = "evm")]
use crate::wallets::evm::ArbiterEvmWallet; 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 { pub struct ArbiterClient {
#[allow(dead_code)] #[allow(dead_code)]
transport: Arc<Mutex<ClientTransport>>, transport: Arc<Mutex<ClientTransport>>,
} }
impl ArbiterClient { impl ArbiterClient {
pub async fn connect(url: ArbiterUrl, metadata: ClientMetadata) -> Result<Self, Error> { pub async fn connect(url: ArbiterUrl) -> Result<Self, ConnectError> {
let storage = FileSigningKeyStorage::from_default_location()?; let storage = FileSigningKeyStorage::from_default_location().broaden()?;
Self::connect_with_storage(url, metadata, &storage).await Self::connect_with_storage(url, &storage).await
} }
pub async fn connect_with_storage<S: SigningKeyStorage>( pub async fn connect_with_storage<S: SigningKeyStorage>(
url: ArbiterUrl, url: ArbiterUrl,
metadata: ClientMetadata,
storage: &S, storage: &S,
) -> Result<Self, Error> { ) -> Result<Self, ConnectError> {
let key = storage.load_or_create()?; let key = storage.load_or_create().broaden()?;
Self::connect_with_key(url, metadata, key).await Self::connect_with_key(url, key).await
} }
pub async fn connect_with_key( pub async fn connect_with_key(
url: ArbiterUrl, url: ArbiterUrl,
metadata: ClientMetadata,
key: ed25519_dalek::SigningKey, key: ed25519_dalek::SigningKey,
) -> Result<Self, Error> { ) -> Result<Self, ConnectError> {
let anchor = webpki::anchor_from_trusted_cert(&url.ca_cert)?.to_owned(); let anchor = webpki::anchor_from_trusted_cert(&url.ca_cert)
.map_err(OneOf::new)?
.to_owned();
let tls = ClientTlsConfig::new().trust_anchor(anchor); let tls = ClientTlsConfig::new().trust_anchor(anchor);
let channel = tonic::transport::Channel::from_shared(format!("https://{}:{}", url.host, url.port))? let channel = tonic::transport::Channel::from_shared(format!("{}:{}", url.host, url.port))
.tls_config(tls)? .map_err(OneOf::new)?
.tls_config(tls)
.map_err(OneOf::new)?
.connect() .connect()
.await?; .await
.map_err(OneOf::new)?;
let mut client = ArbiterServiceClient::new(channel); let mut client = ArbiterServiceClient::new(channel);
let (tx, rx) = mpsc::channel(BUFFER_LENGTH); let (tx, rx) = mpsc::channel(BUFFER_LENGTH);
let response_stream = client.client(ReceiverStream::new(rx)).await?.into_inner(); let response_stream = client
.client(ReceiverStream::new(rx))
.await
.map_err(OneOf::new)?
.into_inner();
let mut transport = ClientTransport { let mut transport = ClientTransport {
sender: tx, sender: tx,
receiver: response_stream, receiver: response_stream,
}; };
authenticate(&mut transport, metadata, &key).await?; authenticate(&mut transport, &key).await?;
Ok(Self { Ok(Self {
transport: Arc::new(Mutex::new(transport)), transport: Arc::new(Mutex::new(transport)),
@@ -83,7 +75,8 @@ impl ArbiterClient {
} }
#[cfg(feature = "evm")] #[cfg(feature = "evm")]
pub async fn evm_wallets(&self) -> Result<Vec<ArbiterEvmWallet>, Error> { pub async fn evm_wallets(&self) -> Result<Vec<ArbiterEvmWallet>, ClientError> {
todo!("fetch EVM wallet list from server") let _ = &self.transport;
Err(OneOf::new(ClientConnectionClosedError))
} }
} }

View File

@@ -0,0 +1,127 @@
use terrors::OneOf;
use thiserror::Error;
#[cfg(feature = "evm")]
use alloy::{primitives::ChainId, signers::Error as AlloySignerError};
pub type StorageError = OneOf<(std::io::Error, InvalidKeyLengthError)>;
pub type ConnectError = OneOf<(
tonic::transport::Error,
http::uri::InvalidUri,
webpki::Error,
tonic::Status,
MissingAuthChallengeError,
ApprovalDeniedError,
NoUserAgentsOnlineError,
UnexpectedAuthResponseError,
std::io::Error,
InvalidKeyLengthError,
)>;
pub type ClientError = OneOf<(tonic::Status, ClientConnectionClosedError)>;
pub(crate) type ClientTransportError =
OneOf<(TransportChannelClosedError, TransportConnectionClosedError)>;
#[cfg(feature = "evm")]
pub(crate) type EvmWalletError = OneOf<(
EvmChainIdMismatchError,
EvmHashSigningUnsupportedError,
EvmTransactionSigningUnsupportedError,
)>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[error("Invalid signing key length in storage: expected {expected} bytes, got {actual} bytes")]
pub struct InvalidKeyLengthError {
pub expected: usize,
pub actual: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[error("Auth challenge was not returned by server")]
pub struct MissingAuthChallengeError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[error("Client approval denied by User Agent")]
pub struct ApprovalDeniedError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[error("No User Agents online to approve client")]
pub struct NoUserAgentsOnlineError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[error("Unexpected auth response payload")]
pub struct UnexpectedAuthResponseError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[error("Connection closed by server")]
pub struct ClientConnectionClosedError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[error("Transport channel closed")]
pub struct TransportChannelClosedError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[error("Connection closed by server")]
pub struct TransportConnectionClosedError;
#[cfg(feature = "evm")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[error("Transaction chain id mismatch: signer {signer}, tx {tx}")]
pub struct EvmChainIdMismatchError {
pub signer: ChainId,
pub tx: ChainId,
}
#[cfg(feature = "evm")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[error("hash-only signing is not supported for ArbiterEvmWallet; use transaction signing")]
pub struct EvmHashSigningUnsupportedError;
#[cfg(feature = "evm")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[error("transaction signing is not supported by current arbiter.client protocol")]
pub struct EvmTransactionSigningUnsupportedError;
pub(crate) fn map_auth_code_error(code: i32) -> ConnectError {
use arbiter_proto::proto::client::AuthResult;
match AuthResult::try_from(code).unwrap_or(AuthResult::Unspecified) {
AuthResult::ApprovalDenied => OneOf::new(ApprovalDeniedError),
AuthResult::NoUserAgentsOnline => OneOf::new(NoUserAgentsOnlineError),
AuthResult::Unspecified
| AuthResult::Success
| AuthResult::InvalidKey
| AuthResult::InvalidSignature
| AuthResult::Internal => OneOf::new(UnexpectedAuthResponseError),
}
}
#[cfg(feature = "evm")]
impl From<EvmChainIdMismatchError> for AlloySignerError {
fn from(value: EvmChainIdMismatchError) -> Self {
AlloySignerError::TransactionChainIdMismatch {
signer: value.signer,
tx: value.tx,
}
}
}
#[cfg(feature = "evm")]
impl From<EvmHashSigningUnsupportedError> for AlloySignerError {
fn from(_value: EvmHashSigningUnsupportedError) -> Self {
AlloySignerError::other(
"hash-only signing is not supported for ArbiterEvmWallet; use transaction signing",
)
}
}
#[cfg(feature = "evm")]
impl From<EvmTransactionSigningUnsupportedError> for AlloySignerError {
fn from(_value: EvmTransactionSigningUnsupportedError) -> Self {
AlloySignerError::other(
"transaction signing is not supported by current arbiter.client protocol",
)
}
}

View File

@@ -1,12 +1,13 @@
mod auth; mod auth;
mod client; mod client;
mod errors;
mod storage; mod storage;
mod transport; mod transport;
pub mod wallets; pub mod wallets;
pub use auth::AuthError; pub use client::ArbiterClient;
pub use client::{ArbiterClient, Error}; pub use errors::{ClientError, ConnectError, StorageError};
pub use storage::{FileSigningKeyStorage, SigningKeyStorage, StorageError}; pub use storage::{FileSigningKeyStorage, SigningKeyStorage};
#[cfg(feature = "evm")] #[cfg(feature = "evm")]
pub use wallets::evm::ArbiterEvmWallet; pub use wallets::evm::ArbiterEvmWallet;

View File

@@ -1,14 +1,8 @@
use arbiter_proto::home_path; use arbiter_proto::home_path;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use terrors::OneOf;
#[derive(Debug, thiserror::Error)] use crate::errors::{InvalidKeyLengthError, StorageError};
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 { pub trait SigningKeyStorage {
fn load_or_create(&self) -> std::result::Result<ed25519_dalek::SigningKey, StorageError>; fn load_or_create(&self) -> std::result::Result<ed25519_dalek::SigningKey, StorageError>;
@@ -27,17 +21,20 @@ impl FileSigningKeyStorage {
} }
pub fn from_default_location() -> std::result::Result<Self, StorageError> { pub fn from_default_location() -> std::result::Result<Self, StorageError> {
Ok(Self::new(home_path()?.join(Self::DEFAULT_FILE_NAME))) Ok(Self::new(
home_path()
.map_err(OneOf::new)?
.join(Self::DEFAULT_FILE_NAME),
))
} }
fn read_key(path: &Path) -> std::result::Result<ed25519_dalek::SigningKey, StorageError> { fn read_key(path: &Path) -> std::result::Result<ed25519_dalek::SigningKey, StorageError> {
let bytes = std::fs::read(path)?; let bytes = std::fs::read(path).map_err(OneOf::new)?;
let raw: [u8; 32] = let raw: [u8; 32] = bytes.try_into().map_err(|v: Vec<u8>| {
bytes OneOf::new(InvalidKeyLengthError {
.try_into()
.map_err(|v: Vec<u8>| StorageError::InvalidKeyLength {
expected: 32, expected: 32,
actual: v.len(), actual: v.len(),
})
})?; })?;
Ok(ed25519_dalek::SigningKey::from_bytes(&raw)) Ok(ed25519_dalek::SigningKey::from_bytes(&raw))
} }
@@ -46,7 +43,7 @@ impl FileSigningKeyStorage {
impl SigningKeyStorage for FileSigningKeyStorage { impl SigningKeyStorage for FileSigningKeyStorage {
fn load_or_create(&self) -> std::result::Result<ed25519_dalek::SigningKey, StorageError> { fn load_or_create(&self) -> std::result::Result<ed25519_dalek::SigningKey, StorageError> {
if let Some(parent) = self.path.parent() { if let Some(parent) = self.path.parent() {
std::fs::create_dir_all(parent)?; std::fs::create_dir_all(parent).map_err(OneOf::new)?;
} }
if self.path.exists() { if self.path.exists() {
@@ -64,20 +61,21 @@ impl SigningKeyStorage for FileSigningKeyStorage {
{ {
Ok(mut file) => { Ok(mut file) => {
use std::io::Write as _; use std::io::Write as _;
file.write_all(&raw_key)?; file.write_all(&raw_key).map_err(OneOf::new)?;
Ok(key) Ok(key)
} }
Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => { Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
Self::read_key(&self.path) Self::read_key(&self.path)
} }
Err(err) => Err(StorageError::Io(err)), Err(err) => Err(OneOf::new(err)),
} }
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{FileSigningKeyStorage, SigningKeyStorage, StorageError}; use super::{FileSigningKeyStorage, SigningKeyStorage};
use crate::errors::InvalidKeyLengthError;
fn unique_temp_key_path() -> std::path::PathBuf { fn unique_temp_key_path() -> std::path::PathBuf {
let nanos = std::time::SystemTime::now() let nanos = std::time::SystemTime::now()
@@ -119,12 +117,12 @@ mod tests {
.load_or_create() .load_or_create()
.expect_err("storage should reject non-32-byte key file"); .expect_err("storage should reject non-32-byte key file");
match err { match err.narrow::<InvalidKeyLengthError, _>() {
StorageError::InvalidKeyLength { expected, actual } => { Ok(invalid_len) => {
assert_eq!(expected, 32); assert_eq!(invalid_len.expected, 32);
assert_eq!(actual, 31); assert_eq!(invalid_len.actual, 31);
} }
other => panic!("unexpected error: {other:?}"), Err(other) => panic!("unexpected io error: {other:?}"),
} }
std::fs::remove_file(path).expect("temp key file should be removable"); std::fs::remove_file(path).expect("temp key file should be removable");

View File

@@ -1,9 +1,12 @@
use arbiter_proto::proto::{ use arbiter_proto::proto::client::{ClientRequest, ClientResponse};
client::{ClientRequest, ClientResponse},
};
use std::sync::atomic::{AtomicI32, Ordering}; use std::sync::atomic::{AtomicI32, Ordering};
use terrors::OneOf;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use crate::errors::{
ClientTransportError, TransportChannelClosedError, TransportConnectionClosedError,
};
pub(crate) const BUFFER_LENGTH: usize = 16; pub(crate) const BUFFER_LENGTH: usize = 16;
static NEXT_REQUEST_ID: AtomicI32 = AtomicI32::new(1); static NEXT_REQUEST_ID: AtomicI32 = AtomicI32::new(1);
@@ -11,15 +14,6 @@ pub(crate) fn next_request_id() -> i32 {
NEXT_REQUEST_ID.fetch_add(1, Ordering::Relaxed) 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) struct ClientTransport {
pub(crate) sender: mpsc::Sender<ClientRequest>, pub(crate) sender: mpsc::Sender<ClientRequest>,
pub(crate) receiver: tonic::Streaming<ClientResponse>, pub(crate) receiver: tonic::Streaming<ClientResponse>,
@@ -29,20 +23,20 @@ impl ClientTransport {
pub(crate) async fn send( pub(crate) async fn send(
&mut self, &mut self,
request: ClientRequest, request: ClientRequest,
) -> std::result::Result<(), ClientSignError> { ) -> std::result::Result<(), ClientTransportError> {
self.sender self.sender
.send(request) .send(request)
.await .await
.map_err(|_| ClientSignError::ChannelClosed) .map_err(|_| OneOf::new(TransportChannelClosedError))
} }
pub(crate) async fn recv( pub(crate) async fn recv(
&mut self, &mut self,
) -> std::result::Result<ClientResponse, ClientSignError> { ) -> std::result::Result<ClientResponse, ClientTransportError> {
match self.receiver.message().await { match self.receiver.message().await {
Ok(Some(resp)) => Ok(resp), Ok(Some(resp)) => Ok(resp),
Ok(None) => Err(ClientSignError::ConnectionClosed), Ok(None) => Err(OneOf::new(TransportConnectionClosedError)),
Err(_) => Err(ClientSignError::ConnectionClosed), Err(_) => Err(OneOf::new(TransportConnectionClosedError)),
} }
} }
} }

View File

@@ -2,13 +2,20 @@ use alloy::{
consensus::SignableTransaction, consensus::SignableTransaction,
network::TxSigner, network::TxSigner,
primitives::{Address, B256, ChainId, Signature}, primitives::{Address, B256, ChainId, Signature},
signers::{Error, Result, Signer}, signers::{Result, Signer},
}; };
use async_trait::async_trait; use async_trait::async_trait;
use std::sync::Arc; use std::sync::Arc;
use terrors::OneOf;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use crate::transport::ClientTransport; use crate::{
errors::{
EvmChainIdMismatchError, EvmHashSigningUnsupportedError,
EvmTransactionSigningUnsupportedError, EvmWalletError,
},
transport::ClientTransport,
};
pub struct ArbiterEvmWallet { pub struct ArbiterEvmWallet {
transport: Arc<Mutex<ClientTransport>>, transport: Arc<Mutex<ClientTransport>>,
@@ -17,6 +24,7 @@ pub struct ArbiterEvmWallet {
} }
impl ArbiterEvmWallet { impl ArbiterEvmWallet {
#[allow(dead_code)]
pub(crate) fn new(transport: Arc<Mutex<ClientTransport>>, address: Address) -> Self { pub(crate) fn new(transport: Arc<Mutex<ClientTransport>>, address: Address) -> Self {
Self { Self {
transport, transport,
@@ -34,14 +42,17 @@ impl ArbiterEvmWallet {
self self
} }
fn validate_chain_id(&self, tx: &mut dyn SignableTransaction<Signature>) -> Result<()> { fn validate_chain_id(
&self,
tx: &mut dyn SignableTransaction<Signature>,
) -> std::result::Result<(), EvmWalletError> {
if let Some(chain_id) = self.chain_id if let Some(chain_id) = self.chain_id
&& !tx.set_chain_id_checked(chain_id) && !tx.set_chain_id_checked(chain_id)
{ {
return Err(Error::TransactionChainIdMismatch { return Err(OneOf::new(EvmChainIdMismatchError {
signer: chain_id, signer: chain_id,
tx: tx.chain_id().unwrap(), tx: tx.chain_id().unwrap(),
}); }));
} }
Ok(()) Ok(())
@@ -51,9 +62,7 @@ impl ArbiterEvmWallet {
#[async_trait] #[async_trait]
impl Signer for ArbiterEvmWallet { impl Signer for ArbiterEvmWallet {
async fn sign_hash(&self, _hash: &B256) -> Result<Signature> { async fn sign_hash(&self, _hash: &B256) -> Result<Signature> {
Err(Error::other( Err(EvmWalletError::new(EvmHashSigningUnsupportedError).into())
"hash-only signing is not supported for ArbiterEvmWallet; use transaction signing",
))
} }
fn address(&self) -> Address { fn address(&self) -> Address {
@@ -80,10 +89,9 @@ impl TxSigner<Signature> for ArbiterEvmWallet {
tx: &mut dyn SignableTransaction<Signature>, tx: &mut dyn SignableTransaction<Signature>,
) -> Result<Signature> { ) -> Result<Signature> {
let _transport = self.transport.lock().await; let _transport = self.transport.lock().await;
self.validate_chain_id(tx)?; self.validate_chain_id(tx)
.map_err(OneOf::into::<alloy::signers::Error>)?;
Err(Error::other( Err(EvmWalletError::new(EvmTransactionSigningUnsupportedError).into())
"transaction signing is not supported by current arbiter.client protocol",
))
} }
} }

View File

@@ -1,21 +1,32 @@
use std::path::PathBuf;
use tonic_prost_build::configure; use tonic_prost_build::configure;
static PROTOBUF_DIR: &str = "../../../protobufs"; static PROTOBUF_DIR: &str = "../../../protobufs";
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("cargo::rerun-if-changed={PROTOBUF_DIR}"); let manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR")?);
let protobuf_dir = manifest_dir.join(PROTOBUF_DIR);
let protoc_include = protoc_bin_vendored::include_path()?;
let protoc_path = protoc_bin_vendored::protoc_bin_path()?;
unsafe {
std::env::set_var("PROTOC", &protoc_path);
std::env::set_var("PROTOC_INCLUDE", &protoc_include);
}
println!("cargo::rerun-if-changed={}", protobuf_dir.display());
configure() configure()
.message_attribute(".", "#[derive(::kameo::Reply)]") .message_attribute(".", "#[derive(::kameo::Reply)]")
.compile_well_known_types(true)
.compile_protos( .compile_protos(
&[ &[
format!("{}/arbiter.proto", PROTOBUF_DIR), protobuf_dir.join("arbiter.proto"),
format!("{}/user_agent.proto", PROTOBUF_DIR), protobuf_dir.join("user_agent.proto"),
format!("{}/client.proto", PROTOBUF_DIR), protobuf_dir.join("client.proto"),
format!("{}/evm.proto", PROTOBUF_DIR), protobuf_dir.join("evm.proto"),
], ],
&[PROTOBUF_DIR.to_string()], &[protobuf_dir],
) )?;
.unwrap();
Ok(()) Ok(())
} }

View File

@@ -3,6 +3,12 @@ pub mod url;
use base64::{Engine, prelude::BASE64_STANDARD}; use base64::{Engine, prelude::BASE64_STANDARD};
pub mod google {
pub mod protobuf {
tonic::include_proto!("google.protobuf");
}
}
pub mod proto { pub mod proto {
tonic::include_proto!("arbiter"); tonic::include_proto!("arbiter");
@@ -19,13 +25,6 @@ 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 fn home_path() -> Result<std::path::PathBuf, std::io::Error> { pub fn home_path() -> Result<std::path::PathBuf, std::io::Error> {

View File

@@ -7,8 +7,6 @@ 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,

View File

@@ -49,7 +49,6 @@ pem = "3.0.6"
k256.workspace = true k256.workspace = true
rsa.workspace = true rsa.workspace = true
sha2.workspace = true sha2.workspace = true
hmac = "0.12"
spki.workspace = true spki.workspace = true
alloy.workspace = true alloy.workspace = true
prost-types.workspace = true prost-types.workspace = true

View File

@@ -40,52 +40,25 @@ 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; insert into arbiter_settings (id) values (1) on conflict do nothing; -- ensure singleton row exists
-- 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,
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,
pubkey_integrity_tag blob,
key_type integer not null default(1), -- 1=Ed25519, 2=ECDSA(secp256k1) key_type integer not null default(1), -- 1=Ed25519, 2=ECDSA(secp256k1)
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
@@ -94,18 +67,8 @@ 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
@@ -115,7 +78,8 @@ 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_access_id integer not null references evm_wallet_access (id) on delete restrict, wallet_id integer not null references evm_wallet(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
@@ -130,14 +94,15 @@ create table if not exists evm_basic_grant (
-- 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,
wallet_access_id integer not null references evm_wallet_access (id) on delete restrict,
grant_id integer not null references evm_basic_grant(id) on delete restrict, grant_id integer not null references evm_basic_grant(id) on delete restrict,
client_id integer not null references program_client(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_access_chain on evm_basic_grant (wallet_access_id, chain_id); create index if not exists idx_evm_basic_grant_wallet_chain on evm_basic_grant(client_id, wallet_id, chain_id);
-- =============================== -- ===============================
-- ERC20 token transfer grant -- ERC20 token transfer grant
@@ -170,11 +135,10 @@ create table if not exists evm_token_transfer_log (
) 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_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_chain on evm_token_transfer_log(chain_id);
-- =============================== -- ===============================
-- Ether transfer grant (uses base log) -- Ether transfer grant (uses base log)
-- =============================== -- ===============================
@@ -192,3 +156,6 @@ create table if not exists evm_ether_transfer_grant_target (
) 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);
CREATE UNIQUE INDEX program_client_public_key_unique
ON program_client (public_key);

View File

@@ -1,10 +1,9 @@
use arbiter_proto::{ use arbiter_proto::{
ClientMetadata, format_challenge, transport::{Bi, expect_message} format_challenge,
transport::{Bi, expect_message},
}; };
use chrono::Utc;
use diesel::{ use diesel::{
ExpressionMethods as _, OptionalExtension as _, QueryDsl as _, SelectableHelper as _, ExpressionMethods as _, OptionalExtension as _, QueryDsl as _, dsl::insert_into, update,
dsl::insert_into, update,
}; };
use diesel_async::RunQueryDsl as _; use diesel_async::RunQueryDsl as _;
use ed25519_dalek::{Signature, VerifyingKey}; use ed25519_dalek::{Signature, VerifyingKey};
@@ -13,14 +12,10 @@ use tracing::error;
use crate::{ use crate::{
actors::{ actors::{
client::{ClientConnection, ClientProfile}, client::ClientConnection,
flow_coordinator::{self, RequestClientApproval}, router::{self, RequestClientApproval},
},
db::{
self,
models::{ProgramClientMetadata, SqliteTimestamp},
schema::program_client,
}, },
db::{self, schema::program_client},
}; };
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)] #[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
@@ -44,18 +39,13 @@ 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(flow_coordinator::ApprovalError), Upstream(router::ApprovalError),
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Inbound { pub enum Inbound {
AuthChallengeRequest { AuthChallengeRequest { pubkey: VerifyingKey },
pubkey: VerifyingKey, AuthChallengeSolution { signature: Signature },
metadata: ClientMetadata,
},
AuthChallengeSolution {
signature: Signature,
},
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -64,17 +54,9 @@ pub enum Outbound {
AuthSuccess, 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_client_and_nonce( async fn get_nonce(db: &db::DatabasePool, pubkey: &VerifyingKey) -> Result<Option<i32>, Error> {
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| {
@@ -101,10 +83,8 @@ async fn get_client_and_nonce(
.execute(conn) .execute(conn)
.await?; .await?;
Ok(Some(ClientInfo { let _ = client_id;
id: client_id, Ok(Some(current_nonce))
current_nonce,
}))
}) })
}) })
.await .await
@@ -116,11 +96,13 @@ async fn get_client_and_nonce(
async fn approve_new_client( async fn approve_new_client(
actors: &crate::actors::GlobalActors, actors: &crate::actors::GlobalActors,
profile: ClientProfile, pubkey: VerifyingKey,
) -> Result<(), Error> { ) -> Result<(), Error> {
let result = actors let result = actors
.flow_coordinator .router
.ask(RequestClientApproval { client: profile }) .ask(RequestClientApproval {
client_pubkey: pubkey,
})
.await; .await;
match result { match result {
@@ -131,124 +113,65 @@ 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 flow coordinator failed"); error!(error = ?e, "Approval request to router failed");
Err(Error::ApproveError(ApproveError::Internal)) Err(Error::ApproveError(ApproveError::Internal))
} }
} }
} }
enum InsertClientResult {
Inserted,
AlreadyExists,
}
async fn insert_client( async fn insert_client(
db: &db::DatabasePool, db: &db::DatabasePool,
pubkey: &VerifyingKey, pubkey: &VerifyingKey,
metadata: &ClientMetadata, ) -> Result<InsertClientResult, Error> {
) -> Result<i32, Error> { let now = std::time::SystemTime::now()
use crate::db::schema::{client_metadata, program_client}; .duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i32;
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
})?; })?;
let metadata_id = insert_into(client_metadata::table) match insert_into(program_client::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(( .values((
program_client::public_key.eq(pubkey.as_bytes().to_vec()), 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 program_client::nonce.eq(1), // pre-incremented; challenge uses 0
)) program_client::created_at.eq(now),
.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| {
error!(error = ?e, "Database pool error");
Error::DatabasePoolUnavailable
})?;
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((
client_metadata_history::metadata_id.eq(current_metadata_id),
client_metadata_history::client_id.eq(client_id),
))
.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(conn) .execute(&mut conn)
.await?; .await
{
Ok(_) => {}
Err(diesel::result::Error::DatabaseError(
diesel::result::DatabaseErrorKind::UniqueViolation,
_,
)) => return Ok(InsertClientResult::AlreadyExists),
Err(e) => {
error!(error = ?e, "Failed to insert new client");
return Err(Error::DatabaseOperationFailed);
}
}
Ok::<(), diesel::result::Error>(()) let client_id = program_client::table
}) .filter(program_client::public_key.eq(pubkey.as_bytes().to_vec()))
}) .order(program_client::id.desc())
.select(program_client::id)
.first::<i32>(&mut conn)
.await .await
.map_err(|e| { .map_err(|e| {
error!(error = ?e, "Database error"); error!(error = ?e, "Failed to load inserted client id");
Error::DatabaseOperationFailed Error::DatabaseOperationFailed
}) })?;
let _ = client_id;
Ok(InsertClientResult::Inserted)
} }
async fn challenge_client<T>( async fn challenge_client<T>(
@@ -294,33 +217,26 @@ pub async fn authenticate<T>(
where where
T: Bi<Inbound, Result<Outbound, Error>> + Send + ?Sized, T: Bi<Inbound, Result<Outbound, Error>> + Send + ?Sized,
{ {
let Some(Inbound::AuthChallengeRequest { pubkey, metadata }) = transport.recv().await else { let Some(Inbound::AuthChallengeRequest { pubkey }) = transport.recv().await
else {
return Err(Error::Transport); return Err(Error::Transport);
}; };
let info = match get_client_and_nonce(&props.db, &pubkey).await? { let nonce = match get_nonce(&props.db, &pubkey).await? {
Some(nonce) => nonce, Some(nonce) => nonce,
None => { None => {
approve_new_client( approve_new_client(&props.actors, pubkey).await?;
&props.actors, match insert_client(&props.db, &pubkey).await? {
ClientProfile { InsertClientResult::Inserted => 0,
pubkey, InsertClientResult::AlreadyExists => match get_nonce(&props.db, &pubkey).await? {
metadata: metadata.clone(), Some(nonce) => nonce,
None => return Err(Error::DatabaseOperationFailed),
}, },
)
.await?;
let client_id = insert_client(&props.db, &pubkey, &metadata).await?;
ClientInfo {
id: client_id,
current_nonce: 0,
} }
} }
}; };
sync_client_metadata(&props.db, info.id, &metadata).await?; challenge_client(transport, pubkey, nonce).await?;
challenge_client(transport, pubkey, info.current_nonce).await?;
transport transport
.send(Ok(Outbound::AuthSuccess)) .send(Ok(Outbound::AuthSuccess))
.await .await

View File

@@ -1,18 +1,12 @@
use arbiter_proto::{ClientMetadata, transport::Bi}; use arbiter_proto::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)]
pub struct ClientProfile {
pub pubkey: ed25519_dalek::VerifyingKey,
pub metadata: ClientMetadata,
}
pub struct ClientConnection { pub struct ClientConnection {
pub(crate) db: db::DatabasePool, pub(crate) db: db::DatabasePool,
pub(crate) actors: GlobalActors, pub(crate) actors: GlobalActors,

View File

@@ -3,8 +3,7 @@ use tracing::error;
use crate::{ use crate::{
actors::{ actors::{
GlobalActors, client::ClientConnection, flow_coordinator::RegisterClient, GlobalActors, client::ClientConnection, keyholder::KeyHolderState, router::RegisterClient,
keyholder::KeyHolderState,
}, },
db, db,
}; };
@@ -48,7 +47,7 @@ impl Actor for ClientSession {
) -> Result<Self, Self::Error> { ) -> Result<Self, Self::Error> {
args.props args.props
.actors .actors
.flow_coordinator .router
.ask(RegisterClient { actor: this }) .ask(RegisterClient { actor: this })
.await .await
.map_err(|_| Error::ConnectionRegistrationFailed)?; .map_err(|_| Error::ConnectionRegistrationFailed)?;

View File

@@ -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, DatabaseError, DatabasePool, self, DatabasePool,
models::{self, SqliteTimestamp}, models::{self, SqliteTimestamp},
schema, schema,
}, },
evm::{ evm::{
self, RunKind, self, ListGrantsError, 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,7 +33,11 @@ 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] DatabaseError), Database(#[from] diesel::result::Error),
#[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))]
@@ -64,7 +68,15 @@ pub enum Error {
#[error("Database error: {0}")] #[error("Database error: {0}")]
#[diagnostic(code(arbiter::evm::database))] #[diagnostic(code(arbiter::evm::database))]
Database(#[from] DatabaseError), Database(#[from] diesel::result::Error),
#[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)]
@@ -93,7 +105,7 @@ impl EvmActor {
#[messages] #[messages]
impl EvmActor { impl EvmActor {
#[message] #[message]
pub async fn generate(&mut self) -> Result<(i32, Address), Error> { pub async fn generate(&mut self) -> Result<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()));
@@ -104,32 +116,29 @@ impl EvmActor {
.await .await
.map_err(|_| Error::KeyholderSend)?; .map_err(|_| Error::KeyholderSend)?;
let mut conn = self.db.get().await.map_err(DatabaseError::from)?; let mut conn = self.db.get().await?;
let wallet_id = insert_into(schema::evm_wallet::table) 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,
}) })
.returning(schema::evm_wallet::id) .execute(&mut conn)
.get_result(&mut conn) .await?;
.await
.map_err(DatabaseError::from)?;
Ok((wallet_id, address)) Ok(address)
} }
#[message] #[message]
pub async fn list_wallets(&self) -> Result<Vec<(i32, Address)>, Error> { pub async fn list_wallets(&self) -> Result<Vec<Address>, Error> {
let mut conn = self.db.get().await.map_err(DatabaseError::from)?; let mut conn = self.db.get().await?;
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| (w.id, Address::from_slice(&w.address))) .map(|w| Address::from_slice(&w.address))
.collect()) .collect())
} }
} }
@@ -139,24 +148,31 @@ 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, DatabaseError> { ) -> Result<i32, evm::CreationError> {
match grant { match grant {
SpecificGrant::EtherTransfer(settings) => { SpecificGrant::EtherTransfer(settings) => {
self.engine self.engine
.create_grant::<EtherTransfer>(FullGrant { .create_grant::<EtherTransfer>(
client_id,
FullGrant {
basic, basic,
specific: settings, specific: settings,
}) },
)
.await .await
} }
SpecificGrant::TokenTransfer(settings) => { SpecificGrant::TokenTransfer(settings) => {
self.engine self.engine
.create_grant::<TokenTransfer>(FullGrant { .create_grant::<TokenTransfer>(
client_id,
FullGrant {
basic, basic,
specific: settings, specific: settings,
}) },
)
.await .await
} }
} }
@@ -164,23 +180,22 @@ 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.map_err(DatabaseError::from)?; let mut conn = self.db.get().await?;
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> {
Ok(self match self.engine.list_all_grants().await {
.engine Ok(grants) => Ok(grants),
.list_all_grants() Err(ListGrantsError::Database(db)) => Err(Error::Database(db)),
.await Err(ListGrantsError::Pool(pool)) => Err(Error::DatabasePool(pool)),
.map_err(DatabaseError::from)?) }
} }
#[message] #[message]
@@ -190,29 +205,24 @@ 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.map_err(DatabaseError::from)?; let mut conn = self.db.get().await?;
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(wallet_access, transaction.clone(), RunKind::Execution) .evaluate_transaction(
wallet.id,
client_id,
transaction.clone(),
RunKind::Execution,
)
.await?; .await?;
Ok(meaning) Ok(meaning)
@@ -225,23 +235,13 @@ 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.map_err(DatabaseError::from)?; let mut conn = self.db.get().await?;
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,7 +256,12 @@ 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(wallet_access, transaction.clone(), RunKind::Execution) .evaluate_transaction(
wallet.id,
client_id,
transaction.clone(),
RunKind::Execution,
)
.await?; .await?;
use alloy::network::TxSignerSync as _; use alloy::network::TxSignerSync as _;

View File

@@ -1,101 +0,0 @@
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();
}
}
}

View File

@@ -1,118 +0,0 @@
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
}
}

View File

@@ -5,7 +5,6 @@ use chacha20poly1305::{
AeadInPlace, Key, KeyInit as _, XChaCha20Poly1305, XNonce, AeadInPlace, Key, KeyInit as _, XChaCha20Poly1305, XNonce,
aead::{AeadMut, Error, Payload}, aead::{AeadMut, Error, Payload},
}; };
use hmac::Mac as _;
use rand::{ use rand::{
Rng as _, SeedableRng, Rng as _, SeedableRng,
rngs::{StdRng, SysRng}, rngs::{StdRng, SysRng},
@@ -15,8 +14,6 @@ use crate::safe_cell::{SafeCell, SafeCellHandle as _};
pub const ROOT_KEY_TAG: &[u8] = "arbiter/seal/v1".as_bytes(); pub const ROOT_KEY_TAG: &[u8] = "arbiter/seal/v1".as_bytes();
pub const TAG: &[u8] = "arbiter/private-key/v1".as_bytes(); pub const TAG: &[u8] = "arbiter/private-key/v1".as_bytes();
pub const USERAGENT_INTEGRITY_DERIVE_TAG: &[u8] = "arbiter/useragent/integrity-key/v1".as_bytes();
pub const USERAGENT_INTEGRITY_TAG: &[u8] = "arbiter/useragent/pubkey-entry/v1".as_bytes();
pub const NONCE_LENGTH: usize = 24; pub const NONCE_LENGTH: usize = 24;
@@ -172,50 +169,6 @@ pub fn derive_seal_key(mut password: SafeCell<Vec<u8>>, salt: &Salt) -> KeyCell
key.into() key.into()
} }
/// Derives a dedicated key used for integrity tags within a specific domain.
pub fn derive_integrity_key(seal_key: &mut KeyCell, derive_tag: &[u8]) -> KeyCell {
type HmacSha256 = hmac::Hmac<sha2::Sha256>;
let mut derived = SafeCell::new(Key::default());
seal_key.0.read_inline(|seal_key_bytes| {
let mut mac = <HmacSha256 as hmac::Mac>::new_from_slice(seal_key_bytes.as_ref())
.expect("HMAC key initialization must not fail for 32-byte key");
mac.update(derive_tag);
let output = mac.finalize().into_bytes();
let mut writer = derived.write();
let writer: &mut [u8] = writer.as_mut();
writer.copy_from_slice(&output);
});
derived.into()
}
/// Computes an integrity tag for a specific domain and payload shape.
pub fn compute_integrity_tag<'a, I>(
integrity_key: &mut KeyCell,
purpose_tag: &[u8],
data_parts: I,
) -> [u8; 32]
where
I: IntoIterator<Item = &'a [u8]>,
{
type HmacSha256 = hmac::Hmac<sha2::Sha256>;
let mut output_tag = [0u8; 32];
integrity_key.0.read_inline(|integrity_key_bytes| {
let mut mac = <HmacSha256 as hmac::Mac>::new_from_slice(integrity_key_bytes.as_ref())
.expect("HMAC key initialization must not fail for 32-byte key");
mac.update(purpose_tag);
for data_part in data_parts {
mac.update(data_part);
}
output_tag.copy_from_slice(&mac.finalize().into_bytes());
});
output_tag
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -287,43 +240,4 @@ mod tests {
] ]
); );
} }
#[test]
pub fn integrity_tag_deterministic() {
let salt = generate_salt();
let mut seal_key = derive_seal_key(SafeCell::new(b"password".to_vec()), &salt);
let mut integrity_key = derive_integrity_key(&mut seal_key, USERAGENT_INTEGRITY_DERIVE_TAG);
let key_type = 1i32.to_be_bytes();
let t1 = compute_integrity_tag(
&mut integrity_key,
USERAGENT_INTEGRITY_TAG,
[key_type.as_slice(), b"pubkey".as_ref()],
);
let t2 = compute_integrity_tag(
&mut integrity_key,
USERAGENT_INTEGRITY_TAG,
[key_type.as_slice(), b"pubkey".as_ref()],
);
assert_eq!(t1, t2);
}
#[test]
pub fn integrity_tag_changes_with_payload() {
let salt = generate_salt();
let mut seal_key = derive_seal_key(SafeCell::new(b"password".to_vec()), &salt);
let mut integrity_key = derive_integrity_key(&mut seal_key, USERAGENT_INTEGRITY_DERIVE_TAG);
let key_type_1 = 1i32.to_be_bytes();
let key_type_2 = 2i32.to_be_bytes();
let t1 = compute_integrity_tag(
&mut integrity_key,
USERAGENT_INTEGRITY_TAG,
[key_type_1.as_slice(), b"pubkey".as_ref()],
);
let t2 = compute_integrity_tag(
&mut integrity_key,
USERAGENT_INTEGRITY_TAG,
[key_type_2.as_slice(), b"pubkey".as_ref()],
);
assert_ne!(t1, t2);
}
} }

View File

@@ -32,7 +32,6 @@ enum State {
Unsealed { Unsealed {
root_key_history_id: i32, root_key_history_id: i32,
root_key: KeyCell, root_key: KeyCell,
integrity_key: KeyCell,
}, },
} }
@@ -146,8 +145,6 @@ impl KeyHolder {
} }
let salt = v1::generate_salt(); let salt = v1::generate_salt();
let mut seal_key = v1::derive_seal_key(seal_key_raw, &salt); let mut seal_key = v1::derive_seal_key(seal_key_raw, &salt);
let integrity_key =
v1::derive_integrity_key(&mut seal_key, v1::USERAGENT_INTEGRITY_DERIVE_TAG);
let mut root_key = KeyCell::new_secure_random(); let mut root_key = KeyCell::new_secure_random();
// Zero nonces are fine because they are one-time // Zero nonces are fine because they are one-time
@@ -196,7 +193,6 @@ impl KeyHolder {
self.state = State::Unsealed { self.state = State::Unsealed {
root_key, root_key,
root_key_history_id, root_key_history_id,
integrity_key,
}; };
info!("Keyholder bootstrapped successfully"); info!("Keyholder bootstrapped successfully");
@@ -230,8 +226,6 @@ impl KeyHolder {
Error::BrokenDatabase Error::BrokenDatabase
})?; })?;
let mut seal_key = v1::derive_seal_key(seal_key_raw, &salt); let mut seal_key = v1::derive_seal_key(seal_key_raw, &salt);
let integrity_key =
v1::derive_integrity_key(&mut seal_key, v1::USERAGENT_INTEGRITY_DERIVE_TAG);
let mut root_key = SafeCell::new(current_key.ciphertext.clone()); let mut root_key = SafeCell::new(current_key.ciphertext.clone());
@@ -255,7 +249,6 @@ impl KeyHolder {
error!(?err, "Broken database: invalid encryption key size"); error!(?err, "Broken database: invalid encryption key size");
Error::BrokenDatabase Error::BrokenDatabase
})?, })?,
integrity_key,
}; };
info!("Keyholder unsealed successfully"); info!("Keyholder unsealed successfully");
@@ -263,25 +256,7 @@ impl KeyHolder {
Ok(()) Ok(())
} }
// Signs a generic integrity payload using the vault-derived integrity key // Decrypts the `aead_encrypted` entry with the given ID and returns the plaintext
#[message]
pub fn sign_integrity_tag(
&mut self,
purpose_tag: Vec<u8>,
data_parts: Vec<Vec<u8>>,
) -> Result<Vec<u8>, Error> {
let State::Unsealed { integrity_key, .. } = &mut self.state else {
return Err(Error::NotBootstrapped);
};
let tag = v1::compute_integrity_tag(
integrity_key,
&purpose_tag,
data_parts.iter().map(Vec::as_slice),
);
Ok(tag.to_vec())
}
#[message] #[message]
pub async fn decrypt(&mut self, aead_id: i32) -> Result<SafeCell<Vec<u8>>, Error> { pub async fn decrypt(&mut self, aead_id: i32) -> Result<SafeCell<Vec<u8>>, Error> {
let State::Unsealed { root_key, .. } = &mut self.state else { let State::Unsealed { root_key, .. } = &mut self.state else {
@@ -317,7 +292,6 @@ impl KeyHolder {
let State::Unsealed { let State::Unsealed {
root_key, root_key,
root_key_history_id, root_key_history_id,
..
} = &mut self.state } = &mut self.state
else { else {
return Err(Error::NotBootstrapped); return Err(Error::NotBootstrapped);

View File

@@ -3,18 +3,15 @@ use miette::Diagnostic;
use thiserror::Error; use thiserror::Error;
use crate::{ use crate::{
actors::{ actors::{bootstrap::Bootstrapper, evm::EvmActor, keyholder::KeyHolder, router::MessageRouter},
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)]
@@ -33,7 +30,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 flow_coordinator: ActorRef<FlowCoordinator>, pub router: ActorRef<MessageRouter>,
pub evm: ActorRef<EvmActor>, pub evm: ActorRef<EvmActor>,
} }
@@ -44,7 +41,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,
flow_coordinator: FlowCoordinator::spawn(FlowCoordinator::default()), router: MessageRouter::spawn(MessageRouter::default()),
}) })
} }
} }

View File

@@ -0,0 +1,173 @@
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<_>>();
tokio::task::spawn(async move {
let result = request_client_approval(&weak_refs, client_pubkey).await;
reply_sender.send(result);
});
reply
}
}

View File

@@ -1,26 +1,17 @@
use arbiter_proto::transport::Bi; 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 kameo::error::SendError;
use tracing::error; use tracing::error;
use super::Error; use super::Error;
use crate::{ use crate::{
actors::{ actors::{
bootstrap::ConsumeToken, bootstrap::ConsumeToken,
keyholder::{self, SignIntegrityTag},
user_agent::{AuthPublicKey, UserAgentConnection, auth::Outbound}, user_agent::{AuthPublicKey, UserAgentConnection, auth::Outbound},
}, },
db::schema, db::schema,
}; };
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AttestationStatus {
Attested,
NotAttested,
Unavailable,
}
pub struct ChallengeRequest { pub struct ChallengeRequest {
pub pubkey: AuthPublicKey, pub pubkey: AuthPublicKey,
} }
@@ -49,11 +40,7 @@ smlang::statemachine!(
} }
); );
async fn create_nonce( async fn create_nonce(db: &crate::db::DatabasePool, pubkey_bytes: &[u8]) -> Result<i32, Error> {
db: &crate::db::DatabasePool,
pubkey_bytes: &[u8],
key_type: crate::db::models::KeyType,
) -> 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::internal("Database unavailable") Error::internal("Database unavailable")
@@ -63,14 +50,12 @@ async fn create_nonce(
Box::pin(async move { Box::pin(async move {
let current_nonce = schema::useragent_client::table let current_nonce = schema::useragent_client::table
.filter(schema::useragent_client::public_key.eq(pubkey_bytes.to_vec())) .filter(schema::useragent_client::public_key.eq(pubkey_bytes.to_vec()))
.filter(schema::useragent_client::key_type.eq(key_type))
.select(schema::useragent_client::nonce) .select(schema::useragent_client::nonce)
.first::<i32>(conn) .first::<i32>(conn)
.await?; .await?;
update(schema::useragent_client::table) update(schema::useragent_client::table)
.filter(schema::useragent_client::public_key.eq(pubkey_bytes.to_vec())) .filter(schema::useragent_client::public_key.eq(pubkey_bytes.to_vec()))
.filter(schema::useragent_client::key_type.eq(key_type))
.set(schema::useragent_client::nonce.eq(current_nonce + 1)) .set(schema::useragent_client::nonce.eq(current_nonce + 1))
.execute(conn) .execute(conn)
.await?; .await?;
@@ -90,11 +75,7 @@ async fn create_nonce(
}) })
} }
async fn register_key( async fn register_key(db: &crate::db::DatabasePool, pubkey: &AuthPublicKey) -> Result<(), Error> {
db: &crate::db::DatabasePool,
pubkey: &AuthPublicKey,
integrity_tag: Option<Vec<u8>>,
) -> Result<(), Error> {
let pubkey_bytes = pubkey.to_stored_bytes(); let pubkey_bytes = pubkey.to_stored_bytes();
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| {
@@ -107,7 +88,6 @@ async fn register_key(
schema::useragent_client::public_key.eq(pubkey_bytes), schema::useragent_client::public_key.eq(pubkey_bytes),
schema::useragent_client::nonce.eq(1), schema::useragent_client::nonce.eq(1),
schema::useragent_client::key_type.eq(key_type), schema::useragent_client::key_type.eq(key_type),
schema::useragent_client::pubkey_integrity_tag.eq(integrity_tag),
)) ))
.execute(&mut conn) .execute(&mut conn)
.await .await
@@ -140,15 +120,8 @@ where
&mut self, &mut self,
ChallengeRequest { pubkey }: ChallengeRequest, ChallengeRequest { pubkey }: ChallengeRequest,
) -> Result<ChallengeContext, Self::Error> { ) -> Result<ChallengeContext, Self::Error> {
match self.verify_pubkey_attestation_status(&pubkey).await? {
AttestationStatus::Attested | AttestationStatus::Unavailable => {}
AttestationStatus::NotAttested => {
return Err(Error::InvalidChallengeSolution);
}
}
let stored_bytes = pubkey.to_stored_bytes(); let stored_bytes = pubkey.to_stored_bytes();
let nonce = create_nonce(&self.conn.db, &stored_bytes, pubkey.key_type()).await?; let nonce = create_nonce(&self.conn.db, &stored_bytes).await?;
self.transport self.transport
.send(Ok(Outbound::AuthChallenge { nonce })) .send(Ok(Outbound::AuthChallenge { nonce }))
@@ -188,15 +161,7 @@ where
return Err(Error::InvalidBootstrapToken); return Err(Error::InvalidBootstrapToken);
} }
let integrity_tag = self register_key(&self.conn.db, &pubkey).await?;
.try_sign_pubkey_integrity_tag(&pubkey)
.await
.map_err(|err| {
error!(?err, "Failed to sign user-agent pubkey integrity tag");
Error::internal("Failed to sign user-agent pubkey integrity tag")
})?;
register_key(&self.conn.db, &pubkey, integrity_tag).await?;
self.transport self.transport
.send(Ok(Outbound::AuthSuccess)) .send(Ok(Outbound::AuthSuccess))
@@ -255,94 +220,3 @@ where
Ok(key.clone()) Ok(key.clone())
} }
} }
impl<T> AuthContext<'_, T>
where
T: Bi<super::Inbound, Result<super::Outbound, Error>> + Send,
{
async fn try_sign_pubkey_integrity_tag(
&self,
pubkey: &AuthPublicKey,
) -> Result<Option<Vec<u8>>, Error> {
let signed = self
.conn
.actors
.key_holder
.ask(SignIntegrityTag {
purpose_tag: keyholder::encryption::v1::USERAGENT_INTEGRITY_TAG.to_vec(),
data_parts: vec![
(pubkey.key_type() as i32).to_be_bytes().to_vec(),
pubkey.to_stored_bytes(),
],
})
.await;
match signed {
Ok(tag) => Ok(Some(tag)),
Err(SendError::HandlerError(keyholder::Error::NotBootstrapped)) => Ok(None),
Err(SendError::HandlerError(err)) => {
error!(
?err,
"Keyholder failed to sign user-agent pubkey integrity tag"
);
Err(Error::internal(
"Keyholder failed to sign user-agent pubkey integrity tag",
))
}
Err(err) => {
error!(
?err,
"Failed to contact keyholder for user-agent pubkey integrity tag"
);
Err(Error::internal(
"Failed to contact keyholder for user-agent pubkey integrity tag",
))
}
}
}
async fn verify_pubkey_attestation_status(
&self,
pubkey: &AuthPublicKey,
) -> Result<AttestationStatus, Error> {
let stored_tag: Option<Option<Vec<u8>>> = {
let mut conn = self.conn.db.get().await.map_err(|e| {
error!(error = ?e, "Database pool error");
Error::internal("Database unavailable")
})?;
schema::useragent_client::table
.filter(schema::useragent_client::public_key.eq(pubkey.to_stored_bytes()))
.filter(schema::useragent_client::key_type.eq(pubkey.key_type()))
.select(schema::useragent_client::pubkey_integrity_tag)
.first::<Option<Vec<u8>>>(&mut conn)
.await
.optional()
.map_err(|e| {
error!(error = ?e, "Database error");
Error::internal("Database operation failed")
})?
};
let Some(stored_tag) = stored_tag else {
return Err(Error::UnregisteredPublicKey);
};
let Some(expected_tag) = self.try_sign_pubkey_integrity_tag(pubkey).await? else {
// Vault sealed/unbootstrapped: cannot verify integrity yet.
return Ok(AttestationStatus::Unavailable);
};
match stored_tag {
Some(stored_tag) if stored_tag == expected_tag => Ok(AttestationStatus::Attested),
Some(_) => {
error!("User-agent pubkey integrity tag mismatch");
Ok(AttestationStatus::NotAttested)
}
None => {
error!("Missing pubkey integrity tag for registered key while vault is unsealed");
Ok(AttestationStatus::NotAttested)
}
}
}
}

View File

@@ -1,5 +1,5 @@
use crate::{ use crate::{
actors::{GlobalActors, client::ClientProfile}, actors::GlobalActors,
db::{self, models::KeyType}, db::{self, models::KeyType},
}; };
@@ -72,8 +72,8 @@ impl TryFrom<(KeyType, Vec<u8>)> for AuthPublicKey {
// Messages, sent by user agent to connection client without having a request // Messages, sent by user agent to connection client without having a request
#[derive(Debug)] #[derive(Debug)]
pub enum OutOfBand { pub enum OutOfBand {
ClientConnectionRequest { profile: ClientProfile }, ClientConnectionRequest { pubkey: ed25519_dalek::VerifyingKey },
ClientConnectionCancel { pubkey: ed25519_dalek::VerifyingKey }, ClientConnectionCancel,
} }
pub struct UserAgentConnection { pub struct UserAgentConnection {

View File

@@ -1,15 +1,15 @@
use std::{borrow::Cow, collections::HashMap}; use std::borrow::Cow;
use arbiter_proto::transport::Sender; use arbiter_proto::transport::Sender;
use async_trait::async_trait; use async_trait::async_trait;
use ed25519_dalek::VerifyingKey; use ed25519_dalek::VerifyingKey;
use kameo::{Actor, actor::ActorRef, messages}; use kameo::{Actor, messages};
use thiserror::Error; use thiserror::Error;
use tokio::sync::watch;
use tracing::error; use tracing::error;
use crate::actors::{ use crate::actors::{
client::ClientProfile, router::RegisterUserAgent,
flow_coordinator::{RegisterUserAgent, client_connect_approval::ClientApprovalController},
user_agent::{OutOfBand, UserAgentConnection}, user_agent::{OutOfBand, UserAgentConnection},
}; };
@@ -25,19 +25,6 @@ pub enum Error {
Internal { message: Cow<'static, str> }, 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 { impl Error {
pub fn internal(message: impl Into<Cow<'static, str>>) -> Self { pub fn internal(message: impl Into<Cow<'static, str>>) -> Self {
Self::Internal { Self::Internal {
@@ -46,19 +33,19 @@ impl Error {
} }
} }
pub struct PendingClientApproval {
controller: ActorRef<ClientApprovalController>,
}
pub struct UserAgentSession { pub struct UserAgentSession {
props: UserAgentConnection, props: UserAgentConnection,
state: UserAgentStateMachine<DummyContext>, state: UserAgentStateMachine<DummyContext>,
#[allow(dead_code, reason = "The session keeps ownership of the outbound transport even before the state-machine flow starts using it directly")]
sender: Box<dyn Sender<OutOfBand>>, sender: Box<dyn Sender<OutOfBand>>,
pending_client_approvals: HashMap<VerifyingKey, PendingClientApproval>,
} }
pub mod connection; mod connection;
pub(crate) use connection::{
BootstrapError, HandleBootstrapEncryptedKey, HandleEvmWalletCreate, HandleEvmWalletList,
HandleGrantCreate, HandleGrantDelete, HandleGrantList, HandleQueryVaultState,
};
pub use connection::{HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError};
impl UserAgentSession { impl UserAgentSession {
pub(crate) fn new(props: UserAgentConnection, sender: Box<dyn Sender<OutOfBand>>) -> Self { pub(crate) fn new(props: UserAgentConnection, sender: Box<dyn Sender<OutOfBand>>) -> Self {
@@ -66,7 +53,6 @@ impl UserAgentSession {
props, props,
state: UserAgentStateMachine::new(DummyContext), state: UserAgentStateMachine::new(DummyContext),
sender, sender,
pending_client_approvals: Default::default(),
} }
} }
@@ -98,28 +84,26 @@ impl UserAgentSession {
#[messages] #[messages]
impl UserAgentSession { impl UserAgentSession {
#[message] #[message]
pub async fn begin_new_client_approval( pub async fn request_new_client_approval(
&mut self, &mut self,
client: ClientProfile, client_pubkey: VerifyingKey,
controller: ActorRef<ClientApprovalController>, mut cancel_flag: watch::Receiver<()>,
) { ) -> Result<bool, ()> {
if let Err(e) = self if self
.sender .sender
.send(OutOfBand::ClientConnectionRequest { .send(OutOfBand::ClientConnectionRequest {
profile: client.clone(), pubkey: client_pubkey,
}) })
.await .await
.is_err()
{ {
error!( return Err(());
?e,
actor = "user_agent",
event = "failed to announce new client connection"
);
return;
} }
self.pending_client_approvals let _ = cancel_flag.changed().await;
.insert(client.pubkey, PendingClientApproval { controller });
let _ = self.sender.send(OutOfBand::ClientConnectionCancel).await;
Ok(false)
} }
} }
@@ -134,48 +118,15 @@ impl Actor for UserAgentSession {
) -> Result<Self, Self::Error> { ) -> Result<Self, Self::Error> {
args.props args.props
.actors .actors
.flow_coordinator .router
.ask(RegisterUserAgent { .ask(RegisterUserAgent {
actor: this.clone(), actor: this.clone(),
}) })
.await .await
.map_err(|err| { .map_err(|err| {
error!( error!(?err, "Failed to register user agent connection with router");
?err, Error::internal("Failed to register user agent connection with router")
"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 on_link_died(
&mut self,
_: kameo::prelude::WeakActorRef<Self>,
id: kameo::prelude::ActorId,
_: kameo::prelude::ActorStopReason,
) -> Result<std::ops::ControlFlow<kameo::prelude::ActorStopReason>, Self::Error> {
let cancelled_pubkey = self
.pending_client_approvals
.iter()
.find_map(|(k, v)| (v.controller.id() == id).then_some(*k));
if let Some(pubkey) = cancelled_pubkey {
self.pending_client_approvals.remove(&pubkey);
if let Err(e) = self
.sender
.send(OutOfBand::ClientConnectionCancel { pubkey })
.await
{
error!(
?e,
actor = "user_agent",
event = "failed to announce client connection cancellation"
);
}
}
Ok(std::ops::ControlFlow::Continue(()))
}
} }

View File

@@ -2,20 +2,13 @@ use std::sync::Mutex;
use alloy::primitives::Address; use alloy::primitives::Address;
use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit}; use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
use diesel::{ExpressionMethods as _, QueryDsl as _, SelectableHelper, dsl::update};
use diesel_async::{AsyncConnection, RunQueryDsl};
use kameo::error::SendError; use kameo::error::SendError;
use kameo::messages; use kameo::messages;
use kameo::prelude::Context;
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::keyholder::KeyHolderState;
use crate::actors::user_agent::session::Error; use crate::actors::user_agent::session::Error;
use crate::db::models::{
EvmWalletAccess, KeyType, NewEvmWalletAccess, ProgramClient, ProgramClientMetadata,
};
use crate::evm::policies::{Grant, SpecificGrant}; use crate::evm::policies::{Grant, SpecificGrant};
use crate::safe_cell::SafeCell; use crate::safe_cell::SafeCell;
use crate::{ use crate::{
@@ -23,7 +16,7 @@ use crate::{
evm::{ evm::{
Generate, ListWallets, UseragentCreateGrant, UseragentDeleteGrant, UseragentListGrants, Generate, ListWallets, UseragentCreateGrant, UseragentDeleteGrant, UseragentListGrants,
}, },
keyholder::{self, Bootstrap, SignIntegrityTag, TryUnseal}, keyholder::{self, Bootstrap, TryUnseal},
user_agent::session::{ user_agent::session::{
UserAgentSession, UserAgentSession,
state::{UnsealContext, UserAgentEvents, UserAgentStates}, state::{UnsealContext, UserAgentEvents, UserAgentStates},
@@ -85,56 +78,6 @@ impl UserAgentSession {
} }
} }
} }
async fn backfill_missing_useragent_pubkey_integrity_tags(&mut self) -> Result<(), Error> {
use crate::db::schema::useragent_client;
let mut conn = self.props.db.get().await?;
let missing_rows: Vec<(i32, Vec<u8>, KeyType)> = useragent_client::table
.filter(useragent_client::pubkey_integrity_tag.is_null())
.select((
useragent_client::id,
useragent_client::public_key,
useragent_client::key_type,
))
.load(&mut conn)
.await?;
drop(conn);
if missing_rows.is_empty() {
return Ok(());
}
let mut updates = Vec::with_capacity(missing_rows.len());
for (id, public_key, key_type) in missing_rows {
let tag = self
.props
.actors
.key_holder
.ask(SignIntegrityTag {
purpose_tag: keyholder::encryption::v1::USERAGENT_INTEGRITY_TAG.to_vec(),
data_parts: vec![(key_type as i32).to_be_bytes().to_vec(), public_key],
})
.await
.map_err(|err| {
error!(?err, "Failed to sign integrity tag");
Error::internal("Failed to sign integrity tag")
})?;
updates.push((id, tag));
}
let mut conn = self.props.db.get().await?;
for (id, tag) in updates {
update(useragent_client::table)
.filter(useragent_client::id.eq(id))
.set(useragent_client::pubkey_integrity_tag.eq(Some(tag)))
.execute(&mut conn)
.await?;
}
info!("Backfilled missing user-agent pubkey integrity tags");
Ok(())
}
} }
pub struct UnsealStartResponse { pub struct UnsealStartResponse {
@@ -222,8 +165,6 @@ impl UserAgentSession {
.await .await
{ {
Ok(_) => { Ok(_) => {
self.backfill_missing_useragent_pubkey_integrity_tags()
.await?;
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(()) Ok(())
@@ -285,8 +226,6 @@ impl UserAgentSession {
.await .await
{ {
Ok(_) => { Ok(_) => {
self.backfill_missing_useragent_pubkey_integrity_tags()
.await?;
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(()) Ok(())
@@ -332,7 +271,7 @@ impl UserAgentSession {
#[messages] #[messages]
impl UserAgentSession { impl UserAgentSession {
#[message] #[message]
pub(crate) async fn handle_evm_wallet_create(&mut self) -> Result<(i32, Address), Error> { pub(crate) async fn handle_evm_wallet_create(&mut self) -> Result<Address, Error> {
match self.props.actors.evm.ask(Generate {}).await { match self.props.actors.evm.ask(Generate {}).await {
Ok(address) => Ok(address), Ok(address) => Ok(address),
Err(SendError::HandlerError(err)) => Err(Error::internal(format!( Err(SendError::HandlerError(err)) => Err(Error::internal(format!(
@@ -346,7 +285,7 @@ impl UserAgentSession {
} }
#[message] #[message]
pub(crate) async fn handle_evm_wallet_list(&mut self) -> Result<Vec<(i32, Address)>, Error> { pub(crate) async fn handle_evm_wallet_list(&mut self) -> Result<Vec<Address>, Error> {
match self.props.actors.evm.ask(ListWallets {}).await { match self.props.actors.evm.ask(ListWallets {}).await {
Ok(wallets) => Ok(wallets), Ok(wallets) => Ok(wallets),
Err(err) => { Err(err) => {
@@ -373,6 +312,7 @@ impl UserAgentSession {
#[message] #[message]
pub(crate) async fn handle_grant_create( 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,
) -> Result<i32, Error> { ) -> Result<i32, Error> {
@@ -380,7 +320,11 @@ impl UserAgentSession {
.props .props
.actors .actors
.evm .evm
.ask(UseragentCreateGrant { basic, grant }) .ask(UseragentCreateGrant {
client_id,
basic,
grant,
})
.await .await
{ {
Ok(grant_id) => Ok(grant_id), Ok(grant_id) => Ok(grant_id),
@@ -407,119 +351,4 @@ impl UserAgentSession {
} }
} }
} }
#[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)
}
} }

View File

@@ -1,4 +1,4 @@
use std::{net::IpAddr, string::FromUtf8Error}; use std::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, SanType, IsCa, Issuer, KeyPair, KeyUsagePurpose,
}; };
use rustls::pki_types::pem::PemObject; use rustls::pki_types::pem::PemObject;
use thiserror::Error; use thiserror::Error;
@@ -114,11 +114,6 @@ 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");

View File

@@ -21,7 +21,7 @@ pub mod types {
sqlite::{Sqlite, SqliteType}, sqlite::{Sqlite, SqliteType},
}; };
#[derive(Debug, FromSqlRow, AsExpression, Clone)] #[derive(Debug, FromSqlRow, AsExpression)]
#[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,53 +185,12 @@ pub struct EvmWallet {
pub created_at: SqliteTimestamp, pub created_at: SqliteTimestamp,
} }
#[derive(Models, Queryable, Debug, Insertable, Selectable, Clone)] #[derive(Queryable, Debug, Insertable, Selectable)]
#[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,
} }
@@ -242,7 +201,6 @@ pub struct UseragentClient {
pub id: i32, pub id: i32,
pub nonce: i32, pub nonce: i32,
pub public_key: Vec<u8>, pub public_key: Vec<u8>,
pub pubkey_integrity_tag: Option<Vec<u8>>,
pub created_at: SqliteTimestamp, pub created_at: SqliteTimestamp,
pub updated_at: SqliteTimestamp, pub updated_at: SqliteTimestamp,
pub key_type: KeyType, pub key_type: KeyType,
@@ -272,7 +230,8 @@ pub struct EvmEtherTransferLimit {
)] )]
pub struct EvmBasicGrant { pub struct EvmBasicGrant {
pub id: i32, pub id: i32,
pub wallet_access_id: i32, // references evm_wallet_access.id pub wallet_id: i32, // references evm_wallet.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>,
@@ -295,7 +254,8 @@ pub struct EvmBasicGrant {
pub struct EvmTransactionLog { pub struct EvmTransactionLog {
pub id: i32, pub id: i32,
pub grant_id: i32, pub grant_id: i32,
pub wallet_access_id: i32, pub client_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,

View File

@@ -20,29 +20,11 @@ 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_access_id -> Integer, wallet_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>,
@@ -113,8 +95,9 @@ 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,
@@ -130,21 +113,11 @@ 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,
} }
@@ -178,19 +151,17 @@ diesel::table! {
id -> Integer, id -> Integer,
nonce -> Integer, nonce -> Integer,
public_key -> Binary, public_key -> Binary,
pubkey_integrity_tag -> Nullable<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!(client_metadata_history -> client_metadata (metadata_id)); diesel::joinable!(evm_basic_grant -> evm_wallet (wallet_id));
diesel::joinable!(client_metadata_history -> program_client (client_id)); diesel::joinable!(evm_basic_grant -> 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));
@@ -198,18 +169,11 @@ 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,
@@ -219,7 +183,6 @@ 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,

View File

@@ -6,16 +6,13 @@ use alloy::{
primitives::{TxKind, U256}, primitives::{TxKind, U256},
}; };
use chrono::Utc; use chrono::Utc;
use diesel::{ExpressionMethods as _, QueryDsl as _, QueryResult, insert_into, sqlite::Sqlite}; use diesel::{ExpressionMethods as _, QueryDsl, 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, DatabaseError, self,
models::{ models::{EvmBasicGrant, NewEvmBasicGrant, NewEvmTransactionLog, SqliteTimestamp},
EvmBasicGrant, EvmWalletAccess, NewEvmBasicGrant, NewEvmTransactionLog, SqliteTimestamp,
},
schema::{self, evm_transaction_log}, schema::{self, evm_transaction_log},
}, },
evm::policies::{ evm::policies::{
@@ -31,8 +28,12 @@ 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 error")] #[error("Database connection pool error")]
Error(#[from] crate::db::DatabaseError), #[diagnostic(code(arbiter_server::evm::policy_error::pool))]
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>),
@@ -54,6 +55,16 @@ 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")]
@@ -65,6 +76,28 @@ 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 {
@@ -132,22 +165,16 @@ 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.map_err(DatabaseError::from)?; let mut conn = self.db.get().await?;
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?;
.map_err(DatabaseError::from)?; violations.extend(P::evaluate(&context, meaning, &grant, &mut conn).await?);
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));
@@ -157,7 +184,8 @@ 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,
wallet_access_id: context.target.id, client_id: context.client_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(),
@@ -171,8 +199,7 @@ impl Engine {
QueryResult::Ok(()) QueryResult::Ok(())
}) })
}) })
.await .await?;
.map_err(DatabaseError::from)?;
} }
Ok(()) Ok(())
@@ -186,8 +213,9 @@ 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, DatabaseError> { ) -> Result<i32, CreationError> {
let mut conn = self.db.get().await?; let mut conn = self.db.get().await?;
let id = conn let id = conn
@@ -197,8 +225,9 @@ 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,
wallet_access_id: full_grant.basic.wallet_access_id, client_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
@@ -233,7 +262,7 @@ impl Engine {
Ok(id) Ok(id)
} }
pub async fn list_all_grants(&self) -> Result<Vec<Grant<SpecificGrant>>, DatabaseError> { pub async fn list_all_grants(&self) -> Result<Vec<Grant<SpecificGrant>>, ListGrantsError> {
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();
@@ -266,7 +295,8 @@ impl Engine {
pub async fn evaluate_transaction( pub async fn evaluate_transaction(
&self, &self,
target: EvmWalletAccess, wallet_id: i32,
client_id: i32,
transaction: TxEip1559, transaction: TxEip1559,
run_kind: RunKind, run_kind: RunKind,
) -> Result<SpecificMeaning, VetError> { ) -> Result<SpecificMeaning, VetError> {
@@ -274,7 +304,8 @@ impl Engine {
return Err(VetError::ContractCreationNotSupported); return Err(VetError::ContractCreationNotSupported);
}; };
let context = policies::EvalContext { let context = policies::EvalContext {
target, wallet_id,
client_id,
chain: transaction.chain_id, chain: transaction.chain_id,
to, to,
value: transaction.value, value: transaction.value,

View File

@@ -10,7 +10,7 @@ use miette::Diagnostic;
use thiserror::Error; use thiserror::Error;
use crate::{ use crate::{
db::models::{self, EvmBasicGrant, EvmWalletAccess}, db::models::{self, EvmBasicGrant},
evm::utils, evm::utils,
}; };
@@ -19,8 +19,9 @@ pub mod token_transfers;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct EvalContext { pub struct EvalContext {
// Which wallet is this transaction for and who requested it // Which wallet is this transaction for
pub target: EvmWalletAccess, pub client_id: i32,
pub wallet_id: i32,
// The transaction data // The transaction data
pub chain: ChainId, pub chain: ChainId,
@@ -144,7 +145,8 @@ pub struct VolumeRateLimit {
#[derive(Clone, Debug, PartialEq, Eq, Hash)] #[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct SharedGrantSettings { pub struct SharedGrantSettings {
pub wallet_access_id: i32, pub wallet_id: i32,
pub client_id: i32,
pub chain: ChainId, pub chain: ChainId,
pub valid_from: Option<DateTime<Utc>>, pub valid_from: Option<DateTime<Utc>>,
@@ -159,7 +161,8 @@ 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_access_id: model.wallet_access_id, wallet_id: model.wallet_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),

View File

@@ -196,8 +196,9 @@ 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_access_id evm_basic_grant::wallet_id
.eq(context.target.id) .eq(context.wallet_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)),
) )

View File

@@ -5,9 +5,7 @@ use diesel_async::RunQueryDsl;
use crate::db::{ use crate::db::{
self, DatabaseConnection, self, DatabaseConnection,
models::{ models::{EvmBasicGrant, NewEvmBasicGrant, NewEvmTransactionLog, SqliteTimestamp},
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::{
@@ -17,7 +15,8 @@ use crate::evm::{
use super::{EtherTransfer, Settings}; use super::{EtherTransfer, Settings};
const WALLET_ACCESS_ID: i32 = 1; const WALLET_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");
@@ -25,12 +24,8 @@ const OTHER: Address = address!("2222222222222222222222222222222222222222");
fn ctx(to: Address, value: U256) -> EvalContext { fn ctx(to: Address, value: U256) -> EvalContext {
EvalContext { EvalContext {
target: EvmWalletAccess { wallet_id: WALLET_ID,
id: WALLET_ACCESS_ID, client_id: CLIENT_ID,
wallet_id: 10,
client_id: 20,
created_at: SqliteTimestamp(Utc::now()),
},
chain: CHAIN_ID, chain: CHAIN_ID,
to, to,
value, value,
@@ -43,7 +38,8 @@ 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_access_id: WALLET_ACCESS_ID, wallet_id: WALLET_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,
@@ -71,13 +67,14 @@ fn make_settings(targets: Vec<Address>, max_volume: u64) -> Settings {
fn shared() -> SharedGrantSettings { fn shared() -> SharedGrantSettings {
SharedGrantSettings { SharedGrantSettings {
wallet_access_id: WALLET_ACCESS_ID, wallet_id: WALLET_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,
} }
} }
@@ -156,7 +153,8 @@ 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,
wallet_access_id: WALLET_ACCESS_ID, client_id: CLIENT_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()),
@@ -196,7 +194,8 @@ 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,
wallet_access_id: WALLET_ACCESS_ID, client_id: CLIENT_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()),
@@ -237,7 +236,8 @@ 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,
wallet_access_id: WALLET_ACCESS_ID, client_id: CLIENT_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()),

View File

@@ -209,7 +209,8 @@ 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_access_id.eq(context.target.id)) .filter(evm_basic_grant::wallet_id.eq(context.wallet_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(),

View File

@@ -6,7 +6,7 @@ use diesel_async::RunQueryDsl;
use crate::db::{ use crate::db::{
self, DatabaseConnection, self, DatabaseConnection,
models::{EvmBasicGrant, EvmWalletAccess, NewEvmBasicGrant, SqliteTimestamp}, models::{EvmBasicGrant, NewEvmBasicGrant, SqliteTimestamp},
schema::evm_basic_grant, schema::evm_basic_grant,
}; };
use crate::evm::{ use crate::evm::{
@@ -21,7 +21,8 @@ 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_ACCESS_ID: i32 = 1; const WALLET_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");
@@ -37,12 +38,8 @@ fn transfer_calldata(to: Address, value: U256) -> Bytes {
fn ctx(to: Address, calldata: Bytes) -> EvalContext { fn ctx(to: Address, calldata: Bytes) -> EvalContext {
EvalContext { EvalContext {
target: EvmWalletAccess { wallet_id: WALLET_ID,
id: WALLET_ACCESS_ID, client_id: CLIENT_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,
@@ -55,7 +52,8 @@ 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_access_id: WALLET_ACCESS_ID, wallet_id: WALLET_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,
@@ -88,13 +86,14 @@ fn make_settings(target: Option<Address>, max_volume: Option<u64>) -> Settings {
fn shared() -> SharedGrantSettings { fn shared() -> SharedGrantSettings {
SharedGrantSettings { SharedGrantSettings {
wallet_access_id: WALLET_ACCESS_ID, wallet_id: WALLET_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,
} }
} }

View File

@@ -22,11 +22,10 @@ use crate::{
keyholder::KeyHolderState, keyholder::KeyHolderState,
}, },
grpc::request_tracker::RequestTracker, grpc::request_tracker::RequestTracker,
utils::defer,
}; };
mod auth; mod auth;
mod inbound;
mod outbound;
async fn dispatch_loop( async fn dispatch_loop(
mut bi: GrpcBi<ClientRequest, ClientResponse>, mut bi: GrpcBi<ClientRequest, ClientResponse>,
@@ -34,53 +33,52 @@ async fn dispatch_loop(
mut request_tracker: RequestTracker, mut request_tracker: RequestTracker,
) { ) {
loop { loop {
let Some(message) = bi.recv().await else { return }; let Some(conn) = bi.recv().await else {
return;
};
let conn = match message { if dispatch_conn_message(&mut bi, &actor, &mut request_tracker, conn)
.await
.is_err()
{
return;
}
}
}
async fn dispatch_conn_message(
bi: &mut GrpcBi<ClientRequest, ClientResponse>,
actor: &ActorRef<ClientSession>,
request_tracker: &mut RequestTracker,
conn: Result<ClientRequest, Status>,
) -> Result<(), ()> {
let conn = match conn {
Ok(conn) => conn, Ok(conn) => conn,
Err(err) => { Err(err) => {
warn!(error = ?err, "Failed to receive client request"); warn!(error = ?err, "Failed to receive client request");
return; return Err(());
} }
}; };
let request_id = match request_tracker.request(conn.request_id) { let request_id = match request_tracker.request(conn.request_id) {
Ok(id) => id, Ok(request_id) => request_id,
Err(err) => { Err(err) => {
let _ = bi.send(Err(err)).await; let _ = bi.send(Err(err)).await;
return; return Err(());
} }
}; };
let Some(payload) = conn.payload else { let Some(payload) = conn.payload else {
let _ = bi.send(Err(Status::invalid_argument("Missing client request payload"))).await; let _ = bi
return; .send(Err(Status::invalid_argument(
"Missing client request payload",
)))
.await;
return Err(());
}; };
match dispatch_inner(&actor, payload).await { let payload = match payload {
Ok(response) => { ClientRequestPayload::QueryVaultState(_) => ClientResponsePayload::VaultState(
if bi.send(Ok(ClientResponse { match actor.ask(HandleQueryVaultState {}).await {
request_id: Some(request_id),
payload: Some(response),
})).await.is_err() {
return;
}
}
Err(status) => {
let _ = bi.send(Err(status)).await;
return;
}
}
}
}
async fn dispatch_inner(
actor: &ActorRef<ClientSession>,
payload: ClientRequestPayload,
) -> Result<ClientResponsePayload, Status> {
match payload {
ClientRequestPayload::QueryVaultState(_) => {
let state = match actor.ask(HandleQueryVaultState {}).await {
Ok(KeyHolderState::Unbootstrapped) => ProtoVaultState::Unbootstrapped, Ok(KeyHolderState::Unbootstrapped) => ProtoVaultState::Unbootstrapped,
Ok(KeyHolderState::Sealed) => ProtoVaultState::Sealed, Ok(KeyHolderState::Sealed) => ProtoVaultState::Sealed,
Ok(KeyHolderState::Unsealed) => ProtoVaultState::Unsealed, Ok(KeyHolderState::Unsealed) => ProtoVaultState::Unsealed,
@@ -89,30 +87,51 @@ async fn dispatch_inner(
warn!(error = ?err, "Failed to query vault state"); warn!(error = ?err, "Failed to query vault state");
ProtoVaultState::Error ProtoVaultState::Error
} }
};
Ok(ClientResponsePayload::VaultState(state.into()))
} }
.into(),
),
payload => { payload => {
warn!(?payload, "Unsupported post-auth client request"); warn!(?payload, "Unsupported post-auth client request");
Err(Status::invalid_argument("Unsupported client request")) let _ = bi
.send(Err(Status::invalid_argument("Unsupported client request")))
.await;
return Err(());
} }
}
}
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)); bi.send(Ok(ClientResponse {
request_id: Some(request_id),
payload: Some(payload),
}))
.await
.map_err(|_| ())
}
pub async fn start(conn: ClientConnection, mut bi: GrpcBi<ClientRequest, ClientResponse>) {
let mut conn = conn;
let mut request_tracker = RequestTracker::default();
let mut response_id = None;
match auth::start(&mut conn, &mut bi, &mut request_tracker, &mut response_id).await {
Ok(_) => {
let actor =
client::session::ClientSession::spawn(client::session::ClientSession::new(conn));
let actor_for_cleanup = actor.clone(); let actor_for_cleanup = actor.clone();
let _ = defer(move || {
actor_for_cleanup.kill();
});
info!("Client authenticated successfully"); info!("Client authenticated successfully");
dispatch_loop(bi, actor, request_tracker).await; dispatch_loop(bi, actor, request_tracker).await;
actor_for_cleanup.kill(); }
Err(e) => {
let mut transport = auth::AuthTransportAdapter::new(
&mut bi,
&mut request_tracker,
&mut response_id,
);
let _ = transport.send(Err(e.clone())).await;
warn!(error = ?e, "Authentication failed");
}
}
} }

View File

@@ -1,11 +1,11 @@
use arbiter_proto::{ use arbiter_proto::{
ClientMetadata, proto::client::{ proto::client::{
AuthChallenge as ProtoAuthChallenge, AuthChallengeRequest as ProtoAuthChallengeRequest, AuthChallenge as ProtoAuthChallenge, AuthChallengeRequest as ProtoAuthChallengeRequest,
AuthChallengeSolution as ProtoAuthChallengeSolution, AuthResult as ProtoAuthResult, AuthChallengeSolution as ProtoAuthChallengeSolution, AuthResult as ProtoAuthResult,
ClientInfo as ProtoClientInfo, ClientRequest, ClientResponse, ClientRequest, ClientResponse, 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, Receiver, Sender, grpc::GrpcBi} },
transport::{Bi, Error as TransportError, Receiver, Sender, grpc::GrpcBi},
}; };
use async_trait::async_trait; use async_trait::async_trait;
use tonic::Status; use tonic::Status;
@@ -19,16 +19,19 @@ use crate::{
pub struct AuthTransportAdapter<'a> { pub struct AuthTransportAdapter<'a> {
bi: &'a mut GrpcBi<ClientRequest, ClientResponse>, bi: &'a mut GrpcBi<ClientRequest, ClientResponse>,
request_tracker: &'a mut RequestTracker, request_tracker: &'a mut RequestTracker,
response_id: &'a mut Option<i32>,
} }
impl<'a> AuthTransportAdapter<'a> { impl<'a> AuthTransportAdapter<'a> {
pub fn new( pub fn new(
bi: &'a mut GrpcBi<ClientRequest, ClientResponse>, bi: &'a mut GrpcBi<ClientRequest, ClientResponse>,
request_tracker: &'a mut RequestTracker, request_tracker: &'a mut RequestTracker,
response_id: &'a mut Option<i32>,
) -> Self { ) -> Self {
Self { Self {
bi, bi,
request_tracker, request_tracker,
response_id,
} }
} }
@@ -54,7 +57,7 @@ impl<'a> AuthTransportAdapter<'a> {
ProtoAuthResult::ApprovalDenied ProtoAuthResult::ApprovalDenied
} }
auth::Error::ApproveError(auth::ApproveError::Upstream( auth::Error::ApproveError(auth::ApproveError::Upstream(
crate::actors::flow_coordinator::ApprovalError::NoUserAgentsConnected, crate::actors::router::ApprovalError::NoUserAgentsConnected,
)) => ProtoAuthResult::NoUserAgentsOnline, )) => ProtoAuthResult::NoUserAgentsOnline,
auth::Error::ApproveError(auth::ApproveError::Internal) auth::Error::ApproveError(auth::ApproveError::Internal)
| auth::Error::DatabasePoolUnavailable | auth::Error::DatabasePoolUnavailable
@@ -69,9 +72,11 @@ impl<'a> AuthTransportAdapter<'a> {
&mut self, &mut self,
payload: ClientResponsePayload, payload: ClientResponsePayload,
) -> Result<(), TransportError> { ) -> Result<(), TransportError> {
let request_id = self.response_id.take();
self.bi self.bi
.send(Ok(ClientResponse { .send(Ok(ClientResponse {
request_id: Some(self.request_tracker.current_request_id()), request_id,
payload: Some(payload), payload: Some(payload),
})) }))
.await .await
@@ -109,27 +114,19 @@ impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
} }
}; };
match self.request_tracker.request(request.request_id) { let request_id = match self.request_tracker.request(request.request_id) {
Ok(request_id) => request_id, Ok(request_id) => request_id,
Err(error) => { Err(error) => {
let _ = self.bi.send(Err(error)).await; let _ = self.bi.send(Err(error)).await;
return None; return None;
} }
}; };
*self.response_id = Some(request_id);
let payload = request.payload?; let payload = request.payload?;
match payload { match payload {
ClientRequestPayload::AuthChallengeRequest(ProtoAuthChallengeRequest { ClientRequestPayload::AuthChallengeRequest(ProtoAuthChallengeRequest { pubkey }) => {
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 Ok(pubkey) = <[u8; 32]>::try_from(pubkey) else {
let _ = self.send_auth_result(ProtoAuthResult::InvalidKey).await; let _ = self.send_auth_result(ProtoAuthResult::InvalidKey).await;
return None; return None;
@@ -138,10 +135,7 @@ impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
let _ = self.send_auth_result(ProtoAuthResult::InvalidKey).await; let _ = self.send_auth_result(ProtoAuthResult::InvalidKey).await;
return None; return None;
}; };
Some(auth::Inbound::AuthChallengeRequest { Some(auth::Inbound::AuthChallengeRequest { pubkey })
pubkey,
metadata: client_metadata_from_proto(client_info),
})
} }
ClientRequestPayload::AuthChallengeSolution(ProtoAuthChallengeSolution { ClientRequestPayload::AuthChallengeSolution(ProtoAuthChallengeSolution {
signature, signature,
@@ -157,9 +151,7 @@ impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
_ => { _ => {
let _ = self let _ = self
.bi .bi
.send(Err(Status::invalid_argument( .send(Err(Status::invalid_argument("Unsupported client auth request")))
"Unsupported client auth request",
)))
.await; .await;
None None
} }
@@ -169,20 +161,13 @@ impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
impl Bi<auth::Inbound, Result<auth::Outbound, auth::Error>> for AuthTransportAdapter<'_> {} 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( pub async fn start(
conn: &mut ClientConnection, conn: &mut ClientConnection,
bi: &mut GrpcBi<ClientRequest, ClientResponse>, bi: &mut GrpcBi<ClientRequest, ClientResponse>,
request_tracker: &mut RequestTracker, request_tracker: &mut RequestTracker,
response_id: &mut Option<i32>,
) -> Result<(), auth::Error> { ) -> Result<(), auth::Error> {
let mut transport = AuthTransportAdapter::new(bi, request_tracker); let mut transport = AuthTransportAdapter::new(bi, request_tracker, response_id);
client::auth::authenticate(conn, &mut transport).await?; client::auth::authenticate(conn, &mut transport).await?;
Ok(()) Ok(())
} }

View File

@@ -18,19 +18,6 @@ pub mod client;
mod request_tracker; 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>>;

View File

@@ -17,10 +17,4 @@ impl RequestTracker {
Ok(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
}
} }

View File

@@ -1,30 +1,29 @@
use tokio::sync::mpsc; use tokio::sync::mpsc;
use arbiter_proto::{ use arbiter_proto::{
google::protobuf::{Empty as ProtoEmpty, Timestamp as ProtoTimestamp},
proto::{ proto::{
client::ClientInfo as ProtoClientMetadata,
evm::{ evm::{
EvmError as ProtoEvmError, EvmGrantCreateRequest, EvmGrantCreateResponse, EtherTransferSettings as ProtoEtherTransferSettings, EvmError as ProtoEvmError,
EvmGrantDeleteRequest, EvmGrantDeleteResponse, EvmGrantList, EvmGrantListResponse, EvmGrantCreateRequest, EvmGrantCreateResponse, EvmGrantDeleteRequest,
GrantEntry, WalletCreateResponse, WalletEntry, WalletList, WalletListResponse, EvmGrantDeleteResponse, EvmGrantList, EvmGrantListResponse, GrantEntry,
evm_grant_create_response::Result as EvmGrantCreateResult, SharedSettings as ProtoSharedSettings, SpecificGrant as ProtoSpecificGrant,
TokenTransferSettings as ProtoTokenTransferSettings,
TransactionRateLimit as ProtoTransactionRateLimit,
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::{
BootstrapEncryptedKey as ProtoBootstrapEncryptedKey, BootstrapEncryptedKey as ProtoBootstrapEncryptedKey,
BootstrapResult as ProtoBootstrapResult, ListWalletAccessResponse, BootstrapResult as ProtoBootstrapResult,
SdkClientConnectionCancel as ProtoSdkClientConnectionCancel, SdkClientConnectionResponse as ProtoSdkClientConnectionResponse,
SdkClientConnectionRequest as ProtoSdkClientConnectionRequest, UnsealEncryptedKey as ProtoUnsealEncryptedKey, UnsealResult as ProtoUnsealResult,
SdkClientEntry as ProtoSdkClientEntry, SdkClientError as ProtoSdkClientError, UnsealStart, UserAgentRequest, UserAgentResponse, VaultState as ProtoVaultState,
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_request::Payload as UserAgentRequestPayload,
user_agent_response::Payload as UserAgentResponsePayload, user_agent_response::Payload as UserAgentResponsePayload,
}, },
@@ -32,33 +31,35 @@ use arbiter_proto::{
transport::{Error as TransportError, Receiver, Sender, grpc::GrpcBi}, transport::{Error as TransportError, Receiver, Sender, grpc::GrpcBi},
}; };
use async_trait::async_trait; use async_trait::async_trait;
use chrono::{TimeZone, Utc};
use kameo::{ use kameo::{
actor::{ActorRef, Spawn as _}, actor::{ActorRef, Spawn as _},
error::SendError, error::SendError,
}; };
use tonic::Status; use tonic::Status;
use tracing::{error, info, warn}; use tracing::{info, warn};
use crate::{ use crate::{
actors::{ actors::{
keyholder::KeyHolderState, keyholder::KeyHolderState,
user_agent::{ user_agent::{
OutOfBand, UserAgentConnection, UserAgentSession, OutOfBand, UserAgentConnection, UserAgentSession,
session::connection::{ session::{
BootstrapError, HandleBootstrapEncryptedKey, HandleEvmWalletCreate, BootstrapError, Error, HandleBootstrapEncryptedKey, HandleEvmWalletCreate,
HandleEvmWalletList, HandleGrantCreate, HandleGrantDelete, HandleEvmWalletList, HandleGrantCreate, HandleGrantDelete, HandleGrantList,
HandleGrantEvmWalletAccess, HandleGrantList, HandleListWalletAccess, HandleQueryVaultState, HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError,
HandleNewClientApprove, HandleQueryVaultState, HandleRevokeEvmWalletAccess,
HandleSdkClientList, HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError,
}, },
}, },
}, },
db::models::{CoreEvmWalletAccess, NewEvmWalletAccess}, evm::policies::{
grpc::{Convert, TryConvert, request_tracker::RequestTracker}, Grant, SharedGrantSettings, SpecificGrant, TransactionRateLimit, VolumeRateLimit,
ether_transfer, token_transfers,
},
grpc::request_tracker::RequestTracker,
utils::defer,
}; };
use alloy::primitives::{Address, U256};
mod auth; mod auth;
mod inbound;
mod outbound;
pub struct OutOfBandAdapter(mpsc::Sender<OutOfBand>); pub struct OutOfBandAdapter(mpsc::Sender<OutOfBand>);
@@ -82,109 +83,94 @@ async fn dispatch_loop(
tokio::select! { tokio::select! {
oob = receiver.recv() => { oob = receiver.recv() => {
let Some(oob) = oob else { let Some(oob) = oob else {
warn!("Out-of-band message channel closed");
return; return;
}; };
let payload = match oob { if send_out_of_band(&mut bi, oob).await.is_err() {
OutOfBand::ClientConnectionRequest { profile } => {
UserAgentResponsePayload::SdkClientConnectionRequest(ProtoSdkClientConnectionRequest {
pubkey: profile.pubkey.to_bytes().to_vec(),
info: Some(ProtoClientMetadata {
name: profile.metadata.name,
description: profile.metadata.description,
version: profile.metadata.version,
}),
})
}
OutOfBand::ClientConnectionCancel { pubkey } => {
UserAgentResponsePayload::SdkClientConnectionCancel(ProtoSdkClientConnectionCancel {
pubkey: pubkey.to_bytes().to_vec(),
})
}
};
if bi.send(Ok(UserAgentResponse { id: None, payload: Some(payload) })).await.is_err() {
return; return;
} }
} }
message = bi.recv() => { conn = bi.recv() => {
let Some(message) = message else { return; }; let Some(conn) = conn else {
return;
};
let conn = match message { if dispatch_conn_message(&mut bi, &actor, &mut request_tracker, conn)
.await
.is_err()
{
return;
}
}
}
}
}
async fn dispatch_conn_message(
bi: &mut GrpcBi<UserAgentRequest, UserAgentResponse>,
actor: &ActorRef<UserAgentSession>,
request_tracker: &mut RequestTracker,
conn: Result<UserAgentRequest, Status>,
) -> Result<(), ()> {
let conn = match conn {
Ok(conn) => conn, Ok(conn) => conn,
Err(err) => { Err(err) => {
warn!(error = ?err, "Failed to receive user agent request"); warn!(error = ?err, "Failed to receive user agent request");
return; return Err(());
} }
}; };
let request_id = match request_tracker.request(conn.id) { let request_id = match request_tracker.request(conn.id) {
Ok(id) => id, Ok(request_id) => request_id,
Err(err) => { Err(err) => {
let _ = bi.send(Err(err)).await; let _ = bi.send(Err(err)).await;
return; return Err(());
} }
}; };
let Some(payload) = conn.payload else { let Some(payload) = conn.payload else {
let _ = bi.send(Err(Status::invalid_argument("Missing user-agent request payload"))).await; let _ = bi
return; .send(Err(Status::invalid_argument(
"Missing user-agent request payload",
)))
.await;
return Err(());
}; };
match dispatch_inner(&actor, payload).await { let payload = match payload {
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 }) => { UserAgentRequestPayload::UnsealStart(UnsealStart { client_pubkey }) => {
let client_pubkey = <[u8; 32]>::try_from(client_pubkey) let client_pubkey = match <[u8; 32]>::try_from(client_pubkey) {
.map(x25519_dalek::PublicKey::from) Ok(bytes) => x25519_dalek::PublicKey::from(bytes),
.map_err(|_| Status::invalid_argument("Invalid X25519 public key"))?; Err(_) => {
let _ = bi
.send(Err(Status::invalid_argument("Invalid X25519 public key")))
.await;
return Err(());
}
};
let response = actor match actor.ask(HandleUnsealRequest { client_pubkey }).await {
.ask(HandleUnsealRequest { client_pubkey }) Ok(response) => UserAgentResponsePayload::UnsealStartResponse(
.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 { arbiter_proto::proto::user_agent::UnsealStartResponse {
server_pubkey: response.server_pubkey.as_bytes().to_vec(), server_pubkey: response.server_pubkey.as_bytes().to_vec(),
}, },
) ),
Err(err) => {
warn!(error = ?err, "Failed to handle unseal start request");
let _ = bi
.send(Err(Status::internal("Failed to start unseal flow")))
.await;
return Err(());
}
}
} }
UserAgentRequestPayload::UnsealEncryptedKey(ProtoUnsealEncryptedKey { UserAgentRequestPayload::UnsealEncryptedKey(ProtoUnsealEncryptedKey {
nonce, nonce,
ciphertext, ciphertext,
associated_data, associated_data,
}) => { }) => UserAgentResponsePayload::UnsealResult(
let result = match actor match actor
.ask(HandleUnsealEncryptedKey { .ask(HandleUnsealEncryptedKey {
nonce, nonce,
ciphertext, ciphertext,
@@ -198,18 +184,20 @@ async fn dispatch_inner(
} }
Err(err) => { Err(err) => {
warn!(error = ?err, "Failed to handle unseal request"); warn!(error = ?err, "Failed to handle unseal request");
return Err(Status::internal("Failed to unseal vault")); let _ = bi
.send(Err(Status::internal("Failed to unseal vault")))
.await;
return Err(());
} }
};
UserAgentResponsePayload::UnsealResult(result.into())
} }
.into(),
),
UserAgentRequestPayload::BootstrapEncryptedKey(ProtoBootstrapEncryptedKey { UserAgentRequestPayload::BootstrapEncryptedKey(ProtoBootstrapEncryptedKey {
nonce, nonce,
ciphertext, ciphertext,
associated_data, associated_data,
}) => { }) => UserAgentResponsePayload::BootstrapResult(
let result = match actor match actor
.ask(HandleBootstrapEncryptedKey { .ask(HandleBootstrapEncryptedKey {
nonce, nonce,
ciphertext, ciphertext,
@@ -226,14 +214,16 @@ async fn dispatch_inner(
} }
Err(err) => { Err(err) => {
warn!(error = ?err, "Failed to handle bootstrap request"); warn!(error = ?err, "Failed to handle bootstrap request");
return Err(Status::internal("Failed to bootstrap vault")); let _ = bi
.send(Err(Status::internal("Failed to bootstrap vault")))
.await;
return Err(());
} }
};
UserAgentResponsePayload::BootstrapResult(result.into())
} }
.into(),
UserAgentRequestPayload::QueryVaultState(_) => { ),
let state = match actor.ask(HandleQueryVaultState {}).await { UserAgentRequestPayload::QueryVaultState(_) => UserAgentResponsePayload::VaultState(
match actor.ask(HandleQueryVaultState {}).await {
Ok(KeyHolderState::Unbootstrapped) => ProtoVaultState::Unbootstrapped, Ok(KeyHolderState::Unbootstrapped) => ProtoVaultState::Unbootstrapped,
Ok(KeyHolderState::Sealed) => ProtoVaultState::Sealed, Ok(KeyHolderState::Sealed) => ProtoVaultState::Sealed,
Ok(KeyHolderState::Unsealed) => ProtoVaultState::Unsealed, Ok(KeyHolderState::Unsealed) => ProtoVaultState::Unsealed,
@@ -241,34 +231,277 @@ async fn dispatch_inner(
warn!(error = ?err, "Failed to query vault state"); warn!(error = ?err, "Failed to query vault state");
ProtoVaultState::Error ProtoVaultState::Error
} }
}
.into(),
),
UserAgentRequestPayload::EvmWalletCreate(_) => UserAgentResponsePayload::EvmWalletCreate(
EvmGrantOrWallet::wallet_create_response(actor.ask(HandleEvmWalletCreate {}).await),
),
UserAgentRequestPayload::EvmWalletList(_) => UserAgentResponsePayload::EvmWalletList(
EvmGrantOrWallet::wallet_list_response(actor.ask(HandleEvmWalletList {}).await),
),
UserAgentRequestPayload::EvmGrantList(_) => UserAgentResponsePayload::EvmGrantList(
EvmGrantOrWallet::grant_list_response(actor.ask(HandleGrantList {}).await),
),
UserAgentRequestPayload::EvmGrantCreate(EvmGrantCreateRequest {
client_id,
shared,
specific,
}) => {
let (basic, grant) = match parse_grant_request(shared, specific) {
Ok(values) => values,
Err(status) => {
let _ = bi.send(Err(status)).await;
return Err(());
}
}; };
UserAgentResponsePayload::VaultState(state.into())
UserAgentResponsePayload::EvmGrantCreate(EvmGrantOrWallet::grant_create_response(
actor
.ask(HandleGrantCreate {
client_id,
basic,
grant,
})
.await,
))
}
UserAgentRequestPayload::EvmGrantDelete(EvmGrantDeleteRequest { grant_id }) => {
UserAgentResponsePayload::EvmGrantDelete(EvmGrantOrWallet::grant_delete_response(
actor.ask(HandleGrantDelete { grant_id }).await,
))
}
payload => {
warn!(?payload, "Unsupported post-auth user agent request");
let _ = bi
.send(Err(Status::invalid_argument(
"Unsupported user-agent request",
)))
.await;
return Err(());
}
};
bi.send(Ok(UserAgentResponse {
id: Some(request_id),
payload: Some(payload),
}))
.await
.map_err(|_| ())
} }
UserAgentRequestPayload::EvmWalletCreate(_) => { async fn send_out_of_band(
let result = match actor.ask(HandleEvmWalletCreate {}).await { bi: &mut GrpcBi<UserAgentRequest, UserAgentResponse>,
Ok((wallet_id, address)) => WalletCreateResult::Wallet(WalletEntry { oob: OutOfBand,
id: wallet_id, ) -> Result<(), ()> {
address: address.to_vec(), let payload = match oob {
// The current protobuf response payload carries only an approval boolean.
// Keep emitting this shape until a dedicated out-of-band request/cancel payload
// is reintroduced in the protocol definition.
OutOfBand::ClientConnectionRequest { pubkey: _ } => {
UserAgentResponsePayload::SdkClientConnectionResponse(
ProtoSdkClientConnectionResponse { approved: false },
)
}
OutOfBand::ClientConnectionCancel => UserAgentResponsePayload::SdkClientConnectionResponse(
ProtoSdkClientConnectionResponse { approved: false },
),
};
bi.send(Ok(UserAgentResponse {
id: None,
payload: Some(payload),
}))
.await
.map_err(|_| ())
}
fn parse_grant_request(
shared: Option<ProtoSharedSettings>,
specific: Option<ProtoSpecificGrant>,
) -> Result<(SharedGrantSettings, SpecificGrant), Status> {
let shared = shared.ok_or_else(|| Status::invalid_argument("Missing shared grant settings"))?;
let specific =
specific.ok_or_else(|| Status::invalid_argument("Missing specific grant settings"))?;
Ok((
shared_settings_from_proto(shared)?,
specific_grant_from_proto(specific)?,
))
}
fn shared_settings_from_proto(shared: ProtoSharedSettings) -> Result<SharedGrantSettings, Status> {
Ok(SharedGrantSettings {
wallet_id: shared.wallet_id,
client_id: 0,
chain: shared.chain_id,
valid_from: shared.valid_from.map(proto_timestamp_to_utc).transpose()?,
valid_until: shared.valid_until.map(proto_timestamp_to_utc).transpose()?,
max_gas_fee_per_gas: shared
.max_gas_fee_per_gas
.as_deref()
.map(u256_from_proto_bytes)
.transpose()?,
max_priority_fee_per_gas: shared
.max_priority_fee_per_gas
.as_deref()
.map(u256_from_proto_bytes)
.transpose()?,
rate_limit: shared.rate_limit.map(|limit| TransactionRateLimit {
count: limit.count,
window: chrono::Duration::seconds(limit.window_secs),
}),
})
}
fn specific_grant_from_proto(specific: ProtoSpecificGrant) -> Result<SpecificGrant, Status> {
match specific.grant {
Some(ProtoSpecificGrantType::EtherTransfer(ProtoEtherTransferSettings {
targets,
limit,
})) => Ok(SpecificGrant::EtherTransfer(ether_transfer::Settings {
target: targets
.into_iter()
.map(address_from_bytes)
.collect::<Result<_, _>>()?,
limit: volume_rate_limit_from_proto(limit.ok_or_else(|| {
Status::invalid_argument("Missing ether transfer volume rate limit")
})?)?,
})),
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(volume_rate_limit_from_proto)
.collect::<Result<_, _>>()?,
})),
None => Err(Status::invalid_argument("Missing specific grant kind")),
}
}
fn volume_rate_limit_from_proto(limit: ProtoVolumeRateLimit) -> Result<VolumeRateLimit, Status> {
Ok(VolumeRateLimit {
max_volume: u256_from_proto_bytes(&limit.max_volume)?,
window: chrono::Duration::seconds(limit.window_secs),
})
}
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))
}
fn proto_timestamp_to_utc(timestamp: ProtoTimestamp) -> Result<chrono::DateTime<Utc>, Status> {
Utc.timestamp_opt(timestamp.seconds, timestamp.nanos as u32)
.single()
.ok_or_else(|| Status::invalid_argument("Invalid timestamp"))
}
fn shared_settings_to_proto(shared: SharedGrantSettings) -> ProtoSharedSettings {
ProtoSharedSettings {
wallet_id: shared.wallet_id,
chain_id: shared.chain,
valid_from: shared.valid_from.map(|time| ProtoTimestamp {
seconds: time.timestamp(),
nanos: time.timestamp_subsec_nanos() as i32,
}),
valid_until: shared.valid_until.map(|time| ProtoTimestamp {
seconds: time.timestamp(),
nanos: time.timestamp_subsec_nanos() as i32,
}),
max_gas_fee_per_gas: shared
.max_gas_fee_per_gas
.map(|value| value.to_be_bytes::<32>().to_vec()),
max_priority_fee_per_gas: shared
.max_priority_fee_per_gas
.map(|value| value.to_be_bytes::<32>().to_vec()),
rate_limit: shared.rate_limit.map(|limit| ProtoTransactionRateLimit {
count: limit.count,
window_secs: limit.window.num_seconds(),
}),
}
}
fn specific_grant_to_proto(grant: SpecificGrant) -> ProtoSpecificGrant {
let grant = match grant {
SpecificGrant::EtherTransfer(settings) => {
ProtoSpecificGrantType::EtherTransfer(ProtoEtherTransferSettings {
targets: settings
.target
.into_iter()
.map(|address| address.to_vec())
.collect(),
limit: Some(ProtoVolumeRateLimit {
max_volume: settings.limit.max_volume.to_be_bytes::<32>().to_vec(),
window_secs: settings.limit.window.num_seconds(),
}),
})
}
SpecificGrant::TokenTransfer(settings) => {
ProtoSpecificGrantType::TokenTransfer(ProtoTokenTransferSettings {
token_contract: settings.token_contract.to_vec(),
target: settings.target.map(|address| address.to_vec()),
volume_limits: settings
.volume_limits
.into_iter()
.map(|limit| ProtoVolumeRateLimit {
max_volume: limit.max_volume.to_be_bytes::<32>().to_vec(),
window_secs: limit.window.num_seconds(),
})
.collect(),
})
}
};
ProtoSpecificGrant { grant: Some(grant) }
}
struct EvmGrantOrWallet;
impl EvmGrantOrWallet {
fn wallet_create_response<M>(
result: Result<Address, SendError<M, Error>>,
) -> WalletCreateResponse {
let result = match result {
Ok(wallet) => WalletCreateResult::Wallet(WalletEntry {
address: wallet.to_vec(),
}), }),
Err(err) => { Err(err) => {
warn!(error = ?err, "Failed to create EVM wallet"); warn!(error = ?err, "Failed to create EVM wallet");
WalletCreateResult::Error(ProtoEvmError::Internal.into()) WalletCreateResult::Error(ProtoEvmError::Internal.into())
} }
}; };
UserAgentResponsePayload::EvmWalletCreate(WalletCreateResponse {
WalletCreateResponse {
result: Some(result), result: Some(result),
}) }
} }
UserAgentRequestPayload::EvmWalletList(_) => { fn wallet_list_response<M>(
let result = match actor.ask(HandleEvmWalletList {}).await { result: Result<Vec<Address>, SendError<M, Error>>,
) -> WalletListResponse {
let result = match result {
Ok(wallets) => WalletListResult::Wallets(WalletList { Ok(wallets) => WalletListResult::Wallets(WalletList {
wallets: wallets wallets: wallets
.into_iter() .into_iter()
.map(|(id, address)| WalletEntry { .map(|wallet| WalletEntry {
address: address.to_vec(), address: wallet.to_vec(),
id,
}) })
.collect(), .collect(),
}), }),
@@ -277,21 +510,54 @@ async fn dispatch_inner(
WalletListResult::Error(ProtoEvmError::Internal.into()) WalletListResult::Error(ProtoEvmError::Internal.into())
} }
}; };
UserAgentResponsePayload::EvmWalletList(WalletListResponse {
WalletListResponse {
result: Some(result), result: Some(result),
}) }
} }
UserAgentRequestPayload::EvmGrantList(_) => { fn grant_create_response<M>(
let result = match actor.ask(HandleGrantList {}).await { result: Result<i32, SendError<M, Error>>,
) -> EvmGrantCreateResponse {
let result = match result {
Ok(grant_id) => EvmGrantCreateResult::GrantId(grant_id),
Err(err) => {
warn!(error = ?err, "Failed to create EVM grant");
EvmGrantCreateResult::Error(ProtoEvmError::Internal.into())
}
};
EvmGrantCreateResponse {
result: Some(result),
}
}
fn grant_delete_response<M>(result: Result<(), SendError<M, Error>>) -> EvmGrantDeleteResponse {
let result = match result {
Ok(()) => EvmGrantDeleteResult::Ok(ProtoEmpty {}),
Err(err) => {
warn!(error = ?err, "Failed to delete EVM grant");
EvmGrantDeleteResult::Error(ProtoEvmError::Internal.into())
}
};
EvmGrantDeleteResponse {
result: Some(result),
}
}
fn grant_list_response<M>(
result: Result<Vec<Grant<SpecificGrant>>, SendError<M, Error>>,
) -> EvmGrantListResponse {
let result = match result {
Ok(grants) => EvmGrantListResult::Grants(EvmGrantList { Ok(grants) => EvmGrantListResult::Grants(EvmGrantList {
grants: grants grants: grants
.into_iter() .into_iter()
.map(|grant| GrantEntry { .map(|grant| GrantEntry {
id: grant.id, id: grant.id,
wallet_access_id: grant.shared.wallet_access_id, client_id: grant.shared.client_id,
shared: Some(grant.shared.convert()), shared: Some(shared_settings_to_proto(grant.shared)),
specific: Some(grant.settings.convert()), specific: Some(specific_grant_to_proto(grant.settings)),
}) })
.collect(), .collect(),
}), }),
@@ -300,152 +566,22 @@ async fn dispatch_inner(
EvmGrantListResult::Error(ProtoEvmError::Internal.into()) EvmGrantListResult::Error(ProtoEvmError::Internal.into())
} }
}; };
UserAgentResponsePayload::EvmGrantList(EvmGrantListResponse {
EvmGrantListResponse {
result: Some(result), result: Some(result),
})
}
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())
}
};
UserAgentResponsePayload::EvmGrantCreate(EvmGrantCreateResponse {
result: Some(result),
})
}
UserAgentRequestPayload::EvmGrantDelete(EvmGrantDeleteRequest { grant_id }) => {
let result = match actor.ask(HandleGrantDelete { grant_id }).await {
Ok(()) => EvmGrantDeleteResult::Ok(()),
Err(err) => {
warn!(error = ?err, "Failed to delete EVM grant");
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(),
}),
Err(err) => {
warn!(error = ?err, "Failed to list SDK clients");
ProtoSdkClientListResult::Error(ProtoSdkClientError::Internal.into())
}
};
UserAgentResponsePayload::SdkClientListResponse(ProtoSdkClientListResponse {
result: Some(result),
})
}
UserAgentRequestPayload::GrantWalletAccess(SdkClientGrantWalletAccess { accesses }) => {
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);
}
Err(err) => {
warn!(error = ?err, "Failed to grant wallet access");
return Err(Status::internal("Failed to grant wallet access"));
} }
} }
} }
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))
}
pub async fn start( pub async fn start(
mut conn: UserAgentConnection, mut conn: UserAgentConnection,
mut bi: GrpcBi<UserAgentRequest, UserAgentResponse>, mut bi: GrpcBi<UserAgentRequest, UserAgentResponse>,
) { ) {
let mut request_tracker = RequestTracker::default(); let mut request_tracker = RequestTracker::default();
let mut response_id = None;
let pubkey = match auth::start(&mut conn, &mut bi, &mut request_tracker).await { let pubkey = match auth::start(&mut conn, &mut bi, &mut request_tracker, &mut response_id).await
{
Ok(pubkey) => pubkey, Ok(pubkey) => pubkey,
Err(e) => { Err(e) => {
warn!(error = ?e, "Authentication failed"); warn!(error = ?e, "Authentication failed");
@@ -459,7 +595,10 @@ pub async fn start(
let actor = UserAgentSession::spawn(UserAgentSession::new(conn, Box::new(oob_adapter))); let actor = UserAgentSession::spawn(UserAgentSession::new(conn, Box::new(oob_adapter)));
let actor_for_cleanup = actor.clone(); let actor_for_cleanup = actor.clone();
let _ = defer(move || {
actor_for_cleanup.kill();
});
info!(?pubkey, "User authenticated successfully"); info!(?pubkey, "User authenticated successfully");
dispatch_loop(bi, actor, oob_receiver, request_tracker).await; dispatch_loop(bi, actor, oob_receiver, request_tracker).await;
actor_for_cleanup.kill();
} }

View File

@@ -21,16 +21,19 @@ use crate::{
pub struct AuthTransportAdapter<'a> { pub struct AuthTransportAdapter<'a> {
bi: &'a mut GrpcBi<UserAgentRequest, UserAgentResponse>, bi: &'a mut GrpcBi<UserAgentRequest, UserAgentResponse>,
request_tracker: &'a mut RequestTracker, request_tracker: &'a mut RequestTracker,
response_id: &'a mut Option<i32>,
} }
impl<'a> AuthTransportAdapter<'a> { impl<'a> AuthTransportAdapter<'a> {
pub fn new( pub fn new(
bi: &'a mut GrpcBi<UserAgentRequest, UserAgentResponse>, bi: &'a mut GrpcBi<UserAgentRequest, UserAgentResponse>,
request_tracker: &'a mut RequestTracker, request_tracker: &'a mut RequestTracker,
response_id: &'a mut Option<i32>,
) -> Self { ) -> Self {
Self { Self {
bi, bi,
request_tracker, request_tracker,
response_id,
} }
} }
@@ -38,9 +41,11 @@ impl<'a> AuthTransportAdapter<'a> {
&mut self, &mut self,
payload: UserAgentResponsePayload, payload: UserAgentResponsePayload,
) -> Result<(), TransportError> { ) -> Result<(), TransportError> {
let id = self.response_id.take();
self.bi self.bi
.send(Ok(UserAgentResponse { .send(Ok(UserAgentResponse {
id: Some(self.request_tracker.current_request_id()), id,
payload: Some(payload), payload: Some(payload),
})) }))
.await .await
@@ -70,14 +75,9 @@ impl Sender<Result<auth::Outbound, auth::Error>> for AuthTransportAdapter<'_> {
Err(Error::InvalidBootstrapToken) => { Err(Error::InvalidBootstrapToken) => {
UserAgentResponsePayload::AuthResult(ProtoAuthResult::TokenInvalid.into()) UserAgentResponsePayload::AuthResult(ProtoAuthResult::TokenInvalid.into())
} }
Err(Error::Internal { details }) => { Err(Error::Internal { details }) => return self.bi.send(Err(Status::internal(details))).await,
return self.bi.send(Err(Status::internal(details))).await;
}
Err(Error::Transport) => { Err(Error::Transport) => {
return self return self.bi.send(Err(Status::unavailable("transport error"))).await;
.bi
.send(Err(Status::unavailable("transport error")))
.await;
} }
}; };
@@ -96,13 +96,14 @@ impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
} }
}; };
match self.request_tracker.request(request.id) { let request_id = match self.request_tracker.request(request.id) {
Ok(request_id) => request_id, Ok(request_id) => request_id,
Err(error) => { Err(error) => {
let _ = self.bi.send(Err(error)).await; let _ = self.bi.send(Err(error)).await;
return None; return None;
} }
}; };
*self.response_id = Some(request_id);
let Some(payload) = request.payload else { let Some(payload) = request.payload else {
warn!( warn!(
@@ -172,7 +173,8 @@ pub async fn start(
conn: &mut UserAgentConnection, conn: &mut UserAgentConnection,
bi: &mut GrpcBi<UserAgentRequest, UserAgentResponse>, bi: &mut GrpcBi<UserAgentRequest, UserAgentResponse>,
request_tracker: &mut RequestTracker, request_tracker: &mut RequestTracker,
response_id: &mut Option<i32>,
) -> Result<AuthPublicKey, auth::Error> { ) -> Result<AuthPublicKey, auth::Error> {
let transport = AuthTransportAdapter::new(bi, request_tracker); let transport = AuthTransportAdapter::new(bi, request_tracker, response_id);
auth::authenticate(conn, transport).await auth::authenticate(conn, transport).await
} }

View File

@@ -1,168 +0,0 @@
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,
})
}
}

View File

@@ -1,111 +0,0 @@
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,
}),
}
}
}

View File

@@ -1,52 +1,15 @@
use arbiter_proto::ClientMetadata;
use arbiter_proto::transport::{Receiver, Sender}; 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, auth, connect_client}, actors::client::{ClientConnection, auth, connect_client},
db, db::{self, schema},
}; };
use diesel::{ExpressionMethods as _, NullableExpressionMethods as _, QueryDsl as _, insert_into}; use diesel::{ExpressionMethods 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() {
@@ -65,7 +28,6 @@ pub async fn test_unregistered_pubkey_rejected() {
test_transport test_transport
.send(auth::Inbound::AuthChallengeRequest { .send(auth::Inbound::AuthChallengeRequest {
pubkey: new_key.verifying_key(), pubkey: new_key.verifying_key(),
metadata: metadata("client", Some("desc"), Some("1.0.0")),
}) })
.await .await
.unwrap(); .unwrap();
@@ -82,12 +44,14 @@ 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( {
&db, let mut conn = db.get().await.unwrap();
pubkey_bytes.clone(), insert_into(schema::program_client::table)
&metadata("client", Some("desc"), Some("1.0.0")), .values(schema::program_client::public_key.eq(pubkey_bytes.clone()))
) .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();
@@ -102,7 +66,6 @@ pub async fn test_challenge_auth() {
test_transport test_transport
.send(auth::Inbound::AuthChallengeRequest { .send(auth::Inbound::AuthChallengeRequest {
pubkey: new_key.verifying_key(), pubkey: new_key.verifying_key(),
metadata: metadata("client", Some("desc"), Some("1.0.0")),
}) })
.await .await
.unwrap(); .unwrap();
@@ -143,182 +106,3 @@ pub async fn test_challenge_auth() {
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())
)
);
}
}

View File

@@ -3,11 +3,9 @@ use arbiter_server::{
actors::{ actors::{
GlobalActors, GlobalActors,
bootstrap::GetToken, bootstrap::GetToken,
keyholder::Bootstrap,
user_agent::{AuthPublicKey, UserAgentConnection, auth}, user_agent::{AuthPublicKey, UserAgentConnection, auth},
}, },
db::{self, schema}, db::{self, schema},
safe_cell::{SafeCell, SafeCellHandle as _},
}; };
use diesel::{ExpressionMethods as _, QueryDsl, insert_into}; use diesel::{ExpressionMethods as _, QueryDsl, insert_into};
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
@@ -167,120 +165,3 @@ pub async fn test_challenge_auth() {
task.await.unwrap().unwrap(); task.await.unwrap().unwrap();
} }
#[tokio::test]
#[test_log::test]
pub async fn test_challenge_auth_rejects_integrity_tag_mismatch_when_unsealed() {
let db = db::create_test_pool().await;
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
actors
.key_holder
.ask(Bootstrap {
seal_key_raw: SafeCell::new(b"test-seal-key".to_vec()),
})
.await
.unwrap();
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec();
{
let mut conn = db.get().await.unwrap();
insert_into(schema::useragent_client::table)
.values((
schema::useragent_client::public_key.eq(pubkey_bytes.clone()),
schema::useragent_client::key_type.eq(1i32),
schema::useragent_client::pubkey_integrity_tag.eq(Some(vec![0u8; 32])),
))
.execute(&mut conn)
.await
.unwrap();
}
let (server_transport, mut test_transport) = ChannelTransport::new();
let db_for_task = db.clone();
let task = tokio::spawn(async move {
let mut props = UserAgentConnection::new(db_for_task, actors);
auth::authenticate(&mut props, server_transport).await
});
test_transport
.send(auth::Inbound::AuthChallengeRequest {
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
bootstrap_token: None,
})
.await
.unwrap();
assert!(matches!(
task.await.unwrap(),
Err(auth::Error::InvalidChallengeSolution)
));
}
#[tokio::test]
#[test_log::test]
pub async fn test_challenge_auth_rejects_invalid_signature() {
let db = db::create_test_pool().await;
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec();
// Pre-register key with key_type
{
let mut conn = db.get().await.unwrap();
insert_into(schema::useragent_client::table)
.values((
schema::useragent_client::public_key.eq(pubkey_bytes.clone()),
schema::useragent_client::key_type.eq(1i32),
))
.execute(&mut conn)
.await
.unwrap();
}
let (server_transport, mut test_transport) = ChannelTransport::new();
let db_for_task = db.clone();
let task = tokio::spawn(async move {
let mut props = UserAgentConnection::new(db_for_task, actors);
auth::authenticate(&mut props, server_transport).await
});
test_transport
.send(auth::Inbound::AuthChallengeRequest {
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
bootstrap_token: None,
})
.await
.unwrap();
let response = test_transport
.recv()
.await
.expect("should receive challenge");
let challenge = match response {
Ok(resp) => match resp {
auth::Outbound::AuthChallenge { nonce } => nonce,
other => panic!("Expected AuthChallenge, got {other:?}"),
},
Err(err) => panic!("Expected Ok response, got Err({err:?})"),
};
// Sign a different challenge value so signature format is valid but verification must fail.
let wrong_challenge = arbiter_proto::format_challenge(challenge + 1, &pubkey_bytes);
let signature = new_key.sign(&wrong_challenge);
test_transport
.send(auth::Inbound::AuthChallengeSolution {
signature: signature.to_bytes().to_vec(),
})
.await
.unwrap();
assert!(matches!(
task.await.unwrap(),
Err(auth::Error::InvalidChallengeSolution)
));
}

View File

@@ -2,17 +2,14 @@ use arbiter_server::{
actors::{ actors::{
GlobalActors, GlobalActors,
keyholder::{Bootstrap, Seal}, keyholder::{Bootstrap, Seal},
user_agent::{ user_agent::session::{
UserAgentSession, HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError, 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 diesel::{ExpressionMethods as _, QueryDsl as _, insert_into};
use diesel_async::RunQueryDsl;
use kameo::actor::Spawn as _; use kameo::actor::Spawn as _;
use x25519_dalek::{EphemeralSecret, PublicKey}; use x25519_dalek::{EphemeralSecret, PublicKey};
@@ -152,42 +149,3 @@ pub async fn test_unseal_retry_after_invalid_key() {
assert!(matches!(response, Ok(()))); assert!(matches!(response, Ok(())));
} }
} }
#[tokio::test]
#[test_log::test]
pub async fn test_unseal_backfills_missing_pubkey_integrity_tags() {
let seal_key = b"test-seal-key";
let (db, user_agent) = setup_sealed_user_agent(seal_key).await;
{
let mut conn = db.get().await.unwrap();
insert_into(arbiter_server::db::schema::useragent_client::table)
.values((
arbiter_server::db::schema::useragent_client::public_key
.eq(vec![1u8, 2u8, 3u8, 4u8]),
arbiter_server::db::schema::useragent_client::key_type.eq(1i32),
arbiter_server::db::schema::useragent_client::pubkey_integrity_tag
.eq(Option::<Vec<u8>>::None),
))
.execute(&mut conn)
.await
.unwrap();
}
let encrypted_key = client_dh_encrypt(&user_agent, seal_key).await;
let response = user_agent.ask(encrypted_key).await;
assert!(matches!(response, Ok(())));
{
let mut conn = db.get().await.unwrap();
let tags: Vec<Option<Vec<u8>>> = arbiter_server::db::schema::useragent_client::table
.select(arbiter_server::db::schema::useragent_client::pubkey_integrity_tag)
.load(&mut conn)
.await
.unwrap();
assert!(
tags.iter()
.all(|tag| matches!(tag, Some(v) if v.len() == 32))
);
}
}

View File

@@ -1,16 +0,0 @@
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;
}

View File

@@ -1,304 +0,0 @@
// 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

View File

@@ -1,25 +0,0 @@
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;
}

View File

@@ -1,602 +0,0 @@
// 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

View File

@@ -1,57 +0,0 @@
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;

View File

@@ -1,67 +0,0 @@
// 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);
}
}

View File

@@ -1,99 +0,0 @@
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,
),
),
),
),
),
],
),
);
}
}

View File

@@ -1,218 +0,0 @@
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,
),
),
],
),
),
],
),
),
);
}
}

View File

@@ -1,51 +0,0 @@
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);
}

View File

@@ -1,50 +0,0 @@
// 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';

View File

@@ -66,7 +66,7 @@ Future<Connection> connectAndAuthorize(
KeyAlgorithm.ed25519 => KeyType.KEY_TYPE_ED25519, KeyAlgorithm.ed25519 => KeyType.KEY_TYPE_ED25519,
}, },
); );
final response = await connection.ask( final response = await connection.request(
UserAgentRequest(authChallengeRequest: req), UserAgentRequest(authChallengeRequest: req),
); );
talker.info( talker.info(
@@ -94,7 +94,7 @@ Future<Connection> connectAndAuthorize(
); );
final signature = await key.sign(challenge); final signature = await key.sign(challenge);
final solutionResponse = await connection.ask( final solutionResponse = await connection.request(
UserAgentRequest(authChallengeSolution: AuthChallengeSolution(signature: signature)), UserAgentRequest(authChallengeSolution: AuthChallengeSolution(signature: signature)),
); );

View File

@@ -29,7 +29,7 @@ class Connection {
Stream<UserAgentResponse> get outOfBandMessages => _outOfBandMessages.stream; Stream<UserAgentResponse> get outOfBandMessages => _outOfBandMessages.stream;
Future<UserAgentResponse> ask(UserAgentRequest message) async { Future<UserAgentResponse> request(UserAgentRequest message) async {
_ensureOpen(); _ensureOpen();
final requestId = _nextRequestId++; final requestId = _nextRequestId++;
@@ -49,23 +49,7 @@ class Connection {
return completer.future; 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);
}
}
Future<void> close() async { Future<void> close() async {
talker.debug('Closing connection...');
final rxSubscription = _rxSubscription; final rxSubscription = _rxSubscription;
if (rxSubscription == null) { if (rxSubscription == null) {
return; return;
@@ -102,7 +86,6 @@ class Connection {
} }
void _handleDone() { void _handleDone() {
talker.debug('Connection closed by server.');
if (_rxSubscription == null) { if (_rxSubscription == null) {
return; return;
} }

View File

@@ -4,7 +4,7 @@ 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 {
final response = await connection.ask( final response = await connection.request(
UserAgentRequest(evmWalletList: Empty()), UserAgentRequest(evmWalletList: Empty()),
); );
if (!response.hasEvmWalletList()) { if (!response.hasEvmWalletList()) {
@@ -25,7 +25,7 @@ Future<List<WalletEntry>> listEvmWallets(Connection connection) async {
} }
Future<void> createEvmWallet(Connection connection) async { Future<void> createEvmWallet(Connection connection) async {
final response = await connection.ask( final response = await connection.request(
UserAgentRequest(evmWalletCreate: Empty()), UserAgentRequest(evmWalletCreate: Empty()),
); );
if (!response.hasEvmWalletCreate()) { if (!response.hasEvmWalletCreate()) {

View File

@@ -4,10 +4,16 @@ 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(Connection connection) async { Future<List<GrantEntry>> listEvmGrants(
Connection connection, {
int? walletId,
}) async {
final request = EvmGrantListRequest(); final request = EvmGrantListRequest();
if (walletId != null) {
request.walletId = walletId;
}
final response = await connection.ask( final response = await connection.request(
UserAgentRequest(evmGrantList: request), UserAgentRequest(evmGrantList: request),
); );
if (!response.hasEvmGrantList()) { if (!response.hasEvmGrantList()) {
@@ -29,31 +35,52 @@ Future<List<GrantEntry>> listEvmGrants(Connection connection) async {
Future<int> createEvmGrant( Future<int> createEvmGrant(
Connection connection, { Connection connection, {
required SharedSettings sharedSettings, required int clientId,
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 {
final request = UserAgentRequest( final response = await connection.request(
UserAgentRequest(
evmGrantCreate: EvmGrantCreateRequest( evmGrantCreate: EvmGrantCreateRequest(
shared: sharedSettings, clientId: clientId,
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,
), ),
),
); );
if (!response.hasEvmGrantCreate()) {
final resp = await connection.ask(request);
if (!resp.hasEvmGrantCreate()) {
throw Exception( throw Exception(
'Expected EVM grant create response, got ${resp.whichPayload()}', 'Expected EVM grant create response, got ${response.whichPayload()}',
); );
} }
final result = resp.evmGrantCreate; final result = response.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 {
final response = await connection.ask( final response = await connection.request(
UserAgentRequest(evmGrantDelete: EvmGrantDeleteRequest(grantId: grantId)), UserAgentRequest(evmGrantDelete: EvmGrantDeleteRequest(grantId: grantId)),
); );
if (!response.hasEvmGrantDelete()) { if (!response.hasEvmGrantDelete()) {

View File

@@ -1,72 +0,0 @@
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
],
),
),
);
}
}

View File

@@ -10,7 +10,7 @@ Future<BootstrapResult> bootstrapVault(
) async { ) async {
final encryptedKey = await _encryptVaultKeyMaterial(connection, password); final encryptedKey = await _encryptVaultKeyMaterial(connection, password);
final response = await connection.ask( final response = await connection.request(
UserAgentRequest( UserAgentRequest(
bootstrapEncryptedKey: BootstrapEncryptedKey( bootstrapEncryptedKey: BootstrapEncryptedKey(
nonce: encryptedKey.nonce, nonce: encryptedKey.nonce,
@@ -31,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);
final response = await connection.ask( final response = await connection.request(
UserAgentRequest( UserAgentRequest(
unsealEncryptedKey: UnsealEncryptedKey( unsealEncryptedKey: UnsealEncryptedKey(
nonce: encryptedKey.nonce, nonce: encryptedKey.nonce,
@@ -56,7 +56,7 @@ Future<_EncryptedVaultKey> _encryptVaultKeyMaterial(
final clientKeyPair = await keyExchange.newKeyPair(); final clientKeyPair = await keyExchange.newKeyPair();
final clientPublicKey = await clientKeyPair.extractPublicKey(); final clientPublicKey = await clientKeyPair.extractPublicKey();
final handshakeResponse = await connection.ask( final handshakeResponse = await connection.request(
UserAgentRequest(unsealStart: UnsealStart(clientPubkey: clientPublicKey.bytes)), UserAgentRequest(unsealStart: UnsealStart(clientPubkey: clientPublicKey.bytes)),
); );
if (!handshakeResponse.hasUnsealStartResponse()) { if (!handshakeResponse.hasUnsealStartResponse()) {

View File

@@ -22,91 +22,12 @@ 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;
} }
@@ -125,8 +46,6 @@ 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.')
@@ -156,17 +75,6 @@ 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 {

View File

@@ -55,62 +55,18 @@ final $typed_data.Uint8List vaultStateDescriptor = $convert.base64Decode(
'VfVU5CT09UU1RSQVBQRUQQARIWChJWQVVMVF9TVEFURV9TRUFMRUQQAhIYChRWQVVMVF9TVEFU' 'VfVU5CT09UU1RSQVBQRUQQARIWChJWQVVMVF9TVEFURV9TRUFMRUQQAhIYChRWQVVMVF9TVEFU'
'RV9VTlNFQUxFRBADEhUKEVZBVUxUX1NUQVRFX0VSUk9SEAQ='); '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 = $convert.base64Decode( final $typed_data.Uint8List authChallengeRequestDescriptor =
'ChRBdXRoQ2hhbGxlbmdlUmVxdWVzdBIWCgZwdWJrZXkYASABKAxSBnB1YmtleRI7CgtjbGllbn' $convert.base64Decode(
'RfaW5mbxgCIAEoCzIaLmFyYml0ZXIuY2xpZW50LkNsaWVudEluZm9SCmNsaWVudEluZm8='); 'ChRBdXRoQ2hhbGxlbmdlUmVxdWVzdBIWCgZwdWJrZXkYASABKAxSBnB1YmtleQ==');
@$core.Deprecated('Use authChallengeDescriptor instead') @$core.Deprecated('Use authChallengeDescriptor instead')
const AuthChallenge$json = { const AuthChallenge$json = {

View File

@@ -26,11 +26,9 @@ 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;
} }
@@ -48,9 +46,8 @@ 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>>(
2, _omitFieldNames ? '' : 'address', $pb.PbFieldType.OY) 1, _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.')
@@ -73,22 +70,13 @@ class WalletEntry extends $pb.GeneratedMessage {
static WalletEntry? _defaultInstance; static WalletEntry? _defaultInstance;
@$pb.TagNumber(1) @$pb.TagNumber(1)
$core.int get id => $_getIZ(0); $core.List<$core.int> get address => $_getN(0);
@$pb.TagNumber(1) @$pb.TagNumber(1)
set id($core.int value) => $_setSignedInt32(0, value); set address($core.List<$core.int> value) => $_setBytes(0, value);
@$pb.TagNumber(1) @$pb.TagNumber(1)
$core.bool hasId() => $_has(0); $core.bool hasAddress() => $_has(0);
@$pb.TagNumber(1) @$pb.TagNumber(1)
void clearId() => $_clearField(1); void clearAddress() => $_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 {
@@ -448,7 +436,7 @@ class VolumeRateLimit extends $pb.GeneratedMessage {
class SharedSettings extends $pb.GeneratedMessage { class SharedSettings extends $pb.GeneratedMessage {
factory SharedSettings({ factory SharedSettings({
$core.int? walletAccessId, $core.int? walletId,
$fixnum.Int64? chainId, $fixnum.Int64? chainId,
$0.Timestamp? validFrom, $0.Timestamp? validFrom,
$0.Timestamp? validUntil, $0.Timestamp? validUntil,
@@ -457,7 +445,7 @@ class SharedSettings extends $pb.GeneratedMessage {
TransactionRateLimit? rateLimit, TransactionRateLimit? rateLimit,
}) { }) {
final result = create(); final result = create();
if (walletAccessId != null) result.walletAccessId = walletAccessId; if (walletId != null) result.walletId = walletId;
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;
@@ -481,7 +469,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 ? '' : 'walletAccessId') ..aI(1, _omitFieldNames ? '' : 'walletId')
..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',
@@ -516,13 +504,13 @@ class SharedSettings extends $pb.GeneratedMessage {
static SharedSettings? _defaultInstance; static SharedSettings? _defaultInstance;
@$pb.TagNumber(1) @$pb.TagNumber(1)
$core.int get walletAccessId => $_getIZ(0); $core.int get walletId => $_getIZ(0);
@$pb.TagNumber(1) @$pb.TagNumber(1)
set walletAccessId($core.int value) => $_setSignedInt32(0, value); set walletId($core.int value) => $_setSignedInt32(0, value);
@$pb.TagNumber(1) @$pb.TagNumber(1)
$core.bool hasWalletAccessId() => $_has(0); $core.bool hasWalletId() => $_has(0);
@$pb.TagNumber(1) @$pb.TagNumber(1)
void clearWalletAccessId() => $_clearField(1); void clearWalletId() => $_clearField(1);
@$pb.TagNumber(2) @$pb.TagNumber(2)
$fixnum.Int64 get chainId => $_getI64(1); $fixnum.Int64 get chainId => $_getI64(1);
@@ -1637,10 +1625,12 @@ 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;
@@ -1659,9 +1649,10 @@ 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)
..aOM<SharedSettings>(1, _omitFieldNames ? '' : 'shared', ..aI(1, _omitFieldNames ? '' : 'clientId')
..aOM<SharedSettings>(2, _omitFieldNames ? '' : 'shared',
subBuilder: SharedSettings.create) subBuilder: SharedSettings.create)
..aOM<SpecificGrant>(2, _omitFieldNames ? '' : 'specific', ..aOM<SpecificGrant>(3, _omitFieldNames ? '' : 'specific',
subBuilder: SpecificGrant.create) subBuilder: SpecificGrant.create)
..hasRequiredFields = false; ..hasRequiredFields = false;
@@ -1686,26 +1677,35 @@ class EvmGrantCreateRequest extends $pb.GeneratedMessage {
static EvmGrantCreateRequest? _defaultInstance; static EvmGrantCreateRequest? _defaultInstance;
@$pb.TagNumber(1) @$pb.TagNumber(1)
SharedSettings get shared => $_getN(0); $core.int get clientId => $_getIZ(0);
@$pb.TagNumber(1) @$pb.TagNumber(1)
set shared(SharedSettings value) => $_setField(1, value); set clientId($core.int value) => $_setSignedInt32(0, value);
@$pb.TagNumber(1) @$pb.TagNumber(1)
$core.bool hasShared() => $_has(0); $core.bool hasClientId() => $_has(0);
@$pb.TagNumber(1) @$pb.TagNumber(1)
void clearShared() => $_clearField(1); void clearClientId() => $_clearField(1);
@$pb.TagNumber(1)
SharedSettings ensureShared() => $_ensure(0);
@$pb.TagNumber(2) @$pb.TagNumber(2)
SpecificGrant get specific => $_getN(1); SharedSettings get shared => $_getN(1);
@$pb.TagNumber(2) @$pb.TagNumber(2)
set specific(SpecificGrant value) => $_setField(2, value); set shared(SharedSettings value) => $_setField(2, value);
@$pb.TagNumber(2) @$pb.TagNumber(2)
$core.bool hasSpecific() => $_has(1); $core.bool hasShared() => $_has(1);
@$pb.TagNumber(2) @$pb.TagNumber(2)
void clearSpecific() => $_clearField(2); void clearShared() => $_clearField(2);
@$pb.TagNumber(2) @$pb.TagNumber(2)
SpecificGrant ensureSpecific() => $_ensure(1); SharedSettings ensureShared() => $_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? walletAccessId, $core.int? clientId,
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 (walletAccessId != null) result.walletAccessId = walletAccessId; 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;
@@ -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 ? '' : 'walletAccessId') ..aI(2, _omitFieldNames ? '' : 'clientId')
..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 walletAccessId => $_getIZ(1); $core.int get clientId => $_getIZ(1);
@$pb.TagNumber(2) @$pb.TagNumber(2)
set walletAccessId($core.int value) => $_setSignedInt32(1, value); set clientId($core.int value) => $_setSignedInt32(1, value);
@$pb.TagNumber(2) @$pb.TagNumber(2)
$core.bool hasWalletAccessId() => $_has(1); $core.bool hasClientId() => $_has(1);
@$pb.TagNumber(2) @$pb.TagNumber(2)
void clearWalletAccessId() => $_clearField(2); void clearClientId() => $_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? walletAccessId, $core.int? walletId,
}) { }) {
final result = create(); final result = create();
if (walletAccessId != null) result.walletAccessId = walletAccessId; if (walletId != null) result.walletId = walletId;
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 ? '' : 'walletAccessId') ..aI(1, _omitFieldNames ? '' : 'walletId')
..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 walletAccessId => $_getIZ(0); $core.int get walletId => $_getIZ(0);
@$pb.TagNumber(1) @$pb.TagNumber(1)
set walletAccessId($core.int value) => $_setSignedInt32(0, value); set walletId($core.int value) => $_setSignedInt32(0, value);
@$pb.TagNumber(1) @$pb.TagNumber(1)
$core.bool hasWalletAccessId() => $_has(0); $core.bool hasWalletId() => $_has(0);
@$pb.TagNumber(1) @$pb.TagNumber(1)
void clearWalletAccessId() => $_clearField(1); void clearWalletId() => $_clearField(1);
} }
enum EvmGrantListResponse_Result { grants, error, notSet } enum EvmGrantListResponse_Result { grants, error, notSet }

View File

@@ -34,15 +34,13 @@ final $typed_data.Uint8List evmErrorDescriptor = $convert.base64Decode(
const WalletEntry$json = { const WalletEntry$json = {
'1': 'WalletEntry', '1': 'WalletEntry',
'2': [ '2': [
{'1': 'id', '3': 1, '4': 1, '5': 5, '10': 'id'}, {'1': 'address', '3': 1, '4': 1, '5': 12, '10': 'address'},
{'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.base64Decode( final $typed_data.Uint8List walletEntryDescriptor = $convert
'CgtXYWxsZXRFbnRyeRIOCgJpZBgBIAEoBVICaWQSGAoHYWRkcmVzcxgCIAEoDFIHYWRkcmVzcw' .base64Decode('CgtXYWxsZXRFbnRyeRIYCgdhZGRyZXNzGAEgASgMUgdhZGRyZXNz');
'==');
@$core.Deprecated('Use walletListDescriptor instead') @$core.Deprecated('Use walletListDescriptor instead')
const WalletList$json = { const WalletList$json = {
@@ -164,7 +162,7 @@ final $typed_data.Uint8List volumeRateLimitDescriptor = $convert.base64Decode(
const SharedSettings$json = { const SharedSettings$json = {
'1': 'SharedSettings', '1': 'SharedSettings',
'2': [ '2': [
{'1': 'wallet_access_id', '3': 1, '4': 1, '5': 5, '10': 'walletAccessId'}, {'1': 'wallet_id', '3': 1, '4': 1, '5': 5, '10': 'walletId'},
{'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',
@@ -226,15 +224,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(
'Cg5TaGFyZWRTZXR0aW5ncxIoChB3YWxsZXRfYWNjZXNzX2lkGAEgASgFUg53YWxsZXRBY2Nlc3' 'Cg5TaGFyZWRTZXR0aW5ncxIbCgl3YWxsZXRfaWQYASABKAVSCHdhbGxldElkEhkKCGNoYWluX2'
'NJZBIZCghjaGFpbl9pZBgCIAEoBFIHY2hhaW5JZBI+Cgp2YWxpZF9mcm9tGAMgASgLMhouZ29v' 'lkGAIgASgEUgdjaGFpbklkEj4KCnZhbGlkX2Zyb20YAyABKAsyGi5nb29nbGUucHJvdG9idWYu'
'Z2xlLnByb3RvYnVmLlRpbWVzdGFtcEgAUgl2YWxpZEZyb22IAQESQAoLdmFsaWRfdW50aWwYBC' 'VGltZXN0YW1wSABSCXZhbGlkRnJvbYgBARJACgt2YWxpZF91bnRpbBgEIAEoCzIaLmdvb2dsZS'
'ABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wSAFSCnZhbGlkVW50aWyIAQESMQoTbWF4' '5wcm90b2J1Zi5UaW1lc3RhbXBIAVIKdmFsaWRVbnRpbIgBARIxChNtYXhfZ2FzX2ZlZV9wZXJf'
'X2dhc19mZWVfcGVyX2dhcxgFIAEoDEgCUg9tYXhHYXNGZWVQZXJHYXOIAQESOwoYbWF4X3ByaW' 'Z2FzGAUgASgMSAJSD21heEdhc0ZlZVBlckdhc4gBARI7ChhtYXhfcHJpb3JpdHlfZmVlX3Blcl'
'9yaXR5X2ZlZV9wZXJfZ2FzGAYgASgMSANSFG1heFByaW9yaXR5RmVlUGVyR2FziAEBEkUKCnJh' '9nYXMYBiABKAxIA1IUbWF4UHJpb3JpdHlGZWVQZXJHYXOIAQESRQoKcmF0ZV9saW1pdBgHIAEo'
'dGVfbGltaXQYByABKAsyIS5hcmJpdGVyLmV2bS5UcmFuc2FjdGlvblJhdGVMaW1pdEgEUglyYX' 'CzIhLmFyYml0ZXIuZXZtLlRyYW5zYWN0aW9uUmF0ZUxpbWl0SARSCXJhdGVMaW1pdIgBAUINCg'
'RlTGltaXSIAQFCDQoLX3ZhbGlkX2Zyb21CDgoMX3ZhbGlkX3VudGlsQhYKFF9tYXhfZ2FzX2Zl' 'tfdmFsaWRfZnJvbUIOCgxfdmFsaWRfdW50aWxCFgoUX21heF9nYXNfZmVlX3Blcl9nYXNCGwoZ'
'ZV9wZXJfZ2FzQhsKGV9tYXhfcHJpb3JpdHlfZmVlX3Blcl9nYXNCDQoLX3JhdGVfbGltaXQ='); 'X21heF9wcmlvcml0eV9mZWVfcGVyX2dhc0INCgtfcmF0ZV9saW1pdA==');
@$core.Deprecated('Use etherTransferSettingsDescriptor instead') @$core.Deprecated('Use etherTransferSettingsDescriptor instead')
const EtherTransferSettings$json = { const EtherTransferSettings$json = {
@@ -633,9 +631,10 @@ 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': 1, '3': 2,
'4': 1, '4': 1,
'5': 11, '5': 11,
'6': '.arbiter.evm.SharedSettings', '6': '.arbiter.evm.SharedSettings',
@@ -643,7 +642,7 @@ const EvmGrantCreateRequest$json = {
}, },
{ {
'1': 'specific', '1': 'specific',
'3': 2, '3': 3,
'4': 1, '4': 1,
'5': 11, '5': 11,
'6': '.arbiter.evm.SpecificGrant', '6': '.arbiter.evm.SpecificGrant',
@@ -654,9 +653,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(
'ChVFdm1HcmFudENyZWF0ZVJlcXVlc3QSMwoGc2hhcmVkGAEgASgLMhsuYXJiaXRlci5ldm0uU2' 'ChVFdm1HcmFudENyZWF0ZVJlcXVlc3QSGwoJY2xpZW50X2lkGAEgASgFUghjbGllbnRJZBIzCg'
'hhcmVkU2V0dGluZ3NSBnNoYXJlZBI2CghzcGVjaWZpYxgCIAEoCzIaLmFyYml0ZXIuZXZtLlNw' 'ZzaGFyZWQYAiABKAsyGy5hcmJpdGVyLmV2bS5TaGFyZWRTZXR0aW5nc1IGc2hhcmVkEjYKCHNw'
'ZWNpZmljR3JhbnRSCHNwZWNpZmlj'); 'ZWNpZmljGAMgASgLMhouYXJiaXRlci5ldm0uU3BlY2lmaWNHcmFudFIIc3BlY2lmaWM=');
@$core.Deprecated('Use evmGrantCreateResponseDescriptor instead') @$core.Deprecated('Use evmGrantCreateResponseDescriptor instead')
const EvmGrantCreateResponse$json = { const EvmGrantCreateResponse$json = {
@@ -735,7 +734,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': 'wallet_access_id', '3': 2, '4': 1, '5': 5, '10': 'walletAccessId'}, {'1': 'client_id', '3': 2, '4': 1, '5': 5, '10': 'clientId'},
{ {
'1': 'shared', '1': 'shared',
'3': 3, '3': 3,
@@ -757,34 +756,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(
'CgpHcmFudEVudHJ5Eg4KAmlkGAEgASgFUgJpZBIoChB3YWxsZXRfYWNjZXNzX2lkGAIgASgFUg' 'CgpHcmFudEVudHJ5Eg4KAmlkGAEgASgFUgJpZBIbCgljbGllbnRfaWQYAiABKAVSCGNsaWVudE'
'53YWxsZXRBY2Nlc3NJZBIzCgZzaGFyZWQYAyABKAsyGy5hcmJpdGVyLmV2bS5TaGFyZWRTZXR0' 'lkEjMKBnNoYXJlZBgDIAEoCzIbLmFyYml0ZXIuZXZtLlNoYXJlZFNldHRpbmdzUgZzaGFyZWQS'
'aW5nc1IGc2hhcmVkEjYKCHNwZWNpZmljGAQgASgLMhouYXJiaXRlci5ldm0uU3BlY2lmaWNHcm' 'NgoIc3BlY2lmaWMYBCABKAsyGi5hcmJpdGVyLmV2bS5TcGVjaWZpY0dyYW50UghzcGVjaWZpYw'
'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_access_id', '1': 'wallet_id',
'3': 1, '3': 1,
'4': 1, '4': 1,
'5': 5, '5': 5,
'9': 0, '9': 0,
'10': 'walletAccessId', '10': 'walletId',
'17': true '17': true
}, },
], ],
'8': [ '8': [
{'1': '_wallet_access_id'}, {'1': '_wallet_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(
'ChNFdm1HcmFudExpc3RSZXF1ZXN0Ei0KEHdhbGxldF9hY2Nlc3NfaWQYASABKAVIAFIOd2FsbG' 'ChNFdm1HcmFudExpc3RSZXF1ZXN0EiAKCXdhbGxldF9pZBgBIAEoBUgAUgh3YWxsZXRJZIgBAU'
'V0QWNjZXNzSWSIAQFCEwoRX3dhbGxldF9hY2Nlc3NfaWQ='); 'IMCgpfd2FsbGV0X2lk');
@$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

View File

@@ -39,36 +39,6 @@ class KeyType extends $pb.ProtobufEnum {
const KeyType._(super.value, super.name); const KeyType._(super.value, super.name);
} }
class SdkClientError extends $pb.ProtobufEnum {
static const SdkClientError SDK_CLIENT_ERROR_UNSPECIFIED =
SdkClientError._(0, _omitEnumNames ? '' : 'SDK_CLIENT_ERROR_UNSPECIFIED');
static const SdkClientError SDK_CLIENT_ERROR_ALREADY_EXISTS =
SdkClientError._(
1, _omitEnumNames ? '' : 'SDK_CLIENT_ERROR_ALREADY_EXISTS');
static const SdkClientError SDK_CLIENT_ERROR_NOT_FOUND =
SdkClientError._(2, _omitEnumNames ? '' : 'SDK_CLIENT_ERROR_NOT_FOUND');
static const SdkClientError SDK_CLIENT_ERROR_HAS_RELATED_DATA =
SdkClientError._(
3, _omitEnumNames ? '' : 'SDK_CLIENT_ERROR_HAS_RELATED_DATA');
static const SdkClientError SDK_CLIENT_ERROR_INTERNAL =
SdkClientError._(4, _omitEnumNames ? '' : 'SDK_CLIENT_ERROR_INTERNAL');
static const $core.List<SdkClientError> values = <SdkClientError>[
SDK_CLIENT_ERROR_UNSPECIFIED,
SDK_CLIENT_ERROR_ALREADY_EXISTS,
SDK_CLIENT_ERROR_NOT_FOUND,
SDK_CLIENT_ERROR_HAS_RELATED_DATA,
SDK_CLIENT_ERROR_INTERNAL,
];
static final $core.List<SdkClientError?> _byValue =
$pb.ProtobufEnum.$_initByValueList(values, 4);
static SdkClientError? valueOf($core.int value) =>
value < 0 || value >= _byValue.length ? null : _byValue[value];
const SdkClientError._(super.value, super.name);
}
class AuthResult extends $pb.ProtobufEnum { class AuthResult extends $pb.ProtobufEnum {
static const AuthResult AUTH_RESULT_UNSPECIFIED = static const AuthResult AUTH_RESULT_UNSPECIFIED =
AuthResult._(0, _omitEnumNames ? '' : 'AUTH_RESULT_UNSPECIFIED'); AuthResult._(0, _omitEnumNames ? '' : 'AUTH_RESULT_UNSPECIFIED');

View File

@@ -31,25 +31,6 @@ final $typed_data.Uint8List keyTypeDescriptor = $convert.base64Decode(
'CgdLZXlUeXBlEhgKFEtFWV9UWVBFX1VOU1BFQ0lGSUVEEAASFAoQS0VZX1RZUEVfRUQyNTUxOR' 'CgdLZXlUeXBlEhgKFEtFWV9UWVBFX1VOU1BFQ0lGSUVEEAASFAoQS0VZX1RZUEVfRUQyNTUxOR'
'ABEhwKGEtFWV9UWVBFX0VDRFNBX1NFQ1AyNTZLMRACEhAKDEtFWV9UWVBFX1JTQRAD'); 'ABEhwKGEtFWV9UWVBFX0VDRFNBX1NFQ1AyNTZLMRACEhAKDEtFWV9UWVBFX1JTQRAD');
@$core.Deprecated('Use sdkClientErrorDescriptor instead')
const SdkClientError$json = {
'1': 'SdkClientError',
'2': [
{'1': 'SDK_CLIENT_ERROR_UNSPECIFIED', '2': 0},
{'1': 'SDK_CLIENT_ERROR_ALREADY_EXISTS', '2': 1},
{'1': 'SDK_CLIENT_ERROR_NOT_FOUND', '2': 2},
{'1': 'SDK_CLIENT_ERROR_HAS_RELATED_DATA', '2': 3},
{'1': 'SDK_CLIENT_ERROR_INTERNAL', '2': 4},
],
};
/// Descriptor for `SdkClientError`. Decode as a `google.protobuf.EnumDescriptorProto`.
final $typed_data.Uint8List sdkClientErrorDescriptor = $convert.base64Decode(
'Cg5TZGtDbGllbnRFcnJvchIgChxTREtfQ0xJRU5UX0VSUk9SX1VOU1BFQ0lGSUVEEAASIwofU0'
'RLX0NMSUVOVF9FUlJPUl9BTFJFQURZX0VYSVNUUxABEh4KGlNES19DTElFTlRfRVJST1JfTk9U'
'X0ZPVU5EEAISJQohU0RLX0NMSUVOVF9FUlJPUl9IQVNfUkVMQVRFRF9EQVRBEAMSHQoZU0RLX0'
'NMSUVOVF9FUlJPUl9JTlRFUk5BTBAE');
@$core.Deprecated('Use authResultDescriptor instead') @$core.Deprecated('Use authResultDescriptor instead')
const AuthResult$json = { const AuthResult$json = {
'1': 'AuthResult', '1': 'AuthResult',
@@ -124,131 +105,6 @@ final $typed_data.Uint8List vaultStateDescriptor = $convert.base64Decode(
'VfVU5CT09UU1RSQVBQRUQQARIWChJWQVVMVF9TVEFURV9TRUFMRUQQAhIYChRWQVVMVF9TVEFU' 'VfVU5CT09UU1RSQVBQRUQQARIWChJWQVVMVF9TVEFURV9TRUFMRUQQAhIYChRWQVVMVF9TVEFU'
'RV9VTlNFQUxFRBADEhUKEVZBVUxUX1NUQVRFX0VSUk9SEAQ='); 'RV9VTlNFQUxFRBADEhUKEVZBVUxUX1NUQVRFX0VSUk9SEAQ=');
@$core.Deprecated('Use sdkClientRevokeRequestDescriptor instead')
const SdkClientRevokeRequest$json = {
'1': 'SdkClientRevokeRequest',
'2': [
{'1': 'client_id', '3': 1, '4': 1, '5': 5, '10': 'clientId'},
],
};
/// Descriptor for `SdkClientRevokeRequest`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List sdkClientRevokeRequestDescriptor =
$convert.base64Decode(
'ChZTZGtDbGllbnRSZXZva2VSZXF1ZXN0EhsKCWNsaWVudF9pZBgBIAEoBVIIY2xpZW50SWQ=');
@$core.Deprecated('Use sdkClientEntryDescriptor instead')
const SdkClientEntry$json = {
'1': 'SdkClientEntry',
'2': [
{'1': 'id', '3': 1, '4': 1, '5': 5, '10': 'id'},
{'1': 'pubkey', '3': 2, '4': 1, '5': 12, '10': 'pubkey'},
{
'1': 'info',
'3': 3,
'4': 1,
'5': 11,
'6': '.arbiter.client.ClientInfo',
'10': 'info'
},
{'1': 'created_at', '3': 4, '4': 1, '5': 5, '10': 'createdAt'},
],
};
/// Descriptor for `SdkClientEntry`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List sdkClientEntryDescriptor = $convert.base64Decode(
'Cg5TZGtDbGllbnRFbnRyeRIOCgJpZBgBIAEoBVICaWQSFgoGcHVia2V5GAIgASgMUgZwdWJrZX'
'kSLgoEaW5mbxgDIAEoCzIaLmFyYml0ZXIuY2xpZW50LkNsaWVudEluZm9SBGluZm8SHQoKY3Jl'
'YXRlZF9hdBgEIAEoBVIJY3JlYXRlZEF0');
@$core.Deprecated('Use sdkClientListDescriptor instead')
const SdkClientList$json = {
'1': 'SdkClientList',
'2': [
{
'1': 'clients',
'3': 1,
'4': 3,
'5': 11,
'6': '.arbiter.user_agent.SdkClientEntry',
'10': 'clients'
},
],
};
/// Descriptor for `SdkClientList`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List sdkClientListDescriptor = $convert.base64Decode(
'Cg1TZGtDbGllbnRMaXN0EjwKB2NsaWVudHMYASADKAsyIi5hcmJpdGVyLnVzZXJfYWdlbnQuU2'
'RrQ2xpZW50RW50cnlSB2NsaWVudHM=');
@$core.Deprecated('Use sdkClientRevokeResponseDescriptor instead')
const SdkClientRevokeResponse$json = {
'1': 'SdkClientRevokeResponse',
'2': [
{
'1': 'ok',
'3': 1,
'4': 1,
'5': 11,
'6': '.google.protobuf.Empty',
'9': 0,
'10': 'ok'
},
{
'1': 'error',
'3': 2,
'4': 1,
'5': 14,
'6': '.arbiter.user_agent.SdkClientError',
'9': 0,
'10': 'error'
},
],
'8': [
{'1': 'result'},
],
};
/// Descriptor for `SdkClientRevokeResponse`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List sdkClientRevokeResponseDescriptor = $convert.base64Decode(
'ChdTZGtDbGllbnRSZXZva2VSZXNwb25zZRIoCgJvaxgBIAEoCzIWLmdvb2dsZS5wcm90b2J1Zi'
'5FbXB0eUgAUgJvaxI6CgVlcnJvchgCIAEoDjIiLmFyYml0ZXIudXNlcl9hZ2VudC5TZGtDbGll'
'bnRFcnJvckgAUgVlcnJvckIICgZyZXN1bHQ=');
@$core.Deprecated('Use sdkClientListResponseDescriptor instead')
const SdkClientListResponse$json = {
'1': 'SdkClientListResponse',
'2': [
{
'1': 'clients',
'3': 1,
'4': 1,
'5': 11,
'6': '.arbiter.user_agent.SdkClientList',
'9': 0,
'10': 'clients'
},
{
'1': 'error',
'3': 2,
'4': 1,
'5': 14,
'6': '.arbiter.user_agent.SdkClientError',
'9': 0,
'10': 'error'
},
],
'8': [
{'1': 'result'},
],
};
/// Descriptor for `SdkClientListResponse`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List sdkClientListResponseDescriptor = $convert.base64Decode(
'ChVTZGtDbGllbnRMaXN0UmVzcG9uc2USPQoHY2xpZW50cxgBIAEoCzIhLmFyYml0ZXIudXNlcl'
'9hZ2VudC5TZGtDbGllbnRMaXN0SABSB2NsaWVudHMSOgoFZXJyb3IYAiABKA4yIi5hcmJpdGVy'
'LnVzZXJfYWdlbnQuU2RrQ2xpZW50RXJyb3JIAFIFZXJyb3JCCAoGcmVzdWx0');
@$core.Deprecated('Use authChallengeRequestDescriptor instead') @$core.Deprecated('Use authChallengeRequestDescriptor instead')
const AuthChallengeRequest$json = { const AuthChallengeRequest$json = {
'1': 'AuthChallengeRequest', '1': 'AuthChallengeRequest',
@@ -368,152 +224,46 @@ final $typed_data.Uint8List bootstrapEncryptedKeyDescriptor = $convert.base64Dec
'RleHQYAiABKAxSCmNpcGhlcnRleHQSJwoPYXNzb2NpYXRlZF9kYXRhGAMgASgMUg5hc3NvY2lh' 'RleHQYAiABKAxSCmNpcGhlcnRleHQSJwoPYXNzb2NpYXRlZF9kYXRhGAMgASgMUg5hc3NvY2lh'
'dGVkRGF0YQ=='); 'dGVkRGF0YQ==');
@$core.Deprecated('Use sdkClientConnectionRequestDescriptor instead') @$core.Deprecated('Use clientConnectionRequestDescriptor instead')
const SdkClientConnectionRequest$json = { const ClientConnectionRequest$json = {
'1': 'SdkClientConnectionRequest', '1': 'ClientConnectionRequest',
'2': [ '2': [
{'1': 'pubkey', '3': 1, '4': 1, '5': 12, '10': 'pubkey'}, {'1': 'pubkey', '3': 1, '4': 1, '5': 12, '10': 'pubkey'},
{
'1': 'info',
'3': 2,
'4': 1,
'5': 11,
'6': '.arbiter.client.ClientInfo',
'10': 'info'
},
], ],
}; };
/// Descriptor for `SdkClientConnectionRequest`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `ClientConnectionRequest`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List sdkClientConnectionRequestDescriptor = final $typed_data.Uint8List clientConnectionRequestDescriptor =
$convert.base64Decode( $convert.base64Decode(
'ChpTZGtDbGllbnRDb25uZWN0aW9uUmVxdWVzdBIWCgZwdWJrZXkYASABKAxSBnB1YmtleRIuCg' 'ChdDbGllbnRDb25uZWN0aW9uUmVxdWVzdBIWCgZwdWJrZXkYASABKAxSBnB1YmtleQ==');
'RpbmZvGAIgASgLMhouYXJiaXRlci5jbGllbnQuQ2xpZW50SW5mb1IEaW5mbw==');
@$core.Deprecated('Use sdkClientConnectionResponseDescriptor instead') @$core.Deprecated('Use clientConnectionResponseDescriptor instead')
const SdkClientConnectionResponse$json = { const ClientConnectionResponse$json = {
'1': 'SdkClientConnectionResponse', '1': 'ClientConnectionResponse',
'2': [ '2': [
{'1': 'approved', '3': 1, '4': 1, '5': 8, '10': 'approved'}, {'1': 'approved', '3': 1, '4': 1, '5': 8, '10': 'approved'},
{'1': 'pubkey', '3': 2, '4': 1, '5': 12, '10': 'pubkey'},
], ],
}; };
/// Descriptor for `SdkClientConnectionResponse`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `ClientConnectionResponse`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List sdkClientConnectionResponseDescriptor = final $typed_data.Uint8List clientConnectionResponseDescriptor =
$convert.base64Decode( $convert.base64Decode(
'ChtTZGtDbGllbnRDb25uZWN0aW9uUmVzcG9uc2USGgoIYXBwcm92ZWQYASABKAhSCGFwcHJvdm' 'ChhDbGllbnRDb25uZWN0aW9uUmVzcG9uc2USGgoIYXBwcm92ZWQYASABKAhSCGFwcHJvdmVk');
'VkEhYKBnB1YmtleRgCIAEoDFIGcHVia2V5');
@$core.Deprecated('Use sdkClientConnectionCancelDescriptor instead') @$core.Deprecated('Use clientConnectionCancelDescriptor instead')
const SdkClientConnectionCancel$json = { const ClientConnectionCancel$json = {
'1': 'SdkClientConnectionCancel', '1': 'ClientConnectionCancel',
'2': [
{'1': 'pubkey', '3': 1, '4': 1, '5': 12, '10': 'pubkey'},
],
}; };
/// Descriptor for `SdkClientConnectionCancel`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `ClientConnectionCancel`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List sdkClientConnectionCancelDescriptor = final $typed_data.Uint8List clientConnectionCancelDescriptor =
$convert.base64Decode( $convert.base64Decode('ChZDbGllbnRDb25uZWN0aW9uQ2FuY2Vs');
'ChlTZGtDbGllbnRDb25uZWN0aW9uQ2FuY2VsEhYKBnB1YmtleRgBIAEoDFIGcHVia2V5');
@$core.Deprecated('Use walletAccessDescriptor instead')
const WalletAccess$json = {
'1': 'WalletAccess',
'2': [
{'1': 'wallet_id', '3': 1, '4': 1, '5': 5, '10': 'walletId'},
{'1': 'sdk_client_id', '3': 2, '4': 1, '5': 5, '10': 'sdkClientId'},
],
};
/// Descriptor for `WalletAccess`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List walletAccessDescriptor = $convert.base64Decode(
'CgxXYWxsZXRBY2Nlc3MSGwoJd2FsbGV0X2lkGAEgASgFUgh3YWxsZXRJZBIiCg1zZGtfY2xpZW'
'50X2lkGAIgASgFUgtzZGtDbGllbnRJZA==');
@$core.Deprecated('Use sdkClientWalletAccessDescriptor instead')
const SdkClientWalletAccess$json = {
'1': 'SdkClientWalletAccess',
'2': [
{'1': 'id', '3': 1, '4': 1, '5': 5, '10': 'id'},
{
'1': 'access',
'3': 2,
'4': 1,
'5': 11,
'6': '.arbiter.user_agent.WalletAccess',
'10': 'access'
},
],
};
/// Descriptor for `SdkClientWalletAccess`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List sdkClientWalletAccessDescriptor = $convert.base64Decode(
'ChVTZGtDbGllbnRXYWxsZXRBY2Nlc3MSDgoCaWQYASABKAVSAmlkEjgKBmFjY2VzcxgCIAEoCz'
'IgLmFyYml0ZXIudXNlcl9hZ2VudC5XYWxsZXRBY2Nlc3NSBmFjY2Vzcw==');
@$core.Deprecated('Use sdkClientGrantWalletAccessDescriptor instead')
const SdkClientGrantWalletAccess$json = {
'1': 'SdkClientGrantWalletAccess',
'2': [
{
'1': 'accesses',
'3': 1,
'4': 3,
'5': 11,
'6': '.arbiter.user_agent.WalletAccess',
'10': 'accesses'
},
],
};
/// Descriptor for `SdkClientGrantWalletAccess`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List sdkClientGrantWalletAccessDescriptor =
$convert.base64Decode(
'ChpTZGtDbGllbnRHcmFudFdhbGxldEFjY2VzcxI8CghhY2Nlc3NlcxgBIAMoCzIgLmFyYml0ZX'
'IudXNlcl9hZ2VudC5XYWxsZXRBY2Nlc3NSCGFjY2Vzc2Vz');
@$core.Deprecated('Use sdkClientRevokeWalletAccessDescriptor instead')
const SdkClientRevokeWalletAccess$json = {
'1': 'SdkClientRevokeWalletAccess',
'2': [
{'1': 'accesses', '3': 1, '4': 3, '5': 5, '10': 'accesses'},
],
};
/// Descriptor for `SdkClientRevokeWalletAccess`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List sdkClientRevokeWalletAccessDescriptor =
$convert.base64Decode(
'ChtTZGtDbGllbnRSZXZva2VXYWxsZXRBY2Nlc3MSGgoIYWNjZXNzZXMYASADKAVSCGFjY2Vzc2'
'Vz');
@$core.Deprecated('Use listWalletAccessResponseDescriptor instead')
const ListWalletAccessResponse$json = {
'1': 'ListWalletAccessResponse',
'2': [
{
'1': 'accesses',
'3': 1,
'4': 3,
'5': 11,
'6': '.arbiter.user_agent.SdkClientWalletAccess',
'10': 'accesses'
},
],
};
/// Descriptor for `ListWalletAccessResponse`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List listWalletAccessResponseDescriptor =
$convert.base64Decode(
'ChhMaXN0V2FsbGV0QWNjZXNzUmVzcG9uc2USRQoIYWNjZXNzZXMYASADKAsyKS5hcmJpdGVyLn'
'VzZXJfYWdlbnQuU2RrQ2xpZW50V2FsbGV0QWNjZXNzUghhY2Nlc3Nlcw==');
@$core.Deprecated('Use userAgentRequestDescriptor instead') @$core.Deprecated('Use userAgentRequestDescriptor instead')
const UserAgentRequest$json = { const UserAgentRequest$json = {
'1': 'UserAgentRequest', '1': 'UserAgentRequest',
'2': [ '2': [
{'1': 'id', '3': 16, '4': 1, '5': 5, '10': 'id'}, {'1': 'id', '3': 14, '4': 1, '5': 5, '10': 'id'},
{ {
'1': 'auth_challenge_request', '1': 'auth_challenge_request',
'3': 1, '3': 1,
@@ -605,68 +355,23 @@ const UserAgentRequest$json = {
'10': 'evmGrantList' '10': 'evmGrantList'
}, },
{ {
'1': 'sdk_client_connection_response', '1': 'client_connection_response',
'3': 11, '3': 11,
'4': 1, '4': 1,
'5': 11, '5': 11,
'6': '.arbiter.user_agent.SdkClientConnectionResponse', '6': '.arbiter.user_agent.ClientConnectionResponse',
'9': 0, '9': 0,
'10': 'sdkClientConnectionResponse' '10': 'clientConnectionResponse'
},
{
'1': 'sdk_client_revoke',
'3': 12,
'4': 1,
'5': 11,
'6': '.arbiter.user_agent.SdkClientRevokeRequest',
'9': 0,
'10': 'sdkClientRevoke'
},
{
'1': 'sdk_client_list',
'3': 13,
'4': 1,
'5': 11,
'6': '.google.protobuf.Empty',
'9': 0,
'10': 'sdkClientList'
}, },
{ {
'1': 'bootstrap_encrypted_key', '1': 'bootstrap_encrypted_key',
'3': 14, '3': 12,
'4': 1, '4': 1,
'5': 11, '5': 11,
'6': '.arbiter.user_agent.BootstrapEncryptedKey', '6': '.arbiter.user_agent.BootstrapEncryptedKey',
'9': 0, '9': 0,
'10': 'bootstrapEncryptedKey' '10': 'bootstrapEncryptedKey'
}, },
{
'1': 'grant_wallet_access',
'3': 15,
'4': 1,
'5': 11,
'6': '.arbiter.user_agent.SdkClientGrantWalletAccess',
'9': 0,
'10': 'grantWalletAccess'
},
{
'1': 'revoke_wallet_access',
'3': 17,
'4': 1,
'5': 11,
'6': '.arbiter.user_agent.SdkClientRevokeWalletAccess',
'9': 0,
'10': 'revokeWalletAccess'
},
{
'1': 'list_wallet_access',
'3': 18,
'4': 1,
'5': 11,
'6': '.google.protobuf.Empty',
'9': 0,
'10': 'listWalletAccess'
},
], ],
'8': [ '8': [
{'1': 'payload'}, {'1': 'payload'},
@@ -675,7 +380,7 @@ const UserAgentRequest$json = {
/// Descriptor for `UserAgentRequest`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `UserAgentRequest`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List userAgentRequestDescriptor = $convert.base64Decode( final $typed_data.Uint8List userAgentRequestDescriptor = $convert.base64Decode(
'ChBVc2VyQWdlbnRSZXF1ZXN0Eg4KAmlkGBAgASgFUgJpZBJgChZhdXRoX2NoYWxsZW5nZV9yZX' 'ChBVc2VyQWdlbnRSZXF1ZXN0Eg4KAmlkGA4gASgFUgJpZBJgChZhdXRoX2NoYWxsZW5nZV9yZX'
'F1ZXN0GAEgASgLMiguYXJiaXRlci51c2VyX2FnZW50LkF1dGhDaGFsbGVuZ2VSZXF1ZXN0SABS' 'F1ZXN0GAEgASgLMiguYXJiaXRlci51c2VyX2FnZW50LkF1dGhDaGFsbGVuZ2VSZXF1ZXN0SABS'
'FGF1dGhDaGFsbGVuZ2VSZXF1ZXN0EmMKF2F1dGhfY2hhbGxlbmdlX3NvbHV0aW9uGAIgASgLMi' 'FGF1dGhDaGFsbGVuZ2VSZXF1ZXN0EmMKF2F1dGhfY2hhbGxlbmdlX3NvbHV0aW9uGAIgASgLMi'
'kuYXJiaXRlci51c2VyX2FnZW50LkF1dGhDaGFsbGVuZ2VTb2x1dGlvbkgAUhVhdXRoQ2hhbGxl' 'kuYXJiaXRlci51c2VyX2FnZW50LkF1dGhDaGFsbGVuZ2VTb2x1dGlvbkgAUhVhdXRoQ2hhbGxl'
@@ -690,24 +395,17 @@ final $typed_data.Uint8List userAgentRequestDescriptor = $convert.base64Decode(
'DmV2bUdyYW50Q3JlYXRlEk4KEGV2bV9ncmFudF9kZWxldGUYCSABKAsyIi5hcmJpdGVyLmV2bS' 'DmV2bUdyYW50Q3JlYXRlEk4KEGV2bV9ncmFudF9kZWxldGUYCSABKAsyIi5hcmJpdGVyLmV2bS'
'5Fdm1HcmFudERlbGV0ZVJlcXVlc3RIAFIOZXZtR3JhbnREZWxldGUSSAoOZXZtX2dyYW50X2xp' '5Fdm1HcmFudERlbGV0ZVJlcXVlc3RIAFIOZXZtR3JhbnREZWxldGUSSAoOZXZtX2dyYW50X2xp'
'c3QYCiABKAsyIC5hcmJpdGVyLmV2bS5Fdm1HcmFudExpc3RSZXF1ZXN0SABSDGV2bUdyYW50TG' 'c3QYCiABKAsyIC5hcmJpdGVyLmV2bS5Fdm1HcmFudExpc3RSZXF1ZXN0SABSDGV2bUdyYW50TG'
'lzdBJ2Ch5zZGtfY2xpZW50X2Nvbm5lY3Rpb25fcmVzcG9uc2UYCyABKAsyLy5hcmJpdGVyLnVz' 'lzdBJsChpjbGllbnRfY29ubmVjdGlvbl9yZXNwb25zZRgLIAEoCzIsLmFyYml0ZXIudXNlcl9h'
'ZXJfYWdlbnQuU2RrQ2xpZW50Q29ubmVjdGlvblJlc3BvbnNlSABSG3Nka0NsaWVudENvbm5lY3' 'Z2VudC5DbGllbnRDb25uZWN0aW9uUmVzcG9uc2VIAFIYY2xpZW50Q29ubmVjdGlvblJlc3Bvbn'
'Rpb25SZXNwb25zZRJYChFzZGtfY2xpZW50X3Jldm9rZRgMIAEoCzIqLmFyYml0ZXIudXNlcl9h' 'NlEmMKF2Jvb3RzdHJhcF9lbmNyeXB0ZWRfa2V5GAwgASgLMikuYXJiaXRlci51c2VyX2FnZW50'
'Z2VudC5TZGtDbGllbnRSZXZva2VSZXF1ZXN0SABSD3Nka0NsaWVudFJldm9rZRJACg9zZGtfY2' 'LkJvb3RzdHJhcEVuY3J5cHRlZEtleUgAUhVib290c3RyYXBFbmNyeXB0ZWRLZXlCCQoHcGF5bG'
'xpZW50X2xpc3QYDSABKAsyFi5nb29nbGUucHJvdG9idWYuRW1wdHlIAFINc2RrQ2xpZW50TGlz' '9hZA==');
'dBJjChdib290c3RyYXBfZW5jcnlwdGVkX2tleRgOIAEoCzIpLmFyYml0ZXIudXNlcl9hZ2VudC'
'5Cb290c3RyYXBFbmNyeXB0ZWRLZXlIAFIVYm9vdHN0cmFwRW5jcnlwdGVkS2V5EmAKE2dyYW50'
'X3dhbGxldF9hY2Nlc3MYDyABKAsyLi5hcmJpdGVyLnVzZXJfYWdlbnQuU2RrQ2xpZW50R3Jhbn'
'RXYWxsZXRBY2Nlc3NIAFIRZ3JhbnRXYWxsZXRBY2Nlc3MSYwoUcmV2b2tlX3dhbGxldF9hY2Nl'
'c3MYESABKAsyLy5hcmJpdGVyLnVzZXJfYWdlbnQuU2RrQ2xpZW50UmV2b2tlV2FsbGV0QWNjZX'
'NzSABSEnJldm9rZVdhbGxldEFjY2VzcxJGChJsaXN0X3dhbGxldF9hY2Nlc3MYEiABKAsyFi5n'
'b29nbGUucHJvdG9idWYuRW1wdHlIAFIQbGlzdFdhbGxldEFjY2Vzc0IJCgdwYXlsb2Fk');
@$core.Deprecated('Use userAgentResponseDescriptor instead') @$core.Deprecated('Use userAgentResponseDescriptor instead')
const UserAgentResponse$json = { const UserAgentResponse$json = {
'1': 'UserAgentResponse', '1': 'UserAgentResponse',
'2': [ '2': [
{'1': 'id', '3': 16, '4': 1, '5': 5, '9': 1, '10': 'id', '17': true}, {'1': 'id', '3': 14, '4': 1, '5': 5, '9': 1, '10': 'id', '17': true},
{ {
'1': 'auth_challenge', '1': 'auth_challenge',
'3': 1, '3': 1,
@@ -799,59 +497,32 @@ const UserAgentResponse$json = {
'10': 'evmGrantList' '10': 'evmGrantList'
}, },
{ {
'1': 'sdk_client_connection_request', '1': 'client_connection_request',
'3': 11, '3': 11,
'4': 1, '4': 1,
'5': 11, '5': 11,
'6': '.arbiter.user_agent.SdkClientConnectionRequest', '6': '.arbiter.user_agent.ClientConnectionRequest',
'9': 0, '9': 0,
'10': 'sdkClientConnectionRequest' '10': 'clientConnectionRequest'
}, },
{ {
'1': 'sdk_client_connection_cancel', '1': 'client_connection_cancel',
'3': 12, '3': 12,
'4': 1, '4': 1,
'5': 11, '5': 11,
'6': '.arbiter.user_agent.SdkClientConnectionCancel', '6': '.arbiter.user_agent.ClientConnectionCancel',
'9': 0, '9': 0,
'10': 'sdkClientConnectionCancel' '10': 'clientConnectionCancel'
},
{
'1': 'sdk_client_revoke_response',
'3': 13,
'4': 1,
'5': 11,
'6': '.arbiter.user_agent.SdkClientRevokeResponse',
'9': 0,
'10': 'sdkClientRevokeResponse'
},
{
'1': 'sdk_client_list_response',
'3': 14,
'4': 1,
'5': 11,
'6': '.arbiter.user_agent.SdkClientListResponse',
'9': 0,
'10': 'sdkClientListResponse'
}, },
{ {
'1': 'bootstrap_result', '1': 'bootstrap_result',
'3': 15, '3': 13,
'4': 1, '4': 1,
'5': 14, '5': 14,
'6': '.arbiter.user_agent.BootstrapResult', '6': '.arbiter.user_agent.BootstrapResult',
'9': 0, '9': 0,
'10': 'bootstrapResult' '10': 'bootstrapResult'
}, },
{
'1': 'list_wallet_access_response',
'3': 17,
'4': 1,
'5': 11,
'6': '.arbiter.user_agent.ListWalletAccessResponse',
'9': 0,
'10': 'listWalletAccessResponse'
},
], ],
'8': [ '8': [
{'1': 'payload'}, {'1': 'payload'},
@@ -861,7 +532,7 @@ const UserAgentResponse$json = {
/// Descriptor for `UserAgentResponse`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `UserAgentResponse`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List userAgentResponseDescriptor = $convert.base64Decode( final $typed_data.Uint8List userAgentResponseDescriptor = $convert.base64Decode(
'ChFVc2VyQWdlbnRSZXNwb25zZRITCgJpZBgQIAEoBUgBUgJpZIgBARJKCg5hdXRoX2NoYWxsZW' 'ChFVc2VyQWdlbnRSZXNwb25zZRITCgJpZBgOIAEoBUgBUgJpZIgBARJKCg5hdXRoX2NoYWxsZW'
'5nZRgBIAEoCzIhLmFyYml0ZXIudXNlcl9hZ2VudC5BdXRoQ2hhbGxlbmdlSABSDWF1dGhDaGFs' '5nZRgBIAEoCzIhLmFyYml0ZXIudXNlcl9hZ2VudC5BdXRoQ2hhbGxlbmdlSABSDWF1dGhDaGFs'
'bGVuZ2USQQoLYXV0aF9yZXN1bHQYAiABKA4yHi5hcmJpdGVyLnVzZXJfYWdlbnQuQXV0aFJlc3' 'bGVuZ2USQQoLYXV0aF9yZXN1bHQYAiABKA4yHi5hcmJpdGVyLnVzZXJfYWdlbnQuQXV0aFJlc3'
'VsdEgAUgphdXRoUmVzdWx0El0KFXVuc2VhbF9zdGFydF9yZXNwb25zZRgDIAEoCzInLmFyYml0' 'VsdEgAUgphdXRoUmVzdWx0El0KFXVuc2VhbF9zdGFydF9yZXNwb25zZRgDIAEoCzInLmFyYml0'
@@ -875,16 +546,10 @@ final $typed_data.Uint8List userAgentResponseDescriptor = $convert.base64Decode(
'5ldm0uRXZtR3JhbnRDcmVhdGVSZXNwb25zZUgAUg5ldm1HcmFudENyZWF0ZRJPChBldm1fZ3Jh' '5ldm0uRXZtR3JhbnRDcmVhdGVSZXNwb25zZUgAUg5ldm1HcmFudENyZWF0ZRJPChBldm1fZ3Jh'
'bnRfZGVsZXRlGAkgASgLMiMuYXJiaXRlci5ldm0uRXZtR3JhbnREZWxldGVSZXNwb25zZUgAUg' 'bnRfZGVsZXRlGAkgASgLMiMuYXJiaXRlci5ldm0uRXZtR3JhbnREZWxldGVSZXNwb25zZUgAUg'
'5ldm1HcmFudERlbGV0ZRJJCg5ldm1fZ3JhbnRfbGlzdBgKIAEoCzIhLmFyYml0ZXIuZXZtLkV2' '5ldm1HcmFudERlbGV0ZRJJCg5ldm1fZ3JhbnRfbGlzdBgKIAEoCzIhLmFyYml0ZXIuZXZtLkV2'
'bUdyYW50TGlzdFJlc3BvbnNlSABSDGV2bUdyYW50TGlzdBJzCh1zZGtfY2xpZW50X2Nvbm5lY3' 'bUdyYW50TGlzdFJlc3BvbnNlSABSDGV2bUdyYW50TGlzdBJpChljbGllbnRfY29ubmVjdGlvbl'
'Rpb25fcmVxdWVzdBgLIAEoCzIuLmFyYml0ZXIudXNlcl9hZ2VudC5TZGtDbGllbnRDb25uZWN0' '9yZXF1ZXN0GAsgASgLMisuYXJiaXRlci51c2VyX2FnZW50LkNsaWVudENvbm5lY3Rpb25SZXF1'
'aW9uUmVxdWVzdEgAUhpzZGtDbGllbnRDb25uZWN0aW9uUmVxdWVzdBJwChxzZGtfY2xpZW50X2' 'ZXN0SABSF2NsaWVudENvbm5lY3Rpb25SZXF1ZXN0EmYKGGNsaWVudF9jb25uZWN0aW9uX2Nhbm'
'Nvbm5lY3Rpb25fY2FuY2VsGAwgASgLMi0uYXJiaXRlci51c2VyX2FnZW50LlNka0NsaWVudENv' 'NlbBgMIAEoCzIqLmFyYml0ZXIudXNlcl9hZ2VudC5DbGllbnRDb25uZWN0aW9uQ2FuY2VsSABS'
'bm5lY3Rpb25DYW5jZWxIAFIZc2RrQ2xpZW50Q29ubmVjdGlvbkNhbmNlbBJqChpzZGtfY2xpZW' 'FmNsaWVudENvbm5lY3Rpb25DYW5jZWwSUAoQYm9vdHN0cmFwX3Jlc3VsdBgNIAEoDjIjLmFyYm'
'50X3Jldm9rZV9yZXNwb25zZRgNIAEoCzIrLmFyYml0ZXIudXNlcl9hZ2VudC5TZGtDbGllbnRS' 'l0ZXIudXNlcl9hZ2VudC5Cb290c3RyYXBSZXN1bHRIAFIPYm9vdHN0cmFwUmVzdWx0QgkKB3Bh'
'ZXZva2VSZXNwb25zZUgAUhdzZGtDbGllbnRSZXZva2VSZXNwb25zZRJkChhzZGtfY2xpZW50X2' 'eWxvYWRCBQoDX2lk');
'xpc3RfcmVzcG9uc2UYDiABKAsyKS5hcmJpdGVyLnVzZXJfYWdlbnQuU2RrQ2xpZW50TGlzdFJl'
'c3BvbnNlSABSFXNka0NsaWVudExpc3RSZXNwb25zZRJQChBib290c3RyYXBfcmVzdWx0GA8gAS'
'gOMiMuYXJiaXRlci51c2VyX2FnZW50LkJvb3RzdHJhcFJlc3VsdEgAUg9ib290c3RyYXBSZXN1'
'bHQSbQobbGlzdF93YWxsZXRfYWNjZXNzX3Jlc3BvbnNlGBEgASgLMiwuYXJiaXRlci51c2VyX2'
'FnZW50Lkxpc3RXYWxsZXRBY2Nlc3NSZXNwb25zZUgAUhhsaXN0V2FsbGV0QWNjZXNzUmVzcG9u'
'c2VCCQoHcGF5bG9hZEIFCgNfaWQ=');

View File

@@ -14,7 +14,7 @@ class ConnectionManager extends _$ConnectionManager {
Future<Connection?> build() async { Future<Connection?> build() async {
final serverInfo = await ref.watch(serverInfoProvider.future); final serverInfo = await ref.watch(serverInfoProvider.future);
final key = await ref.watch(keyProvider.future); final key = await ref.watch(keyProvider.future);
final token = ref.read(bootstrapTokenProvider); final token = ref.watch(bootstrapTokenProvider);
if (serverInfo == null || key == null) { if (serverInfo == null || key == null) {
return null; return null;

View File

@@ -33,7 +33,7 @@ final class ConnectionManagerProvider
ConnectionManager create() => ConnectionManager(); ConnectionManager create() => ConnectionManager();
} }
String _$connectionManagerHash() => r'f471afb49bdcde77238424942f5af1716634f084'; String _$connectionManagerHash() => r'd01084e550f315bc6cadfe74413a7f959426a80e';
abstract class _$ConnectionManager extends $AsyncNotifier<Connection?> { abstract class _$ConnectionManager extends $AsyncNotifier<Connection?> {
FutureOr<Connection?> build(); FutureOr<Connection?> build();

View File

@@ -1,8 +1,6 @@
import 'package:arbiter/features/connection/evm.dart' as evm; import 'package:arbiter/features/connection/evm.dart';
import 'package:arbiter/proto/evm.pb.dart'; import 'package:arbiter/proto/evm.pb.dart';
import 'package:arbiter/providers/connection/connection_manager.dart'; import 'package:arbiter/providers/connection/connection_manager.dart';
import 'package:hooks_riverpod/experimental/mutation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'evm.g.dart'; part 'evm.g.dart';
@@ -16,7 +14,7 @@ class Evm extends _$Evm {
return null; return null;
} }
return evm.listEvmWallets(connection); return listEvmWallets(connection);
} }
Future<void> refreshWallets() async { Future<void> refreshWallets() async {
@@ -27,21 +25,16 @@ class Evm extends _$Evm {
} }
state = const AsyncLoading(); state = const AsyncLoading();
state = await AsyncValue.guard(() => evm.listEvmWallets(connection)); state = await AsyncValue.guard(() => listEvmWallets(connection));
}
} }
final createEvmWallet = Mutation(); Future<void> createWallet() async {
final connection = await ref.read(connectionManagerProvider.future);
Future<void> executeCreateEvmWallet(MutationTarget target) async {
return await createEvmWallet.run(target, (tsx) async {
final connection = await tsx.get(connectionManagerProvider.future);
if (connection == null) { if (connection == null) {
throw Exception('Not connected to the server.'); throw Exception('Not connected to the server.');
} }
await evm.createEvmWallet(connection); await createEvmWallet(connection);
state = await AsyncValue.guard(() => listEvmWallets(connection));
await tsx.get(evmProvider.notifier).refreshWallets(); }
});
} }

View File

@@ -33,7 +33,7 @@ final class EvmProvider
Evm create() => Evm(); Evm create() => Evm();
} }
String _$evmHash() => r'ca2c9736065c5dc7cc45d8485000dd85dfbfa572'; String _$evmHash() => r'f5d05bfa7b820d0b96026a47ca47702a3793af5d';
abstract class _$Evm extends $AsyncNotifier<List<WalletEntry>?> { abstract class _$Evm extends $AsyncNotifier<List<WalletEntry>?> {
FutureOr<List<WalletEntry>?> build(); FutureOr<List<WalletEntry>?> build();

View File

@@ -1,6 +1,7 @@
import 'package:arbiter/features/connection/evm/grants.dart'; import 'package:arbiter/features/connection/evm/grants.dart';
import 'package:arbiter/proto/evm.pb.dart'; import 'package:arbiter/proto/evm.pb.dart';
import 'package:arbiter/providers/connection/connection_manager.dart'; import 'package:arbiter/providers/connection/connection_manager.dart';
import 'package:fixnum/fixnum.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hooks_riverpod/experimental/mutation.dart'; import 'package:hooks_riverpod/experimental/mutation.dart';
import 'package:mtcore/markettakers.dart'; import 'package:mtcore/markettakers.dart';
@@ -72,7 +73,14 @@ class EvmGrants extends _$EvmGrants {
Future<int> executeCreateEvmGrant( Future<int> executeCreateEvmGrant(
MutationTarget ref, { MutationTarget ref, {
required SharedSettings sharedSettings, required int clientId,
required int walletId,
required Int64 chainId,
DateTime? validFrom,
DateTime? validUntil,
List<int>? maxGasFeePerGas,
List<int>? maxPriorityFeePerGas,
TransactionRateLimit? rateLimit,
required SpecificGrant specific, required SpecificGrant specific,
}) { }) {
return createEvmGrantMutation.run(ref, (tsx) async { return createEvmGrantMutation.run(ref, (tsx) async {
@@ -83,7 +91,14 @@ Future<int> executeCreateEvmGrant(
final grantId = await createEvmGrant( final grantId = await createEvmGrant(
connection, connection,
sharedSettings: sharedSettings, clientId: clientId,
walletId: walletId,
chainId: chainId,
validFrom: validFrom,
validUntil: validUntil,
maxGasFeePerGas: maxGasFeePerGas,
maxPriorityFeePerGas: maxPriorityFeePerGas,
rateLimit: rateLimit,
specific: specific, specific: specific,
); );

View File

@@ -1,19 +0,0 @@
import 'package:arbiter/proto/user_agent.pb.dart';
import 'package:arbiter/providers/sdk_clients/list.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'details.g.dart';
@riverpod
Future<SdkClientEntry?> clientDetails(Ref ref, int clientId) async {
final clients = await ref.watch(sdkClientsProvider.future);
if (clients == null) {
return null;
}
for (final client in clients) {
if (client.id == clientId) {
return client;
}
}
return null;
}

View File

@@ -1,85 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'details.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(clientDetails)
final clientDetailsProvider = ClientDetailsFamily._();
final class ClientDetailsProvider
extends
$FunctionalProvider<
AsyncValue<SdkClientEntry?>,
SdkClientEntry?,
FutureOr<SdkClientEntry?>
>
with $FutureModifier<SdkClientEntry?>, $FutureProvider<SdkClientEntry?> {
ClientDetailsProvider._({
required ClientDetailsFamily super.from,
required int super.argument,
}) : super(
retry: null,
name: r'clientDetailsProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$clientDetailsHash();
@override
String toString() {
return r'clientDetailsProvider'
''
'($argument)';
}
@$internal
@override
$FutureProviderElement<SdkClientEntry?> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<SdkClientEntry?> create(Ref ref) {
final argument = this.argument as int;
return clientDetails(ref, argument);
}
@override
bool operator ==(Object other) {
return other is ClientDetailsProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$clientDetailsHash() => r'21449a1a2cc4fa4e65ce761e6342e97c1d957a7a';
final class ClientDetailsFamily extends $Family
with $FunctionalFamilyOverride<FutureOr<SdkClientEntry?>, int> {
ClientDetailsFamily._()
: super(
retry: null,
name: r'clientDetailsProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
ClientDetailsProvider call(int clientId) =>
ClientDetailsProvider._(argument: clientId, from: this);
@override
String toString() => r'clientDetailsProvider';
}

View File

@@ -1,34 +0,0 @@
import 'package:arbiter/proto/user_agent.pb.dart';
import 'package:arbiter/providers/connection/connection_manager.dart';
import 'package:protobuf/well_known_types/google/protobuf/empty.pb.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'list.g.dart';
@riverpod
Future<List<SdkClientEntry>?> sdkClients(Ref ref) async {
final connection = await ref.watch(connectionManagerProvider.future);
if (connection == null) {
return null;
}
final resp = await connection.ask(
UserAgentRequest(sdkClientList: Empty()),
);
if (!resp.hasSdkClientListResponse()) {
throw Exception(
'Expected SDK client list response, got ${resp.whichPayload()}',
);
}
final result = resp.sdkClientListResponse;
switch (result.whichResult()) {
case SdkClientListResponse_Result.clients:
return result.clients.clients.toList(growable: false);
case SdkClientListResponse_Result.error:
throw Exception('Error listing SDK clients: ${result.error}');
case SdkClientListResponse_Result.notSet:
throw Exception('SDK client list response was empty.');
}
}

View File

@@ -1,51 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'list.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(sdkClients)
final sdkClientsProvider = SdkClientsProvider._();
final class SdkClientsProvider
extends
$FunctionalProvider<
AsyncValue<List<SdkClientEntry>?>,
List<SdkClientEntry>?,
FutureOr<List<SdkClientEntry>?>
>
with
$FutureModifier<List<SdkClientEntry>?>,
$FutureProvider<List<SdkClientEntry>?> {
SdkClientsProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'sdkClientsProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$sdkClientsHash();
@$internal
@override
$FutureProviderElement<List<SdkClientEntry>?> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<List<SdkClientEntry>?> create(Ref ref) {
return sdkClients(ref);
}
}
String _$sdkClientsHash() => r'9b50ef901a7b68e4e604d6d0b4777dbd3e6499e1';

View File

@@ -1,174 +0,0 @@
import 'package:arbiter/features/connection/evm/wallet_access.dart';
import 'package:arbiter/proto/evm.pb.dart';
import 'package:arbiter/providers/connection/connection_manager.dart';
import 'package:arbiter/providers/evm/evm.dart';
import 'package:flutter/foundation.dart';
import 'package:hooks_riverpod/experimental/mutation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'wallet_access.g.dart';
class ClientWalletOption {
const ClientWalletOption({required this.walletId, required this.address});
final int walletId;
final String address;
}
class ClientWalletAccessState {
const ClientWalletAccessState({
this.searchQuery = '',
this.originalWalletIds = const {},
this.selectedWalletIds = const {},
});
final String searchQuery;
final Set<int> originalWalletIds;
final Set<int> selectedWalletIds;
bool get hasChanges => !setEquals(originalWalletIds, selectedWalletIds);
ClientWalletAccessState copyWith({
String? searchQuery,
Set<int>? originalWalletIds,
Set<int>? selectedWalletIds,
}) {
return ClientWalletAccessState(
searchQuery: searchQuery ?? this.searchQuery,
originalWalletIds: originalWalletIds ?? this.originalWalletIds,
selectedWalletIds: selectedWalletIds ?? this.selectedWalletIds,
);
}
}
final saveClientWalletAccessMutation = Mutation<void>();
abstract class ClientWalletAccessRepository {
Future<Set<int>> fetchSelectedWalletIds(int clientId);
Future<void> saveSelectedWalletIds(int clientId, Set<int> walletIds);
}
class ServerClientWalletAccessRepository
implements ClientWalletAccessRepository {
ServerClientWalletAccessRepository(this.ref);
final Ref ref;
@override
Future<Set<int>> fetchSelectedWalletIds(int clientId) async {
final connection = await ref.read(connectionManagerProvider.future);
if (connection == null) {
throw Exception('Not connected to the server.');
}
return readClientWalletAccess(connection, clientId: clientId);
}
@override
Future<void> saveSelectedWalletIds(int clientId, Set<int> walletIds) async {
final connection = await ref.read(connectionManagerProvider.future);
if (connection == null) {
throw Exception('Not connected to the server.');
}
await writeClientWalletAccess(
connection,
clientId: clientId,
walletIds: walletIds,
);
}
}
@riverpod
ClientWalletAccessRepository clientWalletAccessRepository(Ref ref) {
return ServerClientWalletAccessRepository(ref);
}
@riverpod
Future<List<ClientWalletOption>> clientWalletOptions(Ref ref) async {
final wallets = await ref.watch(evmProvider.future) ?? const <WalletEntry>[];
return [
for (var index = 0; index < wallets.length; index++)
ClientWalletOption(
walletId: index + 1,
address: formatWalletAddress(wallets[index].address),
),
];
}
@riverpod
Future<Set<int>> clientWalletAccessSelection(Ref ref, int clientId) async {
final repository = ref.watch(clientWalletAccessRepositoryProvider);
return repository.fetchSelectedWalletIds(clientId);
}
@riverpod
class ClientWalletAccessController extends _$ClientWalletAccessController {
@override
ClientWalletAccessState build(int clientId) {
final selection = ref.read(clientWalletAccessSelectionProvider(clientId));
void sync(AsyncValue<Set<int>> value) {
value.when(data: hydrate, error: (_, _) {}, loading: () {});
}
ref.listen<AsyncValue<Set<int>>>(
clientWalletAccessSelectionProvider(clientId),
(_, next) => sync(next),
);
return selection.when(
data: (walletIds) => ClientWalletAccessState(
originalWalletIds: Set.of(walletIds),
selectedWalletIds: Set.of(walletIds),
),
error: (error, _) => const ClientWalletAccessState(),
loading: () => const ClientWalletAccessState(),
);
}
void hydrate(Set<int> selectedWalletIds) {
state = state.copyWith(
originalWalletIds: Set.of(selectedWalletIds),
selectedWalletIds: Set.of(selectedWalletIds),
);
}
void setSearchQuery(String value) {
state = state.copyWith(searchQuery: value);
}
void toggleWallet(int walletId) {
final next = Set<int>.of(state.selectedWalletIds);
if (!next.add(walletId)) {
next.remove(walletId);
}
state = state.copyWith(selectedWalletIds: next);
}
void discardChanges() {
state = state.copyWith(selectedWalletIds: Set.of(state.originalWalletIds));
}
}
Future<void> executeSaveClientWalletAccess(
MutationTarget ref, {
required int clientId,
}) {
final mutation = saveClientWalletAccessMutation(clientId);
return mutation.run(ref, (tsx) async {
final repository = tsx.get(clientWalletAccessRepositoryProvider);
final controller = tsx.get(
clientWalletAccessControllerProvider(clientId).notifier,
);
final selectedWalletIds = tsx
.get(clientWalletAccessControllerProvider(clientId))
.selectedWalletIds;
await repository.saveSelectedWalletIds(clientId, selectedWalletIds);
controller.hydrate(selectedWalletIds);
});
}
String formatWalletAddress(List<int> bytes) {
final hex = bytes
.map((byte) => byte.toRadixString(16).padLeft(2, '0'))
.join();
return '0x$hex';
}

View File

@@ -1,280 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'wallet_access.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(clientWalletAccessRepository)
final clientWalletAccessRepositoryProvider =
ClientWalletAccessRepositoryProvider._();
final class ClientWalletAccessRepositoryProvider
extends
$FunctionalProvider<
ClientWalletAccessRepository,
ClientWalletAccessRepository,
ClientWalletAccessRepository
>
with $Provider<ClientWalletAccessRepository> {
ClientWalletAccessRepositoryProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'clientWalletAccessRepositoryProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$clientWalletAccessRepositoryHash();
@$internal
@override
$ProviderElement<ClientWalletAccessRepository> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
ClientWalletAccessRepository create(Ref ref) {
return clientWalletAccessRepository(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(ClientWalletAccessRepository value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<ClientWalletAccessRepository>(value),
);
}
}
String _$clientWalletAccessRepositoryHash() =>
r'bbc332284bc36a8b5d807bd5c45362b6b12b19e7';
@ProviderFor(clientWalletOptions)
final clientWalletOptionsProvider = ClientWalletOptionsProvider._();
final class ClientWalletOptionsProvider
extends
$FunctionalProvider<
AsyncValue<List<ClientWalletOption>>,
List<ClientWalletOption>,
FutureOr<List<ClientWalletOption>>
>
with
$FutureModifier<List<ClientWalletOption>>,
$FutureProvider<List<ClientWalletOption>> {
ClientWalletOptionsProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'clientWalletOptionsProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$clientWalletOptionsHash();
@$internal
@override
$FutureProviderElement<List<ClientWalletOption>> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<List<ClientWalletOption>> create(Ref ref) {
return clientWalletOptions(ref);
}
}
String _$clientWalletOptionsHash() =>
r'32183c2b281e2a41400de07f2381132a706815ab';
@ProviderFor(clientWalletAccessSelection)
final clientWalletAccessSelectionProvider =
ClientWalletAccessSelectionFamily._();
final class ClientWalletAccessSelectionProvider
extends
$FunctionalProvider<AsyncValue<Set<int>>, Set<int>, FutureOr<Set<int>>>
with $FutureModifier<Set<int>>, $FutureProvider<Set<int>> {
ClientWalletAccessSelectionProvider._({
required ClientWalletAccessSelectionFamily super.from,
required int super.argument,
}) : super(
retry: null,
name: r'clientWalletAccessSelectionProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$clientWalletAccessSelectionHash();
@override
String toString() {
return r'clientWalletAccessSelectionProvider'
''
'($argument)';
}
@$internal
@override
$FutureProviderElement<Set<int>> $createElement($ProviderPointer pointer) =>
$FutureProviderElement(pointer);
@override
FutureOr<Set<int>> create(Ref ref) {
final argument = this.argument as int;
return clientWalletAccessSelection(ref, argument);
}
@override
bool operator ==(Object other) {
return other is ClientWalletAccessSelectionProvider &&
other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$clientWalletAccessSelectionHash() =>
r'f33705ee7201cd9b899cc058d6642de85a22b03e';
final class ClientWalletAccessSelectionFamily extends $Family
with $FunctionalFamilyOverride<FutureOr<Set<int>>, int> {
ClientWalletAccessSelectionFamily._()
: super(
retry: null,
name: r'clientWalletAccessSelectionProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
ClientWalletAccessSelectionProvider call(int clientId) =>
ClientWalletAccessSelectionProvider._(argument: clientId, from: this);
@override
String toString() => r'clientWalletAccessSelectionProvider';
}
@ProviderFor(ClientWalletAccessController)
final clientWalletAccessControllerProvider =
ClientWalletAccessControllerFamily._();
final class ClientWalletAccessControllerProvider
extends
$NotifierProvider<
ClientWalletAccessController,
ClientWalletAccessState
> {
ClientWalletAccessControllerProvider._({
required ClientWalletAccessControllerFamily super.from,
required int super.argument,
}) : super(
retry: null,
name: r'clientWalletAccessControllerProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$clientWalletAccessControllerHash();
@override
String toString() {
return r'clientWalletAccessControllerProvider'
''
'($argument)';
}
@$internal
@override
ClientWalletAccessController create() => ClientWalletAccessController();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(ClientWalletAccessState value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<ClientWalletAccessState>(value),
);
}
@override
bool operator ==(Object other) {
return other is ClientWalletAccessControllerProvider &&
other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$clientWalletAccessControllerHash() =>
r'45bff81382fec3e8610190167b55667a7dfc1111';
final class ClientWalletAccessControllerFamily extends $Family
with
$ClassFamilyOverride<
ClientWalletAccessController,
ClientWalletAccessState,
ClientWalletAccessState,
ClientWalletAccessState,
int
> {
ClientWalletAccessControllerFamily._()
: super(
retry: null,
name: r'clientWalletAccessControllerProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
ClientWalletAccessControllerProvider call(int clientId) =>
ClientWalletAccessControllerProvider._(argument: clientId, from: this);
@override
String toString() => r'clientWalletAccessControllerProvider';
}
abstract class _$ClientWalletAccessController
extends $Notifier<ClientWalletAccessState> {
late final _$args = ref.$arg as int;
int get clientId => _$args;
ClientWalletAccessState build(int clientId);
@$mustCallSuper
@override
void runBuild() {
final ref =
this.ref as $Ref<ClientWalletAccessState, ClientWalletAccessState>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<ClientWalletAccessState, ClientWalletAccessState>,
ClientWalletAccessState,
Object?,
Object?
>;
element.handleCreate(ref, () => build(_$args));
}
}

View File

@@ -1,22 +0,0 @@
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
Future<List<SdkClientWalletAccess>?> walletAccessList(Ref ref) 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;
}
}

View File

@@ -1,51 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'wallet_access_list.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(walletAccessList)
final walletAccessListProvider = WalletAccessListProvider._();
final class WalletAccessListProvider
extends
$FunctionalProvider<
AsyncValue<List<SdkClientWalletAccess>?>,
List<SdkClientWalletAccess>?,
FutureOr<List<SdkClientWalletAccess>?>
>
with
$FutureModifier<List<SdkClientWalletAccess>?>,
$FutureProvider<List<SdkClientWalletAccess>?> {
WalletAccessListProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'walletAccessListProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$walletAccessListHash();
@$internal
@override
$FutureProviderElement<List<SdkClientWalletAccess>?> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<List<SdkClientWalletAccess>?> create(Ref ref) {
return walletAccessList(ref);
}
}
String _$walletAccessListHash() => r'c06006d6792ae463105a539723e9bb396192f96b';

Some files were not shown because too many files have changed in this diff Show More