@wlfi-agent/cli 1.4.12 → 1.4.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +3968 -0
- package/Cargo.toml +50 -0
- package/README.md +426 -6
- package/crates/vault-cli-admin/Cargo.toml +26 -0
- package/crates/vault-cli-admin/src/io_utils.rs +500 -0
- package/crates/vault-cli-admin/src/main.rs +3990 -0
- package/crates/vault-cli-admin/src/shared_config.rs +624 -0
- package/crates/vault-cli-admin/src/tui/amounts.rs +180 -0
- package/crates/vault-cli-admin/src/tui/token_rpc.rs +250 -0
- package/crates/vault-cli-admin/src/tui/utils.rs +82 -0
- package/crates/vault-cli-admin/src/tui.rs +3410 -0
- package/crates/vault-cli-agent/Cargo.toml +24 -0
- package/crates/vault-cli-agent/src/io_utils.rs +576 -0
- package/crates/vault-cli-agent/src/main.rs +833 -0
- package/crates/vault-cli-daemon/Cargo.toml +28 -0
- package/crates/vault-cli-daemon/src/bin/wlfi-agent-system-keychain.rs +216 -0
- package/crates/vault-cli-daemon/src/main.rs +644 -0
- package/crates/vault-cli-daemon/src/relay_sync.rs +894 -0
- package/crates/vault-cli-daemon/tests/system_keychain_helper_acl.rs +167 -0
- package/crates/vault-daemon/Cargo.toml +32 -0
- package/crates/vault-daemon/src/daemon_parts/api_impl_and_utils.rs +1041 -0
- package/crates/vault-daemon/src/daemon_parts/core_helpers.rs +1256 -0
- package/crates/vault-daemon/src/daemon_parts/types_api_rpc.rs +622 -0
- package/crates/vault-daemon/src/lib.rs +54 -0
- package/crates/vault-daemon/src/persistence.rs +441 -0
- package/crates/vault-daemon/src/tests.rs +237 -0
- package/crates/vault-daemon/src/tests_parts/part1.rs +1224 -0
- package/crates/vault-daemon/src/tests_parts/part2.rs +1021 -0
- package/crates/vault-daemon/src/tests_parts/part3.rs +835 -0
- package/crates/vault-daemon/src/tests_parts/part4.rs +604 -0
- package/crates/vault-domain/Cargo.toml +20 -0
- package/crates/vault-domain/src/action.rs +849 -0
- package/crates/vault-domain/src/address.rs +51 -0
- package/crates/vault-domain/src/approval.rs +90 -0
- package/crates/vault-domain/src/constants.rs +4 -0
- package/crates/vault-domain/src/error.rs +54 -0
- package/crates/vault-domain/src/keys.rs +71 -0
- package/crates/vault-domain/src/lib.rs +42 -0
- package/crates/vault-domain/src/nonce.rs +102 -0
- package/crates/vault-domain/src/policy.rs +172 -0
- package/crates/vault-domain/src/request.rs +53 -0
- package/crates/vault-domain/src/scope.rs +24 -0
- package/crates/vault-domain/src/session.rs +50 -0
- package/crates/vault-domain/src/signature.rs +34 -0
- package/crates/vault-domain/src/tests.rs +651 -0
- package/crates/vault-domain/src/u128_as_decimal_string.rs +44 -0
- package/crates/vault-policy/Cargo.toml +17 -0
- package/crates/vault-policy/src/engine.rs +301 -0
- package/crates/vault-policy/src/error.rs +81 -0
- package/crates/vault-policy/src/lib.rs +17 -0
- package/crates/vault-policy/src/report.rs +34 -0
- package/crates/vault-policy/src/tests.rs +891 -0
- package/crates/vault-policy/src/tests_explain.rs +78 -0
- package/crates/vault-sdk-agent/Cargo.toml +21 -0
- package/crates/vault-sdk-agent/src/lib.rs +711 -0
- package/crates/vault-signer/Cargo.toml +25 -0
- package/crates/vault-signer/src/lib.rs +731 -0
- package/crates/vault-signer/tests/secure_enclave_acl.rs +54 -0
- package/crates/vault-transport-unix/Cargo.toml +24 -0
- package/crates/vault-transport-unix/src/lib.rs +1640 -0
- package/crates/vault-transport-xpc/Cargo.toml +25 -0
- package/crates/vault-transport-xpc/src/client_codec_api.rs +635 -0
- package/crates/vault-transport-xpc/src/lib.rs +680 -0
- package/crates/vault-transport-xpc/src/tests.rs +818 -0
- package/crates/vault-transport-xpc/tests/e2e_flow.rs +773 -0
- package/dist/cli.cjs +35088 -0
- package/dist/cli.cjs.map +1 -0
- package/package.json +49 -43
- package/packages/cache/.turbo/turbo-build.log +52 -0
- package/packages/cache/dist/chunk-2QFWMUXT.cjs +43 -0
- package/packages/cache/dist/chunk-2QFWMUXT.cjs.map +1 -0
- package/packages/cache/dist/chunk-4U63TZTQ.js +43 -0
- package/packages/cache/dist/chunk-4U63TZTQ.js.map +1 -0
- package/packages/cache/dist/chunk-ALQ6H7KG.cjs +404 -0
- package/packages/cache/dist/chunk-ALQ6H7KG.cjs.map +1 -0
- package/packages/cache/dist/chunk-FGJEEF5N.js +404 -0
- package/packages/cache/dist/chunk-FGJEEF5N.js.map +1 -0
- package/packages/cache/dist/chunk-UYNEHZHB.cjs +45 -0
- package/packages/cache/dist/chunk-UYNEHZHB.cjs.map +1 -0
- package/packages/cache/dist/chunk-VXVMPG3W.js +45 -0
- package/packages/cache/dist/chunk-VXVMPG3W.js.map +1 -0
- package/packages/cache/dist/client/index.cjs +11 -0
- package/packages/cache/dist/client/index.cjs.map +1 -0
- package/packages/cache/dist/client/index.d.cts +15 -0
- package/packages/cache/dist/client/index.d.ts +15 -0
- package/packages/cache/dist/client/index.js +11 -0
- package/packages/cache/dist/client/index.js.map +1 -0
- package/packages/cache/dist/errors/index.cjs +11 -0
- package/packages/cache/dist/errors/index.cjs.map +1 -0
- package/packages/cache/dist/errors/index.d.cts +26 -0
- package/packages/cache/dist/errors/index.d.ts +26 -0
- package/packages/cache/dist/errors/index.js +11 -0
- package/packages/cache/dist/errors/index.js.map +1 -0
- package/packages/cache/dist/index.cjs +29 -0
- package/packages/cache/dist/index.cjs.map +1 -0
- package/packages/cache/dist/index.d.cts +4 -0
- package/packages/cache/dist/index.d.ts +4 -0
- package/packages/cache/dist/index.js +29 -0
- package/packages/cache/dist/index.js.map +1 -0
- package/packages/cache/dist/service/index.cjs +15 -0
- package/packages/cache/dist/service/index.cjs.map +1 -0
- package/packages/cache/dist/service/index.d.cts +184 -0
- package/packages/cache/dist/service/index.d.ts +184 -0
- package/packages/cache/dist/service/index.js +15 -0
- package/packages/cache/dist/service/index.js.map +1 -0
- package/packages/cache/node_modules/.bin/jiti +17 -0
- package/packages/cache/node_modules/.bin/tsc +17 -0
- package/packages/cache/node_modules/.bin/tsserver +17 -0
- package/packages/cache/node_modules/.bin/tsup +17 -0
- package/packages/cache/node_modules/.bin/tsup-node +17 -0
- package/packages/cache/node_modules/.bin/tsx +17 -0
- package/packages/cache/node_modules/.bin/vitest +17 -0
- package/packages/cache/package.json +48 -0
- package/packages/cache/src/client/index.ts +56 -0
- package/packages/cache/src/errors/index.ts +53 -0
- package/packages/cache/src/index.ts +3 -0
- package/packages/cache/src/service/index.test.ts +263 -0
- package/packages/cache/src/service/index.ts +678 -0
- package/packages/cache/tsconfig.json +13 -0
- package/packages/cache/tsup.config.ts +13 -0
- package/packages/cache/vitest.config.ts +16 -0
- package/packages/config/.turbo/turbo-build.log +18 -0
- package/packages/config/dist/index.cjs +1037 -0
- package/packages/config/dist/index.cjs.map +1 -0
- package/packages/config/dist/index.d.ts +131 -0
- package/packages/config/node_modules/.bin/jiti +17 -0
- package/packages/config/node_modules/.bin/tsc +17 -0
- package/packages/config/node_modules/.bin/tsserver +17 -0
- package/packages/config/node_modules/.bin/tsup +17 -0
- package/packages/config/node_modules/.bin/tsup-node +17 -0
- package/packages/config/node_modules/.bin/tsx +17 -0
- package/packages/config/package.json +21 -0
- package/packages/config/src/index.js +1 -0
- package/packages/config/src/index.ts +1282 -0
- package/packages/config/tsconfig.json +4 -0
- package/packages/rpc/.turbo/turbo-build.log +32 -0
- package/packages/rpc/dist/_esm-BCLXDO2R.cjs +3660 -0
- package/packages/rpc/dist/_esm-BCLXDO2R.cjs.map +1 -0
- package/packages/rpc/dist/ccip-OWJLAW55.cjs +16 -0
- package/packages/rpc/dist/ccip-OWJLAW55.cjs.map +1 -0
- package/packages/rpc/dist/chunk-APQIFZ3B.cjs +6247 -0
- package/packages/rpc/dist/chunk-APQIFZ3B.cjs.map +1 -0
- package/packages/rpc/dist/chunk-CDO2GWRD.cjs +410 -0
- package/packages/rpc/dist/chunk-CDO2GWRD.cjs.map +1 -0
- package/packages/rpc/dist/chunk-QGTNTFJ7.cjs +2249 -0
- package/packages/rpc/dist/chunk-QGTNTFJ7.cjs.map +1 -0
- package/packages/rpc/dist/chunk-TZDTAHWR.cjs +44 -0
- package/packages/rpc/dist/chunk-TZDTAHWR.cjs.map +1 -0
- package/packages/rpc/dist/index.cjs +7342 -0
- package/packages/rpc/dist/index.cjs.map +1 -0
- package/packages/rpc/dist/index.d.ts +3857 -0
- package/packages/rpc/dist/secp256k1-WCNM675D.cjs +18 -0
- package/packages/rpc/dist/secp256k1-WCNM675D.cjs.map +1 -0
- package/packages/rpc/node_modules/.bin/jiti +17 -0
- package/packages/rpc/node_modules/.bin/tsc +17 -0
- package/packages/rpc/node_modules/.bin/tsserver +17 -0
- package/packages/rpc/node_modules/.bin/tsup +17 -0
- package/packages/rpc/node_modules/.bin/tsup-node +17 -0
- package/packages/rpc/node_modules/.bin/tsx +17 -0
- package/packages/rpc/package.json +25 -0
- package/packages/rpc/src/index.ts +206 -0
- package/packages/rpc/tsconfig.json +4 -0
- package/packages/typescript/base.json +36 -0
- package/packages/typescript/nextjs.json +17 -0
- package/packages/typescript/package.json +10 -0
- package/packages/ui/.turbo/turbo-build.log +44 -0
- package/packages/ui/dist/chunk-MOAFBKSA.js +11 -0
- package/packages/ui/dist/chunk-MOAFBKSA.js.map +1 -0
- package/packages/ui/dist/components/badge.d.ts +12 -0
- package/packages/ui/dist/components/badge.js +31 -0
- package/packages/ui/dist/components/badge.js.map +1 -0
- package/packages/ui/dist/components/button.d.ts +13 -0
- package/packages/ui/dist/components/button.js +40 -0
- package/packages/ui/dist/components/button.js.map +1 -0
- package/packages/ui/dist/components/card.d.ts +10 -0
- package/packages/ui/dist/components/card.js +39 -0
- package/packages/ui/dist/components/card.js.map +1 -0
- package/packages/ui/dist/components/input.d.ts +5 -0
- package/packages/ui/dist/components/input.js +28 -0
- package/packages/ui/dist/components/input.js.map +1 -0
- package/packages/ui/dist/components/label.d.ts +5 -0
- package/packages/ui/dist/components/label.js +13 -0
- package/packages/ui/dist/components/label.js.map +1 -0
- package/packages/ui/dist/components/separator.d.ts +5 -0
- package/packages/ui/dist/components/separator.js +13 -0
- package/packages/ui/dist/components/separator.js.map +1 -0
- package/packages/ui/dist/components/textarea.d.ts +5 -0
- package/packages/ui/dist/components/textarea.js +27 -0
- package/packages/ui/dist/components/textarea.js.map +1 -0
- package/packages/ui/dist/tailwind.d.ts +56 -0
- package/packages/ui/dist/tailwind.js +60 -0
- package/packages/ui/dist/tailwind.js.map +1 -0
- package/packages/ui/dist/utils/cn.d.ts +5 -0
- package/packages/ui/dist/utils/cn.js +7 -0
- package/packages/ui/dist/utils/cn.js.map +1 -0
- package/packages/ui/node_modules/.bin/jiti +17 -0
- package/packages/ui/node_modules/.bin/tsc +17 -0
- package/packages/ui/node_modules/.bin/tsserver +17 -0
- package/packages/ui/node_modules/.bin/tsup +17 -0
- package/packages/ui/node_modules/.bin/tsup-node +17 -0
- package/packages/ui/node_modules/.bin/tsx +17 -0
- package/packages/ui/package.json +69 -0
- package/packages/ui/src/components/badge.tsx +27 -0
- package/packages/ui/src/components/button.tsx +40 -0
- package/packages/ui/src/components/card.tsx +31 -0
- package/packages/ui/src/components/input.tsx +21 -0
- package/packages/ui/src/components/label.tsx +6 -0
- package/packages/ui/src/components/separator.tsx +6 -0
- package/packages/ui/src/components/textarea.tsx +20 -0
- package/packages/ui/src/globals.css +70 -0
- package/packages/ui/src/tailwind.ts +56 -0
- package/packages/ui/src/utils/cn.ts +6 -0
- package/packages/ui/tsconfig.json +20 -0
- package/packages/ui/tsup.config.ts +20 -0
- package/pnpm-workspace.yaml +4 -0
- package/scripts/install-rust-binaries.mjs +84 -0
- package/scripts/launchd/install-user-daemon.sh +358 -0
- package/scripts/launchd/run-vault-daemon.sh +5 -0
- package/scripts/launchd/run-wlfi-agent-daemon.sh +73 -0
- package/scripts/launchd/uninstall-user-daemon.sh +103 -0
- package/src/cli.ts +2121 -0
- package/src/lib/admin-guard.js +1 -0
- package/src/lib/admin-guard.ts +185 -0
- package/src/lib/admin-passthrough.ts +33 -0
- package/src/lib/admin-reset.ts +751 -0
- package/src/lib/admin-setup.ts +1612 -0
- package/src/lib/agent-auth-clear.js +1 -0
- package/src/lib/agent-auth-clear.ts +58 -0
- package/src/lib/agent-auth-forwarding.js +1 -0
- package/src/lib/agent-auth-forwarding.ts +149 -0
- package/src/lib/agent-auth-migrate.js +1 -0
- package/src/lib/agent-auth-migrate.ts +150 -0
- package/src/lib/agent-auth-revoke.ts +103 -0
- package/src/lib/agent-auth-rotate.ts +107 -0
- package/src/lib/agent-auth-token.js +1 -0
- package/src/lib/agent-auth-token.ts +25 -0
- package/src/lib/agent-auth.ts +89 -0
- package/src/lib/asset-broadcast.js +1 -0
- package/src/lib/asset-broadcast.ts +285 -0
- package/src/lib/bootstrap-artifacts.js +1 -0
- package/src/lib/bootstrap-artifacts.ts +205 -0
- package/src/lib/bootstrap-credentials.js +1 -0
- package/src/lib/bootstrap-credentials.ts +832 -0
- package/src/lib/config-amounts.js +1 -0
- package/src/lib/config-amounts.ts +189 -0
- package/src/lib/config-mutation.ts +27 -0
- package/src/lib/fs-trust.js +1 -0
- package/src/lib/fs-trust.ts +537 -0
- package/src/lib/keychain.js +1 -0
- package/src/lib/keychain.ts +225 -0
- package/src/lib/local-admin-access.ts +106 -0
- package/src/lib/network-selection.js +1 -0
- package/src/lib/network-selection.ts +71 -0
- package/src/lib/passthrough-security.js +1 -0
- package/src/lib/passthrough-security.ts +114 -0
- package/src/lib/rpc-guard.js +1 -0
- package/src/lib/rpc-guard.ts +7 -0
- package/src/lib/rust-spawn-options.js +1 -0
- package/src/lib/rust-spawn-options.ts +98 -0
- package/src/lib/rust.js +1 -0
- package/src/lib/rust.ts +143 -0
- package/src/lib/signed-tx.js +1 -0
- package/src/lib/signed-tx.ts +116 -0
- package/src/lib/status-repair-cli.ts +116 -0
- package/src/lib/sudo.js +1 -0
- package/src/lib/sudo.ts +172 -0
- package/src/lib/vault-password-forwarding.js +1 -0
- package/src/lib/vault-password-forwarding.ts +155 -0
- package/src/lib/wallet-profile.js +1 -0
- package/src/lib/wallet-profile.ts +332 -0
- package/src/lib/wallet-repair.js +1 -0
- package/src/lib/wallet-repair.ts +304 -0
- package/src/lib/wallet-setup.js +1 -0
- package/src/lib/wallet-setup.ts +1466 -0
- package/src/lib/wallet-status.js +1 -0
- package/src/lib/wallet-status.ts +640 -0
- package/tsconfig.base.json +17 -0
- package/tsconfig.json +10 -0
- package/tsup.config.ts +25 -0
- package/turbo.json +41 -0
- package/LICENSE.md +0 -1
- package/dist/wlfa/index.cjs +0 -250
- package/dist/wlfa/index.d.cts +0 -1
- package/dist/wlfa/index.d.ts +0 -1
- package/dist/wlfa/index.js +0 -250
- package/dist/wlfc/index.cjs +0 -1894
- package/dist/wlfc/index.d.cts +0 -1
- package/dist/wlfc/index.d.ts +0 -1
- package/dist/wlfc/index.js +0 -1894
|
@@ -0,0 +1,849 @@
|
|
|
1
|
+
use alloy_primitives::{aliases::U48, Address, U160, U256};
|
|
2
|
+
use alloy_sol_types::{eip712_domain, sol, Eip712Domain, SolCall, SolStruct};
|
|
3
|
+
use serde::{Deserialize, Serialize};
|
|
4
|
+
|
|
5
|
+
use crate::u128_as_decimal_string;
|
|
6
|
+
use crate::{AssetId, DomainError, EvmAddress};
|
|
7
|
+
|
|
8
|
+
sol! {
|
|
9
|
+
function approve(address spender, uint256 value);
|
|
10
|
+
function transfer(address to, uint256 value);
|
|
11
|
+
function permit(address owner, PermitSingle permitSingle, bytes signature);
|
|
12
|
+
function transferWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, bytes signature);
|
|
13
|
+
function receiveWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, bytes signature);
|
|
14
|
+
|
|
15
|
+
struct PermitDetails {
|
|
16
|
+
address token;
|
|
17
|
+
uint160 amount;
|
|
18
|
+
uint48 expiration;
|
|
19
|
+
uint48 nonce;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
struct PermitSingle {
|
|
23
|
+
PermitDetails details;
|
|
24
|
+
address spender;
|
|
25
|
+
uint256 sigDeadline;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
struct TransferWithAuthorization {
|
|
29
|
+
address from;
|
|
30
|
+
address to;
|
|
31
|
+
uint256 value;
|
|
32
|
+
uint256 validAfter;
|
|
33
|
+
uint256 validBefore;
|
|
34
|
+
bytes32 nonce;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
struct ReceiveWithAuthorization {
|
|
38
|
+
address from;
|
|
39
|
+
address to;
|
|
40
|
+
uint256 value;
|
|
41
|
+
uint256 validAfter;
|
|
42
|
+
uint256 validBefore;
|
|
43
|
+
bytes32 nonce;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// Decoded ERC-20 method call.
|
|
48
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
49
|
+
pub enum Erc20Call {
|
|
50
|
+
/// `approve(spender, value)`
|
|
51
|
+
Approve {
|
|
52
|
+
/// Spender address.
|
|
53
|
+
spender: EvmAddress,
|
|
54
|
+
/// Approved amount in wei.
|
|
55
|
+
amount_wei: u128,
|
|
56
|
+
},
|
|
57
|
+
/// `transfer(to, value)`
|
|
58
|
+
Transfer {
|
|
59
|
+
/// Recipient address.
|
|
60
|
+
to: EvmAddress,
|
|
61
|
+
/// Transfer amount in wei.
|
|
62
|
+
amount_wei: u128,
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// Permit2 `PermitSingle` authorization signed for the Permit2 contract.
|
|
67
|
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
68
|
+
pub struct Permit2Permit {
|
|
69
|
+
/// EVM network chain ID.
|
|
70
|
+
pub chain_id: u64,
|
|
71
|
+
/// Permit2 verifying contract address.
|
|
72
|
+
pub permit2_contract: EvmAddress,
|
|
73
|
+
/// ERC-20 token contract address.
|
|
74
|
+
pub token: EvmAddress,
|
|
75
|
+
/// Authorized spender.
|
|
76
|
+
pub spender: EvmAddress,
|
|
77
|
+
/// Approved amount in wei.
|
|
78
|
+
#[serde(with = "u128_as_decimal_string")]
|
|
79
|
+
pub amount_wei: u128,
|
|
80
|
+
/// Permit expiration timestamp.
|
|
81
|
+
pub expiration: u64,
|
|
82
|
+
/// Permit nonce.
|
|
83
|
+
pub nonce: u64,
|
|
84
|
+
/// Signature deadline timestamp.
|
|
85
|
+
pub sig_deadline: u64,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
impl Permit2Permit {
|
|
89
|
+
/// Validates structural constraints and `uint160` / `uint48` field widths.
|
|
90
|
+
pub fn validate(&self) -> Result<(), DomainError> {
|
|
91
|
+
if self.chain_id == 0 {
|
|
92
|
+
return Err(DomainError::InvalidChainId);
|
|
93
|
+
}
|
|
94
|
+
if self.amount_wei == 0 {
|
|
95
|
+
return Err(DomainError::InvalidAmount);
|
|
96
|
+
}
|
|
97
|
+
if self.expiration > permit2_max_timestamp() || self.nonce > permit2_max_timestamp() {
|
|
98
|
+
return Err(DomainError::AmountOutOfRange);
|
|
99
|
+
}
|
|
100
|
+
Ok(())
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// Returns the Permit2 EIP-712 domain.
|
|
104
|
+
#[must_use]
|
|
105
|
+
pub fn eip712_domain(&self) -> Eip712Domain {
|
|
106
|
+
eip712_domain! {
|
|
107
|
+
name: "Permit2",
|
|
108
|
+
chain_id: self.chain_id,
|
|
109
|
+
verifying_contract: evm_to_alloy_address(&self.permit2_contract),
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// Returns the EIP-712 signing digest for this authorization.
|
|
114
|
+
pub fn signing_hash(&self) -> Result<[u8; 32], DomainError> {
|
|
115
|
+
self.validate()?;
|
|
116
|
+
let details = PermitDetails {
|
|
117
|
+
token: evm_to_alloy_address(&self.token),
|
|
118
|
+
amount: u128_to_u160(self.amount_wei),
|
|
119
|
+
expiration: u64_to_u48(self.expiration),
|
|
120
|
+
nonce: u64_to_u48(self.nonce),
|
|
121
|
+
};
|
|
122
|
+
let permit = PermitSingle {
|
|
123
|
+
details,
|
|
124
|
+
spender: evm_to_alloy_address(&self.spender),
|
|
125
|
+
sigDeadline: U256::from(self.sig_deadline),
|
|
126
|
+
};
|
|
127
|
+
Ok(permit.eip712_signing_hash(&self.eip712_domain()).0)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/// EIP-3009 transfer authorization signed for a token contract.
|
|
132
|
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
133
|
+
pub struct Eip3009Transfer {
|
|
134
|
+
/// EVM network chain ID.
|
|
135
|
+
pub chain_id: u64,
|
|
136
|
+
/// Token contract address used as EIP-712 verifying contract.
|
|
137
|
+
pub token: EvmAddress,
|
|
138
|
+
/// Token name used by the token's EIP-712 domain separator.
|
|
139
|
+
pub token_name: String,
|
|
140
|
+
/// Token version used by the token's EIP-712 domain separator.
|
|
141
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
142
|
+
pub token_version: Option<String>,
|
|
143
|
+
/// Authorization signer / owner.
|
|
144
|
+
pub from: EvmAddress,
|
|
145
|
+
/// Authorized recipient.
|
|
146
|
+
pub to: EvmAddress,
|
|
147
|
+
/// Transfer amount in wei.
|
|
148
|
+
#[serde(with = "u128_as_decimal_string")]
|
|
149
|
+
pub amount_wei: u128,
|
|
150
|
+
/// Authorization validity lower bound.
|
|
151
|
+
pub valid_after: u64,
|
|
152
|
+
/// Authorization validity upper bound.
|
|
153
|
+
pub valid_before: u64,
|
|
154
|
+
/// Authorization nonce as `0x`-prefixed 32-byte hex.
|
|
155
|
+
pub nonce_hex: String,
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
impl Eip3009Transfer {
|
|
159
|
+
/// Validates structural constraints.
|
|
160
|
+
pub fn validate(&self) -> Result<(), DomainError> {
|
|
161
|
+
if self.chain_id == 0 {
|
|
162
|
+
return Err(DomainError::InvalidChainId);
|
|
163
|
+
}
|
|
164
|
+
if self.amount_wei == 0 {
|
|
165
|
+
return Err(DomainError::InvalidAmount);
|
|
166
|
+
}
|
|
167
|
+
if self.token_name.trim().is_empty() {
|
|
168
|
+
return Err(DomainError::InvalidTypedDataDomain(
|
|
169
|
+
"token_name must not be empty".to_string(),
|
|
170
|
+
));
|
|
171
|
+
}
|
|
172
|
+
if self.valid_before <= self.valid_after {
|
|
173
|
+
return Err(DomainError::InvalidAuthorizationWindow);
|
|
174
|
+
}
|
|
175
|
+
let _ = self.nonce_bytes32()?;
|
|
176
|
+
Ok(())
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/// Parses the nonce as a strict 32-byte value.
|
|
180
|
+
pub fn nonce_bytes32(&self) -> Result<[u8; 32], DomainError> {
|
|
181
|
+
decode_hex_32(&self.nonce_hex, "eip3009 nonce")
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/// Returns the EIP-712 domain for this token authorization.
|
|
185
|
+
#[must_use]
|
|
186
|
+
pub fn eip712_domain(&self) -> Eip712Domain {
|
|
187
|
+
Eip712Domain::new(
|
|
188
|
+
Some(self.token_name.clone().into()),
|
|
189
|
+
self.token_version
|
|
190
|
+
.as_ref()
|
|
191
|
+
.filter(|value| !value.is_empty())
|
|
192
|
+
.cloned()
|
|
193
|
+
.map(Into::into),
|
|
194
|
+
Some(U256::from(self.chain_id)),
|
|
195
|
+
Some(evm_to_alloy_address(&self.token)),
|
|
196
|
+
None,
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/// Returns the EIP-712 signing digest for `transferWithAuthorization`.
|
|
201
|
+
pub fn transfer_signing_hash(&self) -> Result<[u8; 32], DomainError> {
|
|
202
|
+
self.validate()?;
|
|
203
|
+
let auth = TransferWithAuthorization {
|
|
204
|
+
from: evm_to_alloy_address(&self.from),
|
|
205
|
+
to: evm_to_alloy_address(&self.to),
|
|
206
|
+
value: U256::from(self.amount_wei),
|
|
207
|
+
validAfter: U256::from(self.valid_after),
|
|
208
|
+
validBefore: U256::from(self.valid_before),
|
|
209
|
+
nonce: self.nonce_bytes32().map(Into::into)?,
|
|
210
|
+
};
|
|
211
|
+
Ok(auth.eip712_signing_hash(&self.eip712_domain()).0)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/// Returns the EIP-712 signing digest for `receiveWithAuthorization`.
|
|
215
|
+
pub fn receive_signing_hash(&self) -> Result<[u8; 32], DomainError> {
|
|
216
|
+
self.validate()?;
|
|
217
|
+
let auth = ReceiveWithAuthorization {
|
|
218
|
+
from: evm_to_alloy_address(&self.from),
|
|
219
|
+
to: evm_to_alloy_address(&self.to),
|
|
220
|
+
value: U256::from(self.amount_wei),
|
|
221
|
+
validAfter: U256::from(self.valid_after),
|
|
222
|
+
validBefore: U256::from(self.valid_before),
|
|
223
|
+
nonce: self.nonce_bytes32().map(Into::into)?,
|
|
224
|
+
};
|
|
225
|
+
Ok(auth.eip712_signing_hash(&self.eip712_domain()).0)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/// Agent-submitted broadcast transaction.
|
|
230
|
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
231
|
+
pub struct BroadcastTx {
|
|
232
|
+
/// EVM network chain id.
|
|
233
|
+
pub chain_id: u64,
|
|
234
|
+
/// Sender account nonce.
|
|
235
|
+
pub nonce: u64,
|
|
236
|
+
/// Destination address.
|
|
237
|
+
pub to: EvmAddress,
|
|
238
|
+
/// Native value in wei.
|
|
239
|
+
#[serde(with = "u128_as_decimal_string")]
|
|
240
|
+
pub value_wei: u128,
|
|
241
|
+
/// Transaction calldata as hex (`0x`-prefixed or plain).
|
|
242
|
+
pub data_hex: String,
|
|
243
|
+
/// Gas limit.
|
|
244
|
+
pub gas_limit: u64,
|
|
245
|
+
/// Max fee per gas in wei.
|
|
246
|
+
#[serde(with = "u128_as_decimal_string")]
|
|
247
|
+
pub max_fee_per_gas_wei: u128,
|
|
248
|
+
/// Max priority fee per gas in wei.
|
|
249
|
+
#[serde(with = "u128_as_decimal_string")]
|
|
250
|
+
pub max_priority_fee_per_gas_wei: u128,
|
|
251
|
+
/// Transaction type id (`0x02` for EIP-1559, `0x04` for EIP-7702).
|
|
252
|
+
pub tx_type: u8,
|
|
253
|
+
/// Whether tx includes delegation authorization material.
|
|
254
|
+
pub delegation_enabled: bool,
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
impl BroadcastTx {
|
|
258
|
+
/// Returns calldata bytes after strict hex decode.
|
|
259
|
+
pub fn data_bytes(&self) -> Result<Vec<u8>, DomainError> {
|
|
260
|
+
decode_hex_payload(&self.data_hex)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/// Returns maximum fee exposure for this tx (`gas_limit * max_fee_per_gas_wei`).
|
|
264
|
+
pub fn max_gas_spend_wei(&self) -> Result<u128, DomainError> {
|
|
265
|
+
u128::from(self.gas_limit)
|
|
266
|
+
.checked_mul(self.max_fee_per_gas_wei)
|
|
267
|
+
.ok_or(DomainError::AmountOutOfRange)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/// Validates structural tx constraints.
|
|
271
|
+
pub fn validate(&self) -> Result<(), DomainError> {
|
|
272
|
+
if self.chain_id == 0 {
|
|
273
|
+
return Err(DomainError::InvalidChainId);
|
|
274
|
+
}
|
|
275
|
+
if self.gas_limit == 0 || self.max_fee_per_gas_wei == 0 {
|
|
276
|
+
return Err(DomainError::InvalidGasConfiguration);
|
|
277
|
+
}
|
|
278
|
+
if self.max_priority_fee_per_gas_wei > self.max_fee_per_gas_wei {
|
|
279
|
+
return Err(DomainError::InvalidGasConfiguration);
|
|
280
|
+
}
|
|
281
|
+
if self.delegation_enabled {
|
|
282
|
+
return Err(DomainError::DelegationNotAllowed);
|
|
283
|
+
}
|
|
284
|
+
let data = self.data_bytes()?;
|
|
285
|
+
if parse_broadcast_policy_call(self, &data).is_ok() && self.value_wei > 0 {
|
|
286
|
+
return Err(DomainError::Erc20CallWithNativeValue);
|
|
287
|
+
}
|
|
288
|
+
let _ = self.max_gas_spend_wei()?;
|
|
289
|
+
Ok(())
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/// Returns typed EIP-1559 signing message (`0x02 || rlp(unsigned_fields)`).
|
|
293
|
+
pub fn eip1559_signing_message(&self) -> Result<Vec<u8>, DomainError> {
|
|
294
|
+
self.validate()?;
|
|
295
|
+
if self.tx_type != 0x02 {
|
|
296
|
+
return Err(DomainError::UnsupportedTransactionType(self.tx_type));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
let data = self.data_bytes()?;
|
|
300
|
+
let to = decode_hex_payload(self.to.as_str())?;
|
|
301
|
+
let unsigned_fields = vec![
|
|
302
|
+
rlp_encode_u64(self.chain_id),
|
|
303
|
+
rlp_encode_u64(self.nonce),
|
|
304
|
+
rlp_encode_u128(self.max_priority_fee_per_gas_wei),
|
|
305
|
+
rlp_encode_u128(self.max_fee_per_gas_wei),
|
|
306
|
+
rlp_encode_u64(self.gas_limit),
|
|
307
|
+
rlp_encode_bytes(&to),
|
|
308
|
+
rlp_encode_u128(self.value_wei),
|
|
309
|
+
rlp_encode_bytes(&data),
|
|
310
|
+
rlp_encode_list(&[]), // empty access list
|
|
311
|
+
];
|
|
312
|
+
|
|
313
|
+
let mut out = vec![0x02];
|
|
314
|
+
out.extend(rlp_encode_list(&unsigned_fields));
|
|
315
|
+
Ok(out)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/// Builds typed EIP-1559 signed raw transaction bytes.
|
|
319
|
+
pub fn eip1559_signed_raw_transaction(
|
|
320
|
+
&self,
|
|
321
|
+
y_parity: u8,
|
|
322
|
+
r: [u8; 32],
|
|
323
|
+
s: [u8; 32],
|
|
324
|
+
) -> Result<Vec<u8>, DomainError> {
|
|
325
|
+
if y_parity > 1 {
|
|
326
|
+
return Err(DomainError::InvalidSignatureParity);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
self.validate()?;
|
|
330
|
+
if self.tx_type != 0x02 {
|
|
331
|
+
return Err(DomainError::UnsupportedTransactionType(self.tx_type));
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
let data = self.data_bytes()?;
|
|
335
|
+
let to = decode_hex_payload(self.to.as_str())?;
|
|
336
|
+
|
|
337
|
+
let signed_fields = vec![
|
|
338
|
+
rlp_encode_u64(self.chain_id),
|
|
339
|
+
rlp_encode_u64(self.nonce),
|
|
340
|
+
rlp_encode_u128(self.max_priority_fee_per_gas_wei),
|
|
341
|
+
rlp_encode_u128(self.max_fee_per_gas_wei),
|
|
342
|
+
rlp_encode_u64(self.gas_limit),
|
|
343
|
+
rlp_encode_bytes(&to),
|
|
344
|
+
rlp_encode_u128(self.value_wei),
|
|
345
|
+
rlp_encode_bytes(&data),
|
|
346
|
+
rlp_encode_list(&[]), // empty access list
|
|
347
|
+
rlp_encode_u64(u64::from(y_parity)),
|
|
348
|
+
rlp_encode_u256_bytes(&r),
|
|
349
|
+
rlp_encode_u256_bytes(&s),
|
|
350
|
+
];
|
|
351
|
+
|
|
352
|
+
let mut out = vec![0x02];
|
|
353
|
+
out.extend(rlp_encode_list(&signed_fields));
|
|
354
|
+
Ok(out)
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/// Actions an agent can request the daemon to sign.
|
|
359
|
+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
360
|
+
#[serde(tag = "kind")]
|
|
361
|
+
pub enum AgentAction {
|
|
362
|
+
/// ERC-20 approve.
|
|
363
|
+
Approve {
|
|
364
|
+
/// EVM network chain ID.
|
|
365
|
+
chain_id: u64,
|
|
366
|
+
/// Token contract address.
|
|
367
|
+
token: EvmAddress,
|
|
368
|
+
/// Spender address.
|
|
369
|
+
spender: EvmAddress,
|
|
370
|
+
/// Approved amount in wei.
|
|
371
|
+
#[serde(with = "u128_as_decimal_string")]
|
|
372
|
+
amount_wei: u128,
|
|
373
|
+
},
|
|
374
|
+
/// ERC-20 transfer.
|
|
375
|
+
Transfer {
|
|
376
|
+
/// EVM network chain ID.
|
|
377
|
+
chain_id: u64,
|
|
378
|
+
/// Token contract address.
|
|
379
|
+
token: EvmAddress,
|
|
380
|
+
/// Recipient address.
|
|
381
|
+
to: EvmAddress,
|
|
382
|
+
/// Transfer amount in wei.
|
|
383
|
+
#[serde(with = "u128_as_decimal_string")]
|
|
384
|
+
amount_wei: u128,
|
|
385
|
+
},
|
|
386
|
+
/// Native ETH transfer.
|
|
387
|
+
TransferNative {
|
|
388
|
+
/// EVM network chain ID.
|
|
389
|
+
chain_id: u64,
|
|
390
|
+
/// Recipient address.
|
|
391
|
+
to: EvmAddress,
|
|
392
|
+
/// Transfer amount in wei.
|
|
393
|
+
#[serde(with = "u128_as_decimal_string")]
|
|
394
|
+
amount_wei: u128,
|
|
395
|
+
},
|
|
396
|
+
/// Permit2 `PermitSingle` approval.
|
|
397
|
+
Permit2Permit {
|
|
398
|
+
/// Typed authorization payload.
|
|
399
|
+
permit: Permit2Permit,
|
|
400
|
+
},
|
|
401
|
+
/// EIP-3009 `transferWithAuthorization`.
|
|
402
|
+
Eip3009TransferWithAuthorization {
|
|
403
|
+
/// Typed authorization payload.
|
|
404
|
+
authorization: Eip3009Transfer,
|
|
405
|
+
},
|
|
406
|
+
/// EIP-3009 `receiveWithAuthorization`.
|
|
407
|
+
Eip3009ReceiveWithAuthorization {
|
|
408
|
+
/// Typed authorization payload.
|
|
409
|
+
authorization: Eip3009Transfer,
|
|
410
|
+
},
|
|
411
|
+
/// Raw transaction broadcast request.
|
|
412
|
+
BroadcastTx {
|
|
413
|
+
/// Unsinged tx fields to authorize and sign.
|
|
414
|
+
tx: BroadcastTx,
|
|
415
|
+
},
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
impl AgentAction {
|
|
419
|
+
/// Returns action amount in wei.
|
|
420
|
+
#[must_use]
|
|
421
|
+
pub fn amount_wei(&self) -> u128 {
|
|
422
|
+
match self {
|
|
423
|
+
Self::Approve { amount_wei, .. }
|
|
424
|
+
| Self::Transfer { amount_wei, .. }
|
|
425
|
+
| Self::TransferNative { amount_wei, .. } => *amount_wei,
|
|
426
|
+
Self::Permit2Permit { permit } => permit.amount_wei,
|
|
427
|
+
Self::Eip3009TransferWithAuthorization { authorization }
|
|
428
|
+
| Self::Eip3009ReceiveWithAuthorization { authorization } => authorization.amount_wei,
|
|
429
|
+
Self::BroadcastTx { tx } => self.broadcast_effective_amount_wei(tx),
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/// Returns action chain id.
|
|
434
|
+
#[must_use]
|
|
435
|
+
pub fn chain_id(&self) -> u64 {
|
|
436
|
+
match self {
|
|
437
|
+
Self::Approve { chain_id, .. }
|
|
438
|
+
| Self::Transfer { chain_id, .. }
|
|
439
|
+
| Self::TransferNative { chain_id, .. } => *chain_id,
|
|
440
|
+
Self::Permit2Permit { permit } => permit.chain_id,
|
|
441
|
+
Self::Eip3009TransferWithAuthorization { authorization }
|
|
442
|
+
| Self::Eip3009ReceiveWithAuthorization { authorization } => authorization.chain_id,
|
|
443
|
+
Self::BroadcastTx { tx } => tx.chain_id,
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/// Returns action asset id.
|
|
448
|
+
#[must_use]
|
|
449
|
+
pub fn asset(&self) -> AssetId {
|
|
450
|
+
match self {
|
|
451
|
+
Self::Approve { token, .. } | Self::Transfer { token, .. } => {
|
|
452
|
+
AssetId::Erc20(token.clone())
|
|
453
|
+
}
|
|
454
|
+
Self::TransferNative { .. } => AssetId::NativeEth,
|
|
455
|
+
Self::Permit2Permit { permit } => AssetId::Erc20(permit.token.clone()),
|
|
456
|
+
Self::Eip3009TransferWithAuthorization { authorization }
|
|
457
|
+
| Self::Eip3009ReceiveWithAuthorization { authorization } => {
|
|
458
|
+
AssetId::Erc20(authorization.token.clone())
|
|
459
|
+
}
|
|
460
|
+
Self::BroadcastTx { tx } => self.broadcast_effective_asset(tx),
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/// Returns recipient/spender address used for policy scope matching.
|
|
465
|
+
#[must_use]
|
|
466
|
+
pub fn recipient(&self) -> EvmAddress {
|
|
467
|
+
match self {
|
|
468
|
+
Self::Approve { spender, .. } => spender.clone(),
|
|
469
|
+
Self::Transfer { to, .. } | Self::TransferNative { to, .. } => to.clone(),
|
|
470
|
+
Self::Permit2Permit { permit } => permit.spender.clone(),
|
|
471
|
+
Self::Eip3009TransferWithAuthorization { authorization }
|
|
472
|
+
| Self::Eip3009ReceiveWithAuthorization { authorization } => authorization.to.clone(),
|
|
473
|
+
Self::BroadcastTx { tx } => self.broadcast_effective_recipient(tx),
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/// Returns optional max gas spend in wei for actions that contain tx gas metadata.
|
|
478
|
+
#[must_use]
|
|
479
|
+
pub fn max_gas_spend_wei(&self) -> Option<u128> {
|
|
480
|
+
match self {
|
|
481
|
+
Self::BroadcastTx { tx } => tx.max_gas_spend_wei().ok(),
|
|
482
|
+
_ => None,
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/// Returns optional transaction max-fee-per-gas in wei.
|
|
487
|
+
#[must_use]
|
|
488
|
+
pub fn max_fee_per_gas_wei(&self) -> Option<u128> {
|
|
489
|
+
match self {
|
|
490
|
+
Self::BroadcastTx { tx } => Some(tx.max_fee_per_gas_wei),
|
|
491
|
+
_ => None,
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/// Returns optional transaction priority-fee-per-gas in wei.
|
|
496
|
+
#[must_use]
|
|
497
|
+
pub fn max_priority_fee_per_gas_wei(&self) -> Option<u128> {
|
|
498
|
+
match self {
|
|
499
|
+
Self::BroadcastTx { tx } => Some(tx.max_priority_fee_per_gas_wei),
|
|
500
|
+
_ => None,
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/// Returns optional calldata length in bytes.
|
|
505
|
+
#[must_use]
|
|
506
|
+
pub fn calldata_len_bytes(&self) -> Option<usize> {
|
|
507
|
+
match self {
|
|
508
|
+
Self::BroadcastTx { tx } => tx.data_bytes().ok().map(|data| data.len()),
|
|
509
|
+
_ => None,
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/// Returns the EIP-712 signing digest for typed-data actions.
|
|
514
|
+
pub fn signing_hash(&self) -> Result<Option<[u8; 32]>, DomainError> {
|
|
515
|
+
match self {
|
|
516
|
+
Self::Permit2Permit { permit } => permit.signing_hash().map(Some),
|
|
517
|
+
Self::Eip3009TransferWithAuthorization { authorization } => {
|
|
518
|
+
authorization.transfer_signing_hash().map(Some)
|
|
519
|
+
}
|
|
520
|
+
Self::Eip3009ReceiveWithAuthorization { authorization } => {
|
|
521
|
+
authorization.receive_signing_hash().map(Some)
|
|
522
|
+
}
|
|
523
|
+
_ => Ok(None),
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/// Returns whether action has valid fundamental invariants.
|
|
528
|
+
pub fn validate(&self) -> Result<(), DomainError> {
|
|
529
|
+
match self {
|
|
530
|
+
Self::BroadcastTx { tx } => tx.validate(),
|
|
531
|
+
Self::Permit2Permit { permit } => permit.validate(),
|
|
532
|
+
Self::Eip3009TransferWithAuthorization { authorization }
|
|
533
|
+
| Self::Eip3009ReceiveWithAuthorization { authorization } => authorization.validate(),
|
|
534
|
+
_ => {
|
|
535
|
+
if self.amount_wei() == 0 {
|
|
536
|
+
return Err(DomainError::InvalidAmount);
|
|
537
|
+
}
|
|
538
|
+
if self.chain_id() == 0 {
|
|
539
|
+
return Err(DomainError::InvalidChainId);
|
|
540
|
+
}
|
|
541
|
+
Ok(())
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
fn broadcast_effective_asset(&self, tx: &BroadcastTx) -> AssetId {
|
|
547
|
+
if let Some(projection) = self.broadcast_policy_projection(tx) {
|
|
548
|
+
return projection.asset;
|
|
549
|
+
}
|
|
550
|
+
AssetId::NativeEth
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
fn broadcast_effective_recipient(&self, tx: &BroadcastTx) -> EvmAddress {
|
|
554
|
+
if let Some(projection) = self.broadcast_policy_projection(tx) {
|
|
555
|
+
return projection.recipient;
|
|
556
|
+
}
|
|
557
|
+
tx.to.clone()
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
fn broadcast_effective_amount_wei(&self, tx: &BroadcastTx) -> u128 {
|
|
561
|
+
if let Some(projection) = self.broadcast_policy_projection(tx) {
|
|
562
|
+
return projection.amount_wei;
|
|
563
|
+
}
|
|
564
|
+
tx.value_wei
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
fn broadcast_policy_projection(&self, tx: &BroadcastTx) -> Option<BroadcastPolicyProjection> {
|
|
568
|
+
let data = tx.data_bytes().ok()?;
|
|
569
|
+
parse_broadcast_policy_call(tx, &data).ok()
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
574
|
+
struct BroadcastPolicyProjection {
|
|
575
|
+
asset: AssetId,
|
|
576
|
+
recipient: EvmAddress,
|
|
577
|
+
amount_wei: u128,
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/// Parses ERC-20 call data and returns a strongly-typed call description.
|
|
581
|
+
///
|
|
582
|
+
/// Supported methods:
|
|
583
|
+
/// - `approve(address spender, uint256 value)`
|
|
584
|
+
/// - `transfer(address to, uint256 value)`
|
|
585
|
+
pub fn parse_erc20_call(calldata: &[u8]) -> Result<Erc20Call, DomainError> {
|
|
586
|
+
if calldata.len() < 4 {
|
|
587
|
+
return Err(DomainError::InvalidErc20Calldata(
|
|
588
|
+
"missing 4-byte selector".to_string(),
|
|
589
|
+
));
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
let selector = &calldata[..4];
|
|
593
|
+
if selector == approveCall::SELECTOR {
|
|
594
|
+
let decoded = approveCall::abi_decode(calldata, true)
|
|
595
|
+
.map_err(|err| DomainError::InvalidErc20Calldata(err.to_string()))?;
|
|
596
|
+
return Ok(Erc20Call::Approve {
|
|
597
|
+
spender: alloy_address_to_evm(decoded.spender)?,
|
|
598
|
+
amount_wei: u256_to_u128(decoded.value)?,
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if selector == transferCall::SELECTOR {
|
|
603
|
+
let decoded = transferCall::abi_decode(calldata, true)
|
|
604
|
+
.map_err(|err| DomainError::InvalidErc20Calldata(err.to_string()))?;
|
|
605
|
+
return Ok(Erc20Call::Transfer {
|
|
606
|
+
to: alloy_address_to_evm(decoded.to)?,
|
|
607
|
+
amount_wei: u256_to_u128(decoded.value)?,
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
Err(DomainError::InvalidErc20Calldata(format!(
|
|
612
|
+
"unsupported selector 0x{}",
|
|
613
|
+
hex::encode(selector)
|
|
614
|
+
)))
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/// Constructs an [`AgentAction`] from ERC-20 calldata for a given contract and network.
|
|
618
|
+
pub fn action_from_erc20_calldata(
|
|
619
|
+
chain_id: u64,
|
|
620
|
+
token: EvmAddress,
|
|
621
|
+
calldata: &[u8],
|
|
622
|
+
) -> Result<AgentAction, DomainError> {
|
|
623
|
+
if chain_id == 0 {
|
|
624
|
+
return Err(DomainError::InvalidChainId);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
match parse_erc20_call(calldata)? {
|
|
628
|
+
Erc20Call::Approve {
|
|
629
|
+
spender,
|
|
630
|
+
amount_wei,
|
|
631
|
+
} => Ok(AgentAction::Approve {
|
|
632
|
+
chain_id,
|
|
633
|
+
token,
|
|
634
|
+
spender,
|
|
635
|
+
amount_wei,
|
|
636
|
+
}),
|
|
637
|
+
Erc20Call::Transfer { to, amount_wei } => Ok(AgentAction::Transfer {
|
|
638
|
+
chain_id,
|
|
639
|
+
token,
|
|
640
|
+
to,
|
|
641
|
+
amount_wei,
|
|
642
|
+
}),
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
fn parse_broadcast_policy_call(
|
|
647
|
+
tx: &BroadcastTx,
|
|
648
|
+
calldata: &[u8],
|
|
649
|
+
) -> Result<BroadcastPolicyProjection, DomainError> {
|
|
650
|
+
if let Ok(call) = parse_erc20_call(calldata) {
|
|
651
|
+
let asset = AssetId::Erc20(tx.to.clone());
|
|
652
|
+
return Ok(match call {
|
|
653
|
+
Erc20Call::Approve {
|
|
654
|
+
spender,
|
|
655
|
+
amount_wei,
|
|
656
|
+
} => BroadcastPolicyProjection {
|
|
657
|
+
asset,
|
|
658
|
+
recipient: spender,
|
|
659
|
+
amount_wei,
|
|
660
|
+
},
|
|
661
|
+
Erc20Call::Transfer { to, amount_wei } => BroadcastPolicyProjection {
|
|
662
|
+
asset,
|
|
663
|
+
recipient: to,
|
|
664
|
+
amount_wei,
|
|
665
|
+
},
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if calldata.len() >= 4 {
|
|
670
|
+
if let Ok(decoded) = permitCall::abi_decode(calldata, true) {
|
|
671
|
+
return Ok(BroadcastPolicyProjection {
|
|
672
|
+
asset: AssetId::Erc20(alloy_address_to_evm(decoded.permitSingle.details.token)?),
|
|
673
|
+
recipient: alloy_address_to_evm(decoded.permitSingle.spender)?,
|
|
674
|
+
amount_wei: u160_to_u128(decoded.permitSingle.details.amount)?,
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
if let Ok(decoded) = transferWithAuthorizationCall::abi_decode(calldata, true) {
|
|
679
|
+
return Ok(BroadcastPolicyProjection {
|
|
680
|
+
asset: AssetId::Erc20(tx.to.clone()),
|
|
681
|
+
recipient: alloy_address_to_evm(decoded.to)?,
|
|
682
|
+
amount_wei: u256_to_u128(decoded.value)?,
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if let Ok(decoded) = receiveWithAuthorizationCall::abi_decode(calldata, true) {
|
|
687
|
+
return Ok(BroadcastPolicyProjection {
|
|
688
|
+
asset: AssetId::Erc20(tx.to.clone()),
|
|
689
|
+
recipient: alloy_address_to_evm(decoded.to)?,
|
|
690
|
+
amount_wei: u256_to_u128(decoded.value)?,
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
Err(DomainError::InvalidErc20Calldata(
|
|
696
|
+
"unsupported broadcast policy projection".to_string(),
|
|
697
|
+
))
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
fn evm_to_alloy_address(address: &EvmAddress) -> Address {
|
|
701
|
+
Address::parse_checksummed(address.as_str(), None)
|
|
702
|
+
.or_else(|_| address.as_str().parse())
|
|
703
|
+
.expect("validated evm address should always parse as alloy address")
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
fn alloy_address_to_evm(address: alloy_primitives::Address) -> Result<EvmAddress, DomainError> {
|
|
707
|
+
let value = format!("0x{}", hex::encode(address.as_slice()));
|
|
708
|
+
value.parse::<EvmAddress>()
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
fn decode_hex_payload(input: &str) -> Result<Vec<u8>, DomainError> {
|
|
712
|
+
let trimmed = input.trim();
|
|
713
|
+
let payload = trimmed.strip_prefix("0x").unwrap_or(trimmed);
|
|
714
|
+
if payload.is_empty() {
|
|
715
|
+
return Ok(Vec::new());
|
|
716
|
+
}
|
|
717
|
+
if !payload.len().is_multiple_of(2) || !payload.chars().all(|ch| ch.is_ascii_hexdigit()) {
|
|
718
|
+
return Err(DomainError::InvalidTransactionDataHex);
|
|
719
|
+
}
|
|
720
|
+
hex::decode(payload).map_err(|_| DomainError::InvalidTransactionDataHex)
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
fn decode_hex_32(input: &str, label: &str) -> Result<[u8; 32], DomainError> {
|
|
724
|
+
let bytes = decode_hex_payload(input).map_err(|_| {
|
|
725
|
+
DomainError::InvalidTypedDataDomain(format!("{label} must be a 32-byte hex value"))
|
|
726
|
+
})?;
|
|
727
|
+
if bytes.len() != 32 {
|
|
728
|
+
return Err(DomainError::InvalidTypedDataDomain(format!(
|
|
729
|
+
"{label} must be exactly 32 bytes"
|
|
730
|
+
)));
|
|
731
|
+
}
|
|
732
|
+
let mut out = [0u8; 32];
|
|
733
|
+
out.copy_from_slice(&bytes);
|
|
734
|
+
Ok(out)
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
fn u256_to_u128(value: U256) -> Result<u128, DomainError> {
|
|
738
|
+
if value > U256::from(u128::MAX) {
|
|
739
|
+
return Err(DomainError::AmountOutOfRange);
|
|
740
|
+
}
|
|
741
|
+
Ok(value.to::<u128>())
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
fn u160_to_u128(value: U160) -> Result<u128, DomainError> {
|
|
745
|
+
let bytes = value.to_be_bytes::<20>();
|
|
746
|
+
if bytes[..4].iter().any(|byte| *byte != 0) {
|
|
747
|
+
return Err(DomainError::AmountOutOfRange);
|
|
748
|
+
}
|
|
749
|
+
Ok(u128::from_be_bytes(
|
|
750
|
+
bytes[4..].try_into().expect("16-byte slice"),
|
|
751
|
+
))
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
fn u128_to_u160(value: u128) -> U160 {
|
|
755
|
+
U160::from_be_slice(&value.to_be_bytes())
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
fn u64_to_u48(value: u64) -> U48 {
|
|
759
|
+
U48::from_be_slice(&value.to_be_bytes()[2..])
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
fn permit2_max_timestamp() -> u64 {
|
|
763
|
+
(1u64 << 48) - 1
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
fn rlp_encode_u64(value: u64) -> Vec<u8> {
|
|
767
|
+
if value == 0 {
|
|
768
|
+
return rlp_encode_bytes(&[]);
|
|
769
|
+
}
|
|
770
|
+
let mut bytes = value.to_be_bytes().to_vec();
|
|
771
|
+
let first_non_zero = bytes
|
|
772
|
+
.iter()
|
|
773
|
+
.position(|byte| *byte != 0)
|
|
774
|
+
.unwrap_or(bytes.len());
|
|
775
|
+
bytes.drain(..first_non_zero);
|
|
776
|
+
rlp_encode_bytes(&bytes)
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
fn rlp_encode_u128(value: u128) -> Vec<u8> {
|
|
780
|
+
if value == 0 {
|
|
781
|
+
return rlp_encode_bytes(&[]);
|
|
782
|
+
}
|
|
783
|
+
let mut bytes = value.to_be_bytes().to_vec();
|
|
784
|
+
let first_non_zero = bytes
|
|
785
|
+
.iter()
|
|
786
|
+
.position(|byte| *byte != 0)
|
|
787
|
+
.unwrap_or(bytes.len());
|
|
788
|
+
bytes.drain(..first_non_zero);
|
|
789
|
+
rlp_encode_bytes(&bytes)
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
fn rlp_encode_u256_bytes(value: &[u8; 32]) -> Vec<u8> {
|
|
793
|
+
let first_non_zero = value
|
|
794
|
+
.iter()
|
|
795
|
+
.position(|byte| *byte != 0)
|
|
796
|
+
.unwrap_or(value.len());
|
|
797
|
+
rlp_encode_bytes(&value[first_non_zero..])
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
fn rlp_encode_bytes(value: &[u8]) -> Vec<u8> {
|
|
801
|
+
if value.len() == 1 && value[0] < 0x80 {
|
|
802
|
+
return vec![value[0]];
|
|
803
|
+
}
|
|
804
|
+
if value.len() <= 55 {
|
|
805
|
+
let mut out = Vec::with_capacity(1 + value.len());
|
|
806
|
+
out.push(0x80 + value.len() as u8);
|
|
807
|
+
out.extend(value);
|
|
808
|
+
return out;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
let len_bytes = usize_to_be_bytes_no_leading_zero(value.len());
|
|
812
|
+
let mut out = Vec::with_capacity(1 + len_bytes.len() + value.len());
|
|
813
|
+
out.push(0xb7 + len_bytes.len() as u8);
|
|
814
|
+
out.extend(len_bytes);
|
|
815
|
+
out.extend(value);
|
|
816
|
+
out
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
fn rlp_encode_list(items: &[Vec<u8>]) -> Vec<u8> {
|
|
820
|
+
let payload_len: usize = items.iter().map(Vec::len).sum();
|
|
821
|
+
let mut payload: Vec<u8> = Vec::with_capacity(payload_len);
|
|
822
|
+
for item in items {
|
|
823
|
+
payload.extend_from_slice(item);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if payload.len() <= 55 {
|
|
827
|
+
let mut out = Vec::with_capacity(1 + payload.len());
|
|
828
|
+
out.push(0xc0 + payload.len() as u8);
|
|
829
|
+
out.extend(payload);
|
|
830
|
+
return out;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
let len_bytes = usize_to_be_bytes_no_leading_zero(payload.len());
|
|
834
|
+
let mut out = Vec::with_capacity(1 + len_bytes.len() + payload.len());
|
|
835
|
+
out.push(0xf7 + len_bytes.len() as u8);
|
|
836
|
+
out.extend(len_bytes);
|
|
837
|
+
out.extend(payload);
|
|
838
|
+
out
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
fn usize_to_be_bytes_no_leading_zero(value: usize) -> Vec<u8> {
|
|
842
|
+
let mut bytes = value.to_be_bytes().to_vec();
|
|
843
|
+
let first_non_zero = bytes
|
|
844
|
+
.iter()
|
|
845
|
+
.position(|byte| *byte != 0)
|
|
846
|
+
.unwrap_or(bytes.len());
|
|
847
|
+
bytes.drain(..first_non_zero);
|
|
848
|
+
bytes
|
|
849
|
+
}
|