Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
refactor: drop the new_account parameter and derive the destination f…
…rom the signer
  • Loading branch information
sameh-farouk committed Sep 4, 2025
commit d41eb678d963ce356ac567d08e215419800715fc
4 changes: 2 additions & 2 deletions docs/architecture/0024-twin-ownership-transfer.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ Introduce a two-step, time-bound transfer protocol implemented in `pallet-tfgrid

### Dispatchables

- `request_twin_transfer(origin=new_account, twin_id, new_account)`
- Origin must match `new_account`.
- `request_twin_transfer(origin=new_account, twin_id)`
- Origin is the prospective new account.
- Validates preconditions and creates a pending transfer with expiry.
- Emits `TwinTransferRequested { twin_id, old_account, new_account }`.

Expand Down
7 changes: 3 additions & 4 deletions docs/misc/twin_transfer.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ This document explains the twin ownership transfer flow in the `pallet-tfgrid` p

## Overview

- The new owner initiates the transfer via `request_twin_transfer(twin_id, new_account)`.
- The new owner initiates the transfer via `request_twin_transfer(twin_id)`.
- The current owner accepts via `accept_twin_transfer(request_id)`.
- On success, ownership of the twin moves to the new account, reserved balances are repatriated, and indices are updated.

## Dispatchables

- request_twin_transfer(origin, twin_id, new_account)
- Origin must be the new account.
- request_twin_transfer(origin, twin_id)
- Origin (signer) is the new account.
- Emits `TwinTransferRequested { twin_id, old_account, new_account }`.
- accept_twin_transfer(origin, request_id)
- Origin must be the current (old) owner of the twin.
Expand All @@ -28,7 +28,6 @@ This document explains the twin ownership transfer flow in the `pallet-tfgrid` p
## Common Errors

- UserDidNotSignTermsAndConditions: new account did not accept T&C.
- TwinTransferRequestMustBeFromNewAccount: request must be signed by the new account.
- TwinTransferNewAccountHasTwin: new account already has a twin.
- TwinTransferPendingExists: a pending transfer already exists for this twin.
- TwinTransferRequestNotFound: request ID does not exist.
Expand Down
4 changes: 1 addition & 3 deletions substrate-node/pallets/pallet-tfgrid/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -624,7 +624,6 @@ pub mod pallet {
TwinTransferRequestAlreadyCompleted,
TwinTransferNewAccountHasTwin,
TwinTransferPendingExists,
TwinTransferRequestMustBeFromNewAccount,
}

#[pallet::genesis_config]
Expand Down Expand Up @@ -1296,9 +1295,8 @@ pub mod pallet {
pub fn request_twin_transfer(
origin: OriginFor<T>,
twin_id: u32,
new_account: T::AccountId,
) -> DispatchResultWithPostInfo {
Self::_request_twin_transfer(origin, twin_id, new_account)
Self::_request_twin_transfer(origin, twin_id)
}

// Twin ownership transfer: accept by current owner
Expand Down
29 changes: 4 additions & 25 deletions substrate-node/pallets/pallet-tfgrid/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ fn twin_transfer_request_happy_path() {
assert_ok!(TfgridModule::_request_twin_transfer(
RuntimeOrigin::signed(bob()),
1,
bob(),
));

// storage checks
Expand All @@ -56,23 +55,7 @@ fn twin_transfer_request_happy_path() {
});
}

#[test]
fn twin_transfer_request_wrong_initiator_fails() {
ExternalityBuilder::build().execute_with(|| {
create_twin();
assert_ok!(TfgridModule::user_accept_tc(
RuntimeOrigin::signed(bob()),
get_document_link_input(b"some_link"),
get_document_hash_input(b"some_hash"),
));

// alice cannot initiate a request for bob
assert_noop!(
TfgridModule::_request_twin_transfer(RuntimeOrigin::signed(alice()), 1, bob()),
Error::<TestRuntime>::TwinTransferRequestMustBeFromNewAccount
);
});
}
// Note: request must be initiated by the new account (signer). No test needed for mismatched initiator.

#[test]
fn twin_transfer_request_without_tc_fails() {
Expand All @@ -83,7 +66,7 @@ fn twin_transfer_request_without_tc_fails() {
// When: bob (new prospective owner) did NOT accept T&C and tries to request transfer
// Then: it should fail with UserDidNotSignTermsAndConditions
assert_noop!(
TfgridModule::_request_twin_transfer(RuntimeOrigin::signed(bob()), 1, bob()),
TfgridModule::_request_twin_transfer(RuntimeOrigin::signed(bob()), 1),
Error::<TestRuntime>::UserDidNotSignTermsAndConditions
);
});
Expand All @@ -96,7 +79,7 @@ fn twin_transfer_request_new_account_has_twin_fails() {
create_twin_bob(); // bob already has a twin

assert_noop!(
TfgridModule::_request_twin_transfer(RuntimeOrigin::signed(bob()), 1, bob()),
TfgridModule::_request_twin_transfer(RuntimeOrigin::signed(bob()), 1),
Error::<TestRuntime>::TwinTransferNewAccountHasTwin
);
});
Expand All @@ -114,12 +97,11 @@ fn twin_transfer_request_duplicate_pending_fails() {
assert_ok!(TfgridModule::_request_twin_transfer(
RuntimeOrigin::signed(bob()),
1,
bob(),
));

// second request while pending should fail
assert_noop!(
TfgridModule::_request_twin_transfer(RuntimeOrigin::signed(bob()), 1, bob()),
TfgridModule::_request_twin_transfer(RuntimeOrigin::signed(bob()), 1),
Error::<TestRuntime>::TwinTransferPendingExists
);
});
Expand All @@ -139,7 +121,6 @@ fn twin_transfer_accept_happy_path_moves_reserved_and_updates_owner() {
assert_ok!(TfgridModule::_request_twin_transfer(
RuntimeOrigin::signed(bob()),
1,
bob(),
));

// reserve some balance on alice
Expand Down Expand Up @@ -183,7 +164,6 @@ fn twin_transfer_accept_expired_fails() {
assert_ok!(TfgridModule::_request_twin_transfer(
RuntimeOrigin::signed(bob()),
1,
bob(),
));

// move block number beyond expiry
Expand All @@ -209,7 +189,6 @@ fn twin_transfer_accept_wrong_signer_fails() {
assert_ok!(TfgridModule::_request_twin_transfer(
RuntimeOrigin::signed(bob()),
1,
bob(),
));

// bob (new account) cannot accept; must be current owner (alice)
Expand Down
9 changes: 2 additions & 7 deletions substrate-node/pallets/pallet-tfgrid/src/twin_transfer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,9 @@ impl<T: Config> Pallet<T> {
pub fn _request_twin_transfer(
origin: T::RuntimeOrigin,
twin_id: u32,
new_account: T::AccountId,
) -> DispatchResultWithPostInfo {
let requester = ensure_signed(origin)?;
// Request must be initiated by the new account
ensure!(
requester == new_account,
Error::<T>::TwinTransferRequestMustBeFromNewAccount
);
// Derive the new account directly from the signer
let new_account = ensure_signed(origin)?;

// Twin must exist
let twin = Twins::<T>::get(&twin_id).ok_or(Error::<T>::TwinNotExists)?;
Expand Down