@wlfi-agent/cli 1.4.16 → 1.4.18
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 +26 -20
- package/Cargo.toml +1 -1
- package/README.md +61 -28
- package/crates/vault-cli-admin/src/io_utils.rs +149 -1
- package/crates/vault-cli-admin/src/main.rs +639 -16
- package/crates/vault-cli-admin/src/shared_config.rs +18 -18
- package/crates/vault-cli-admin/src/tui/token_rpc.rs +190 -3
- package/crates/vault-cli-admin/src/tui/utils.rs +59 -0
- package/crates/vault-cli-admin/src/tui.rs +1205 -120
- package/crates/vault-cli-agent/Cargo.toml +1 -0
- package/crates/vault-cli-agent/src/io_utils.rs +163 -2
- package/crates/vault-cli-agent/src/main.rs +648 -32
- package/crates/vault-cli-daemon/Cargo.toml +4 -0
- package/crates/vault-cli-daemon/src/main.rs +617 -67
- package/crates/vault-cli-daemon/src/relay_sync.rs +776 -4
- package/crates/vault-cli-daemon/tests/system_keychain_helper_acl.rs +5 -0
- package/crates/vault-daemon/src/daemon_parts/api_impl_and_utils.rs +32 -1
- package/crates/vault-daemon/src/persistence.rs +637 -100
- package/crates/vault-daemon/src/tests.rs +1013 -3
- package/crates/vault-daemon/src/tests_parts/part2.rs +99 -0
- package/crates/vault-daemon/src/tests_parts/part4.rs +11 -7
- package/crates/vault-domain/src/nonce.rs +4 -0
- package/crates/vault-domain/src/tests.rs +616 -0
- package/crates/vault-policy/src/engine.rs +55 -32
- package/crates/vault-policy/src/tests.rs +195 -0
- package/crates/vault-sdk-agent/src/lib.rs +415 -22
- package/crates/vault-signer/Cargo.toml +3 -0
- package/crates/vault-signer/src/lib.rs +266 -40
- package/crates/vault-transport-unix/src/lib.rs +653 -5
- package/crates/vault-transport-xpc/src/tests.rs +531 -3
- package/crates/vault-transport-xpc/tests/e2e_flow.rs +3 -0
- package/dist/cli.cjs +663 -190
- package/dist/cli.cjs.map +1 -1
- package/package.json +5 -2
- package/packages/cache/.turbo/turbo-build.log +53 -52
- package/packages/cache/coverage/clover.xml +529 -394
- package/packages/cache/coverage/coverage-final.json +2 -2
- package/packages/cache/coverage/index.html +21 -21
- package/packages/cache/coverage/src/client/index.html +1 -1
- package/packages/cache/coverage/src/client/index.ts.html +1 -1
- package/packages/cache/coverage/src/errors/index.html +1 -1
- package/packages/cache/coverage/src/errors/index.ts.html +12 -12
- package/packages/cache/coverage/src/index.html +1 -1
- package/packages/cache/coverage/src/index.ts.html +1 -1
- package/packages/cache/coverage/src/service/index.html +21 -21
- package/packages/cache/coverage/src/service/index.ts.html +769 -313
- package/packages/cache/dist/{chunk-QNK6GOTI.js → chunk-KC53LH5Z.js} +35 -2
- package/packages/cache/dist/chunk-KC53LH5Z.js.map +1 -0
- package/packages/cache/dist/{chunk-QF4XKEIA.cjs → chunk-UVU7VFE3.cjs} +35 -2
- package/packages/cache/dist/chunk-UVU7VFE3.cjs.map +1 -0
- package/packages/cache/dist/index.cjs +2 -2
- package/packages/cache/dist/index.js +1 -1
- package/packages/cache/dist/service/index.cjs +2 -2
- package/packages/cache/dist/service/index.js +1 -1
- package/packages/cache/node_modules/.bin/tsc +2 -2
- package/packages/cache/node_modules/.bin/tsserver +2 -2
- package/packages/cache/node_modules/.bin/tsup +2 -2
- package/packages/cache/node_modules/.bin/tsup-node +2 -2
- package/packages/cache/node_modules/.bin/vitest +4 -4
- package/packages/cache/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/packages/cache/src/service/index.test.ts +165 -19
- package/packages/cache/src/service/index.ts +38 -1
- package/packages/config/.turbo/turbo-build.log +18 -17
- package/packages/config/dist/index.cjs +0 -17
- package/packages/config/dist/index.cjs.map +1 -1
- package/packages/config/src/index.ts +0 -17
- package/packages/rpc/.turbo/turbo-build.log +32 -31
- package/packages/rpc/dist/index.cjs +0 -17
- package/packages/rpc/dist/index.cjs.map +1 -1
- package/packages/rpc/src/index.js +1 -0
- package/packages/ui/.turbo/turbo-build.log +44 -43
- package/packages/ui/dist/components/badge.d.ts +1 -1
- package/packages/ui/dist/components/button.d.ts +1 -1
- package/packages/ui/node_modules/.bin/tsc +2 -2
- package/packages/ui/node_modules/.bin/tsserver +2 -2
- package/packages/ui/node_modules/.bin/tsup +2 -2
- package/packages/ui/node_modules/.bin/tsup-node +2 -2
- package/scripts/install-cli-launcher.mjs +37 -0
- package/scripts/install-rust-binaries.mjs +112 -0
- package/scripts/run-tests-isolated.mjs +210 -0
- package/src/cli.ts +310 -50
- package/src/lib/admin-reset.ts +15 -30
- package/src/lib/admin-setup.ts +246 -55
- package/src/lib/agent-auth-migrate.ts +5 -1
- package/src/lib/asset-broadcast.ts +15 -4
- package/src/lib/config-amounts.ts +6 -4
- package/src/lib/hidden-tty-prompt.js +1 -0
- package/src/lib/hidden-tty-prompt.ts +105 -0
- package/src/lib/keychain.ts +1 -0
- package/src/lib/local-admin-access.ts +4 -29
- package/src/lib/rust.ts +129 -33
- package/src/lib/signed-tx.ts +1 -0
- package/src/lib/sudo.ts +15 -5
- package/src/lib/wallet-profile.ts +3 -0
- package/src/lib/wallet-setup.ts +52 -0
- package/packages/cache/dist/chunk-QF4XKEIA.cjs.map +0 -1
- package/packages/cache/dist/chunk-QNK6GOTI.js.map +0 -1
|
@@ -89,7 +89,7 @@ pub trait AgentOperations: Send + Sync {
|
|
|
89
89
|
async fn reserve_broadcast_nonce(
|
|
90
90
|
&self,
|
|
91
91
|
chain_id: u64,
|
|
92
|
-
|
|
92
|
+
nonce: u64,
|
|
93
93
|
) -> Result<NonceReservation, AgentSdkError>;
|
|
94
94
|
|
|
95
95
|
/// Explicitly releases an unused nonce reservation.
|
|
@@ -244,7 +244,21 @@ where
|
|
|
244
244
|
}
|
|
245
245
|
|
|
246
246
|
async fn broadcast_tx(&self, mut tx: BroadcastTx) -> Result<Signature, AgentSdkError> {
|
|
247
|
-
let
|
|
247
|
+
let requested_nonce = tx.nonce;
|
|
248
|
+
let reservation = self
|
|
249
|
+
.reserve_broadcast_nonce(tx.chain_id, requested_nonce)
|
|
250
|
+
.await?;
|
|
251
|
+
if reservation.nonce != requested_nonce {
|
|
252
|
+
let _ = self
|
|
253
|
+
.release_broadcast_nonce(reservation.reservation_id)
|
|
254
|
+
.await;
|
|
255
|
+
return Err(AgentSdkError::Daemon(DaemonError::InvalidNonceReservation(
|
|
256
|
+
format!(
|
|
257
|
+
"requested exact nonce {requested_nonce} for chain_id {} but daemon reserved {}",
|
|
258
|
+
tx.chain_id, reservation.nonce
|
|
259
|
+
),
|
|
260
|
+
)));
|
|
261
|
+
}
|
|
248
262
|
tx.nonce = reservation.nonce;
|
|
249
263
|
|
|
250
264
|
match self.sign_action(AgentAction::BroadcastTx { tx }).await {
|
|
@@ -261,7 +275,7 @@ where
|
|
|
261
275
|
async fn reserve_broadcast_nonce(
|
|
262
276
|
&self,
|
|
263
277
|
chain_id: u64,
|
|
264
|
-
|
|
278
|
+
nonce: u64,
|
|
265
279
|
) -> Result<NonceReservation, AgentSdkError> {
|
|
266
280
|
let now = OffsetDateTime::now_utc();
|
|
267
281
|
let request = NonceReservationRequest {
|
|
@@ -269,7 +283,8 @@ where
|
|
|
269
283
|
agent_key_id: self.agent_key_id,
|
|
270
284
|
agent_auth_token: self.agent_auth_token.to_string(),
|
|
271
285
|
chain_id,
|
|
272
|
-
min_nonce,
|
|
286
|
+
min_nonce: nonce,
|
|
287
|
+
exact_nonce: true,
|
|
273
288
|
requested_at: now,
|
|
274
289
|
expires_at: now + Self::REQUEST_TTL,
|
|
275
290
|
};
|
|
@@ -302,10 +317,10 @@ mod tests {
|
|
|
302
317
|
use uuid::Uuid;
|
|
303
318
|
use vault_daemon::{DaemonError, KeyManagerDaemonApi};
|
|
304
319
|
use vault_domain::{
|
|
305
|
-
AdminSession, AgentAction, AgentCredentials, AgentKey, BroadcastTx,
|
|
306
|
-
ManualApprovalDecision, ManualApprovalRequest, NonceReleaseRequest,
|
|
307
|
-
NonceReservationRequest, PolicyAttachment, RelayConfig, SignRequest,
|
|
308
|
-
SpendingPolicy, VaultKey,
|
|
320
|
+
AdminSession, AgentAction, AgentCredentials, AgentKey, BroadcastTx, EntityScope,
|
|
321
|
+
EvmAddress, Lease, ManualApprovalDecision, ManualApprovalRequest, NonceReleaseRequest,
|
|
322
|
+
NonceReservation, NonceReservationRequest, PolicyAttachment, RelayConfig, SignRequest,
|
|
323
|
+
Signature, SpendingPolicy, VaultKey, DomainError,
|
|
309
324
|
};
|
|
310
325
|
use vault_policy::PolicyEvaluation;
|
|
311
326
|
use vault_signer::KeyCreateRequest;
|
|
@@ -315,6 +330,8 @@ mod tests {
|
|
|
315
330
|
#[derive(Default)]
|
|
316
331
|
pub(super) struct RecordingDaemon {
|
|
317
332
|
pub(super) request: Mutex<Option<SignRequest>>,
|
|
333
|
+
pub(super) nonce_request: Mutex<Option<NonceReservationRequest>>,
|
|
334
|
+
pub(super) released_reservation: Mutex<Option<Uuid>>,
|
|
318
335
|
fail_sign: bool,
|
|
319
336
|
reserved_nonce_override: Option<u64>,
|
|
320
337
|
}
|
|
@@ -440,6 +457,7 @@ mod tests {
|
|
|
440
457
|
&self,
|
|
441
458
|
request: NonceReservationRequest,
|
|
442
459
|
) -> Result<NonceReservation, DaemonError> {
|
|
460
|
+
*self.nonce_request.lock().expect("lock") = Some(request.clone());
|
|
443
461
|
Ok(NonceReservation {
|
|
444
462
|
reservation_id: Uuid::new_v4(),
|
|
445
463
|
agent_key_id: request.agent_key_id,
|
|
@@ -451,7 +469,8 @@ mod tests {
|
|
|
451
469
|
})
|
|
452
470
|
}
|
|
453
471
|
|
|
454
|
-
async fn release_nonce(&self,
|
|
472
|
+
async fn release_nonce(&self, request: NonceReleaseRequest) -> Result<(), DaemonError> {
|
|
473
|
+
*self.released_reservation.lock().expect("lock") = Some(request.reservation_id);
|
|
455
474
|
Ok(())
|
|
456
475
|
}
|
|
457
476
|
|
|
@@ -476,6 +495,178 @@ mod tests {
|
|
|
476
495
|
}
|
|
477
496
|
}
|
|
478
497
|
|
|
498
|
+
fn sample_admin_session() -> AdminSession {
|
|
499
|
+
AdminSession {
|
|
500
|
+
vault_password: "vault-password".to_string(),
|
|
501
|
+
lease: Lease {
|
|
502
|
+
lease_id: Uuid::new_v4(),
|
|
503
|
+
issued_at: OffsetDateTime::UNIX_EPOCH,
|
|
504
|
+
expires_at: OffsetDateTime::UNIX_EPOCH + time::Duration::minutes(5),
|
|
505
|
+
},
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
fn sample_policy() -> SpendingPolicy {
|
|
510
|
+
SpendingPolicy::new(
|
|
511
|
+
0,
|
|
512
|
+
vault_domain::PolicyType::PerTxMaxSpending,
|
|
513
|
+
100,
|
|
514
|
+
EntityScope::All,
|
|
515
|
+
EntityScope::All,
|
|
516
|
+
EntityScope::All,
|
|
517
|
+
)
|
|
518
|
+
.expect("policy")
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
fn sample_sign_request() -> SignRequest {
|
|
522
|
+
SignRequest {
|
|
523
|
+
request_id: Uuid::new_v4(),
|
|
524
|
+
agent_key_id: Uuid::new_v4(),
|
|
525
|
+
agent_auth_token: "agent-secret-token".to_string(),
|
|
526
|
+
payload: br#"{"kind":"transfer_native"}"#.to_vec(),
|
|
527
|
+
action: AgentAction::TransferNative {
|
|
528
|
+
chain_id: 1,
|
|
529
|
+
to: "0x1111111111111111111111111111111111111111"
|
|
530
|
+
.parse()
|
|
531
|
+
.expect("to"),
|
|
532
|
+
amount_wei: 1,
|
|
533
|
+
},
|
|
534
|
+
requested_at: OffsetDateTime::UNIX_EPOCH,
|
|
535
|
+
expires_at: OffsetDateTime::UNIX_EPOCH + time::Duration::minutes(2),
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
fn sample_nonce_release_request() -> NonceReleaseRequest {
|
|
540
|
+
NonceReleaseRequest {
|
|
541
|
+
request_id: Uuid::new_v4(),
|
|
542
|
+
agent_key_id: Uuid::new_v4(),
|
|
543
|
+
agent_auth_token: "agent-secret-token".to_string(),
|
|
544
|
+
reservation_id: Uuid::new_v4(),
|
|
545
|
+
requested_at: OffsetDateTime::UNIX_EPOCH,
|
|
546
|
+
expires_at: OffsetDateTime::UNIX_EPOCH + time::Duration::minutes(2),
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
fn sample_nonce_reservation_request() -> NonceReservationRequest {
|
|
551
|
+
NonceReservationRequest {
|
|
552
|
+
request_id: Uuid::new_v4(),
|
|
553
|
+
agent_key_id: Uuid::new_v4(),
|
|
554
|
+
agent_auth_token: "agent-secret-token".to_string(),
|
|
555
|
+
chain_id: 1,
|
|
556
|
+
min_nonce: 7,
|
|
557
|
+
exact_nonce: false,
|
|
558
|
+
requested_at: OffsetDateTime::UNIX_EPOCH,
|
|
559
|
+
expires_at: OffsetDateTime::UNIX_EPOCH + time::Duration::minutes(2),
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
#[tokio::test]
|
|
564
|
+
async fn recording_daemon_not_used_paths_return_transport_errors() {
|
|
565
|
+
let daemon = RecordingDaemon::default();
|
|
566
|
+
let session = sample_admin_session();
|
|
567
|
+
let policy = sample_policy();
|
|
568
|
+
let request = sample_sign_request();
|
|
569
|
+
let reservation_request = sample_nonce_reservation_request();
|
|
570
|
+
let release_request = sample_nonce_release_request();
|
|
571
|
+
|
|
572
|
+
assert!(matches!(
|
|
573
|
+
daemon.issue_lease("pw").await,
|
|
574
|
+
Err(DaemonError::Transport(message)) if message == "not used"
|
|
575
|
+
));
|
|
576
|
+
assert!(matches!(
|
|
577
|
+
daemon.add_policy(&session, policy.clone()).await,
|
|
578
|
+
Err(DaemonError::Transport(message)) if message == "not used"
|
|
579
|
+
));
|
|
580
|
+
assert!(matches!(
|
|
581
|
+
daemon.list_policies(&session).await,
|
|
582
|
+
Err(DaemonError::Transport(message)) if message == "not used"
|
|
583
|
+
));
|
|
584
|
+
assert!(matches!(
|
|
585
|
+
daemon.disable_policy(&session, Uuid::new_v4()).await,
|
|
586
|
+
Err(DaemonError::Transport(message)) if message == "not used"
|
|
587
|
+
));
|
|
588
|
+
assert!(matches!(
|
|
589
|
+
daemon
|
|
590
|
+
.create_vault_key(&session, KeyCreateRequest::Generate)
|
|
591
|
+
.await,
|
|
592
|
+
Err(DaemonError::Transport(message)) if message == "not used"
|
|
593
|
+
));
|
|
594
|
+
assert!(matches!(
|
|
595
|
+
daemon
|
|
596
|
+
.export_vault_private_key(&session, Uuid::new_v4())
|
|
597
|
+
.await,
|
|
598
|
+
Err(DaemonError::Transport(message)) if message == "not used"
|
|
599
|
+
));
|
|
600
|
+
assert!(matches!(
|
|
601
|
+
daemon
|
|
602
|
+
.create_agent_key(&session, Uuid::new_v4(), PolicyAttachment::AllPolicies)
|
|
603
|
+
.await,
|
|
604
|
+
Err(DaemonError::Transport(message)) if message == "not used"
|
|
605
|
+
));
|
|
606
|
+
assert!(matches!(
|
|
607
|
+
daemon.rotate_agent_auth_token(&session, Uuid::new_v4()).await,
|
|
608
|
+
Err(DaemonError::Transport(message)) if message == "not used"
|
|
609
|
+
));
|
|
610
|
+
assert!(matches!(
|
|
611
|
+
daemon.revoke_agent_key(&session, Uuid::new_v4()).await,
|
|
612
|
+
Err(DaemonError::Transport(message)) if message == "not used"
|
|
613
|
+
));
|
|
614
|
+
assert!(matches!(
|
|
615
|
+
daemon.list_manual_approval_requests(&session).await,
|
|
616
|
+
Err(DaemonError::Transport(message)) if message == "not used"
|
|
617
|
+
));
|
|
618
|
+
assert!(matches!(
|
|
619
|
+
daemon
|
|
620
|
+
.decide_manual_approval_request(
|
|
621
|
+
&session,
|
|
622
|
+
Uuid::new_v4(),
|
|
623
|
+
ManualApprovalDecision::Approve,
|
|
624
|
+
None
|
|
625
|
+
)
|
|
626
|
+
.await,
|
|
627
|
+
Err(DaemonError::Transport(message)) if message == "not used"
|
|
628
|
+
));
|
|
629
|
+
assert!(matches!(
|
|
630
|
+
daemon
|
|
631
|
+
.set_relay_config(&session, Some("https://relay.example".to_string()), None)
|
|
632
|
+
.await,
|
|
633
|
+
Err(DaemonError::Transport(message)) if message == "not used"
|
|
634
|
+
));
|
|
635
|
+
assert!(matches!(
|
|
636
|
+
daemon.get_relay_config(&session).await,
|
|
637
|
+
Err(DaemonError::Transport(message)) if message == "not used"
|
|
638
|
+
));
|
|
639
|
+
assert!(matches!(
|
|
640
|
+
daemon.evaluate_for_agent(request.clone()).await,
|
|
641
|
+
Err(DaemonError::Transport(message)) if message == "not used"
|
|
642
|
+
));
|
|
643
|
+
assert!(matches!(
|
|
644
|
+
daemon.explain_for_agent(request).await,
|
|
645
|
+
Err(DaemonError::Transport(message)) if message == "not used"
|
|
646
|
+
));
|
|
647
|
+
|
|
648
|
+
let reservation = daemon
|
|
649
|
+
.reserve_nonce(reservation_request.clone())
|
|
650
|
+
.await
|
|
651
|
+
.expect("reserve nonce");
|
|
652
|
+
assert_eq!(reservation.chain_id, reservation_request.chain_id);
|
|
653
|
+
assert_eq!(reservation.nonce, reservation_request.min_nonce);
|
|
654
|
+
assert_eq!(
|
|
655
|
+
daemon
|
|
656
|
+
.nonce_request
|
|
657
|
+
.lock()
|
|
658
|
+
.expect("lock")
|
|
659
|
+
.clone()
|
|
660
|
+
.expect("captured nonce request")
|
|
661
|
+
.exact_nonce,
|
|
662
|
+
reservation_request.exact_nonce
|
|
663
|
+
);
|
|
664
|
+
daemon
|
|
665
|
+
.release_nonce(release_request)
|
|
666
|
+
.await
|
|
667
|
+
.expect("release nonce");
|
|
668
|
+
}
|
|
669
|
+
|
|
479
670
|
#[tokio::test]
|
|
480
671
|
async fn approve_sends_canonical_action_payload_and_auth_token() {
|
|
481
672
|
let daemon = Arc::new(RecordingDaemon::default());
|
|
@@ -518,10 +709,109 @@ mod tests {
|
|
|
518
709
|
assert_eq!(decoded, captured.action);
|
|
519
710
|
}
|
|
520
711
|
|
|
712
|
+
#[tokio::test]
|
|
713
|
+
async fn constructors_transfer_native_and_reservation_helpers_use_expected_identity() {
|
|
714
|
+
let daemon = Arc::new(RecordingDaemon::default());
|
|
715
|
+
let key_id = Uuid::new_v4();
|
|
716
|
+
let sdk = AgentSdk::new_with_key_id_and_token(
|
|
717
|
+
daemon.clone(),
|
|
718
|
+
key_id,
|
|
719
|
+
"agent-secret-token".to_string(),
|
|
720
|
+
);
|
|
721
|
+
|
|
722
|
+
let recipient: EvmAddress = "0x2100000000000000000000000000000000000000"
|
|
723
|
+
.parse()
|
|
724
|
+
.expect("recipient");
|
|
725
|
+
let signature = sdk
|
|
726
|
+
.transfer_native(1, recipient.clone(), 5)
|
|
727
|
+
.await
|
|
728
|
+
.expect("transfer native");
|
|
729
|
+
assert_eq!(signature.bytes, vec![0x11, 0x22]);
|
|
730
|
+
|
|
731
|
+
let captured = daemon
|
|
732
|
+
.request
|
|
733
|
+
.lock()
|
|
734
|
+
.expect("lock")
|
|
735
|
+
.clone()
|
|
736
|
+
.expect("captured request");
|
|
737
|
+
assert_eq!(captured.agent_key_id, key_id);
|
|
738
|
+
assert_eq!(captured.agent_auth_token, "agent-secret-token");
|
|
739
|
+
assert_eq!(
|
|
740
|
+
captured.action,
|
|
741
|
+
AgentAction::TransferNative {
|
|
742
|
+
chain_id: 1,
|
|
743
|
+
to: recipient,
|
|
744
|
+
amount_wei: 5
|
|
745
|
+
}
|
|
746
|
+
);
|
|
747
|
+
|
|
748
|
+
let reservation = sdk
|
|
749
|
+
.reserve_broadcast_nonce(56, 99)
|
|
750
|
+
.await
|
|
751
|
+
.expect("reserve nonce");
|
|
752
|
+
assert_eq!(reservation.chain_id, 56);
|
|
753
|
+
assert_eq!(reservation.nonce, 99);
|
|
754
|
+
let captured_reservation = daemon
|
|
755
|
+
.nonce_request
|
|
756
|
+
.lock()
|
|
757
|
+
.expect("lock")
|
|
758
|
+
.clone()
|
|
759
|
+
.expect("captured nonce request");
|
|
760
|
+
assert_eq!(captured_reservation.chain_id, 56);
|
|
761
|
+
assert_eq!(captured_reservation.min_nonce, 99);
|
|
762
|
+
assert!(captured_reservation.exact_nonce);
|
|
763
|
+
sdk.release_broadcast_nonce(reservation.reservation_id)
|
|
764
|
+
.await
|
|
765
|
+
.expect("release nonce");
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
#[tokio::test]
|
|
769
|
+
async fn sign_erc20_calldata_and_invalid_actions_propagate_domain_errors() {
|
|
770
|
+
let daemon = Arc::new(RecordingDaemon::default());
|
|
771
|
+
let sdk = AgentSdk::new(daemon.clone(), test_credentials());
|
|
772
|
+
let encoded = vec![
|
|
773
|
+
0x09, 0x5e, 0xa7, 0xb3, // approve(address,uint256)
|
|
774
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
775
|
+
0x00, 0x00, 0x00, 0x00, 0x44, 0x44, 0x44, 0x44,
|
|
776
|
+
0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44,
|
|
777
|
+
0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44,
|
|
778
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
779
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
780
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
781
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c,
|
|
782
|
+
];
|
|
783
|
+
|
|
784
|
+
let signature = sdk
|
|
785
|
+
.sign_erc20_calldata(
|
|
786
|
+
1,
|
|
787
|
+
"0x3300000000000000000000000000000000000000"
|
|
788
|
+
.parse()
|
|
789
|
+
.expect("token"),
|
|
790
|
+
encoded,
|
|
791
|
+
)
|
|
792
|
+
.await
|
|
793
|
+
.expect("sign erc20 calldata");
|
|
794
|
+
assert_eq!(signature.bytes, vec![0x11, 0x22]);
|
|
795
|
+
|
|
796
|
+
let err = sdk
|
|
797
|
+
.transfer_native(
|
|
798
|
+
1,
|
|
799
|
+
"0x5500000000000000000000000000000000000000"
|
|
800
|
+
.parse()
|
|
801
|
+
.expect("to"),
|
|
802
|
+
0,
|
|
803
|
+
)
|
|
804
|
+
.await
|
|
805
|
+
.expect_err("invalid action");
|
|
806
|
+
assert!(matches!(err, AgentSdkError::Domain(DomainError::InvalidAmount)));
|
|
807
|
+
}
|
|
808
|
+
|
|
521
809
|
#[tokio::test]
|
|
522
810
|
async fn transfer_propagates_daemon_errors() {
|
|
523
811
|
let daemon = Arc::new(RecordingDaemon {
|
|
524
812
|
request: Mutex::new(None),
|
|
813
|
+
nonce_request: Mutex::new(None),
|
|
814
|
+
released_reservation: Mutex::new(None),
|
|
525
815
|
fail_sign: true,
|
|
526
816
|
reserved_nonce_override: None,
|
|
527
817
|
});
|
|
@@ -580,9 +870,11 @@ mod tests {
|
|
|
580
870
|
}
|
|
581
871
|
|
|
582
872
|
#[tokio::test]
|
|
583
|
-
async fn
|
|
873
|
+
async fn broadcast_tx_rejects_mismatched_reserved_nonce_from_daemon() {
|
|
584
874
|
let daemon = Arc::new(RecordingDaemon {
|
|
585
875
|
request: Mutex::new(None),
|
|
876
|
+
nonce_request: Mutex::new(None),
|
|
877
|
+
released_reservation: Mutex::new(None),
|
|
586
878
|
fail_sign: false,
|
|
587
879
|
reserved_nonce_override: Some(1),
|
|
588
880
|
});
|
|
@@ -603,22 +895,34 @@ mod tests {
|
|
|
603
895
|
delegation_enabled: false,
|
|
604
896
|
};
|
|
605
897
|
|
|
606
|
-
sdk
|
|
898
|
+
let err = sdk
|
|
899
|
+
.broadcast_tx(tx.clone())
|
|
900
|
+
.await
|
|
901
|
+
.expect_err("mismatched reserved nonce must fail");
|
|
902
|
+
assert!(matches!(
|
|
903
|
+
err,
|
|
904
|
+
AgentSdkError::Daemon(DaemonError::InvalidNonceReservation(message))
|
|
905
|
+
if message.contains("requested exact nonce 0")
|
|
906
|
+
&& message.contains("chain_id 56")
|
|
907
|
+
&& message.contains("reserved 1")
|
|
908
|
+
));
|
|
607
909
|
|
|
608
|
-
|
|
609
|
-
|
|
910
|
+
assert!(daemon.request.lock().expect("lock").is_none());
|
|
911
|
+
assert!(
|
|
912
|
+
daemon
|
|
913
|
+
.released_reservation
|
|
914
|
+
.lock()
|
|
915
|
+
.expect("lock")
|
|
916
|
+
.is_some()
|
|
917
|
+
);
|
|
918
|
+
let captured_reservation = daemon
|
|
919
|
+
.nonce_request
|
|
610
920
|
.lock()
|
|
611
921
|
.expect("lock")
|
|
612
922
|
.clone()
|
|
613
|
-
.expect("captured request");
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
assert_eq!(signed_tx.nonce, 1);
|
|
617
|
-
assert_eq!(signed_tx.chain_id, tx.chain_id);
|
|
618
|
-
assert_eq!(signed_tx.to, tx.to);
|
|
619
|
-
}
|
|
620
|
-
other => panic!("unexpected action: {other:?}"),
|
|
621
|
-
}
|
|
923
|
+
.expect("captured nonce request");
|
|
924
|
+
assert!(captured_reservation.exact_nonce);
|
|
925
|
+
assert_eq!(captured_reservation.min_nonce, tx.nonce);
|
|
622
926
|
}
|
|
623
927
|
}
|
|
624
928
|
|
|
@@ -708,4 +1012,93 @@ mod typed_data_tests {
|
|
|
708
1012
|
let decoded: AgentAction = serde_json::from_slice(&captured.payload).expect("decode");
|
|
709
1013
|
assert_eq!(decoded, captured.action);
|
|
710
1014
|
}
|
|
1015
|
+
|
|
1016
|
+
#[tokio::test]
|
|
1017
|
+
async fn transfer_and_eip3009_transfer_sends_canonical_action_payload() {
|
|
1018
|
+
let daemon = Arc::new(RecordingDaemon::default());
|
|
1019
|
+
let sdk = AgentSdk::new(daemon.clone(), test_credentials());
|
|
1020
|
+
|
|
1021
|
+
let token = "0x3000000000000000000000000000000000000000"
|
|
1022
|
+
.parse()
|
|
1023
|
+
.expect("token");
|
|
1024
|
+
let to = "0x5000000000000000000000000000000000000000"
|
|
1025
|
+
.parse()
|
|
1026
|
+
.expect("to");
|
|
1027
|
+
let signature = sdk
|
|
1028
|
+
.transfer(1, token, to, 9)
|
|
1029
|
+
.await
|
|
1030
|
+
.expect("transfer");
|
|
1031
|
+
assert_eq!(signature.bytes, vec![0x11, 0x22]);
|
|
1032
|
+
|
|
1033
|
+
let transfer_action = daemon
|
|
1034
|
+
.request
|
|
1035
|
+
.lock()
|
|
1036
|
+
.expect("lock")
|
|
1037
|
+
.clone()
|
|
1038
|
+
.expect("captured request")
|
|
1039
|
+
.action;
|
|
1040
|
+
assert!(matches!(transfer_action, AgentAction::Transfer { .. }));
|
|
1041
|
+
|
|
1042
|
+
let authorization = Eip3009Transfer {
|
|
1043
|
+
chain_id: 1,
|
|
1044
|
+
token: "0x3000000000000000000000000000000000000000"
|
|
1045
|
+
.parse()
|
|
1046
|
+
.expect("token"),
|
|
1047
|
+
token_name: "USD Coin".to_string(),
|
|
1048
|
+
token_version: Some("2".to_string()),
|
|
1049
|
+
from: "0x4000000000000000000000000000000000000000"
|
|
1050
|
+
.parse()
|
|
1051
|
+
.expect("from"),
|
|
1052
|
+
to: "0x5000000000000000000000000000000000000000"
|
|
1053
|
+
.parse()
|
|
1054
|
+
.expect("to"),
|
|
1055
|
+
amount_wei: 9,
|
|
1056
|
+
valid_after: 1,
|
|
1057
|
+
valid_before: 2,
|
|
1058
|
+
nonce_hex: "0x5555555555555555555555555555555555555555555555555555555555555555"
|
|
1059
|
+
.to_string(),
|
|
1060
|
+
};
|
|
1061
|
+
|
|
1062
|
+
let signature = sdk
|
|
1063
|
+
.eip3009_transfer_with_authorization(authorization.clone())
|
|
1064
|
+
.await
|
|
1065
|
+
.expect("eip3009 transfer");
|
|
1066
|
+
assert_eq!(signature.bytes, vec![0x11, 0x22]);
|
|
1067
|
+
|
|
1068
|
+
let captured = daemon
|
|
1069
|
+
.request
|
|
1070
|
+
.lock()
|
|
1071
|
+
.expect("lock")
|
|
1072
|
+
.clone()
|
|
1073
|
+
.expect("captured request");
|
|
1074
|
+
assert_eq!(
|
|
1075
|
+
captured.action,
|
|
1076
|
+
AgentAction::Eip3009TransferWithAuthorization { authorization }
|
|
1077
|
+
);
|
|
1078
|
+
let decoded: AgentAction = serde_json::from_slice(&captured.payload).expect("decode");
|
|
1079
|
+
assert_eq!(decoded, captured.action);
|
|
1080
|
+
|
|
1081
|
+
let erc20_transfer = vec![
|
|
1082
|
+
0xa9, 0x05, 0x9c, 0xbb, // transfer(address,uint256)
|
|
1083
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
1084
|
+
0x00, 0x00, 0x00, 0x00, 0x55, 0x55, 0x55, 0x55,
|
|
1085
|
+
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
|
|
1086
|
+
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
|
|
1087
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
1088
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
1089
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
1090
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07,
|
|
1091
|
+
];
|
|
1092
|
+
let signature = sdk
|
|
1093
|
+
.sign_erc20_calldata(
|
|
1094
|
+
1,
|
|
1095
|
+
"0x6000000000000000000000000000000000000000"
|
|
1096
|
+
.parse()
|
|
1097
|
+
.expect("token"),
|
|
1098
|
+
erc20_transfer,
|
|
1099
|
+
)
|
|
1100
|
+
.await
|
|
1101
|
+
.expect("erc20 transfer");
|
|
1102
|
+
assert_eq!(signature.bytes, vec![0x11, 0x22]);
|
|
1103
|
+
}
|
|
711
1104
|
}
|