From 414b248b12b198931c70e1470763ae531f00d865 Mon Sep 17 00:00:00 2001 From: Ye Park Date: Mon, 9 Sep 2024 13:02:28 +0000 Subject: [PATCH 1/4] [framework] introduce private entry function for creating multisig account This eliminates the need for generating the complex proof the existing create_with_existing_account functions require. --- aptos-move/framework/README.md | 2 +- .../aptos-framework/doc/multisig_account.md | 101 ++++++++++++ .../sources/multisig_account.move | 107 ++++++++++++ .../src/aptos_framework_sdk_builder.rs | 156 ++++++++++++++++++ 4 files changed, 365 insertions(+), 1 deletion(-) diff --git a/aptos-move/framework/README.md b/aptos-move/framework/README.md index d308719bb50e0..31f6e61e81b06 100644 --- a/aptos-move/framework/README.md +++ b/aptos-move/framework/README.md @@ -79,7 +79,7 @@ The overall structure of the Aptos Framework is as follows: ├── aptos-token # Sources, testing and generated documentation for Aptos token component ├── aptos-stdlib # Sources, testing and generated documentation for Aptos stdlib component ├── move-stdlib # Sources, testing and generated documentation for Move stdlib component -├── cached-packages # Tooling to generate SDK from mvoe sources. +├── cached-packages # Tooling to generate SDK from move sources. ├── src # Compilation and generation of information from Move source files in the Aptos Framework. Not designed to be used as a Rust library ├── releases # Move release bundles └── tests diff --git a/aptos-move/framework/aptos-framework/doc/multisig_account.md b/aptos-move/framework/aptos-framework/doc/multisig_account.md index 8ce70dc316506..74dca99c66d88 100644 --- a/aptos-move/framework/aptos-framework/doc/multisig_account.md +++ b/aptos-move/framework/aptos-framework/doc/multisig_account.md @@ -82,7 +82,9 @@ and implement the governance voting logic on top. - [Function `next_sequence_number`](#0x1_multisig_account_next_sequence_number) - [Function `vote`](#0x1_multisig_account_vote) - [Function `available_transaction_queue_capacity`](#0x1_multisig_account_available_transaction_queue_capacity) +- [Function `create_with_existing_account_call`](#0x1_multisig_account_create_with_existing_account_call) - [Function `create_with_existing_account`](#0x1_multisig_account_create_with_existing_account) +- [Function `create_with_existing_account_and_revoke_auth_key_call`](#0x1_multisig_account_create_with_existing_account_and_revoke_auth_key_call) - [Function `create_with_existing_account_and_revoke_auth_key`](#0x1_multisig_account_create_with_existing_account_and_revoke_auth_key) - [Function `create`](#0x1_multisig_account_create) - [Function `create_with_owners`](#0x1_multisig_account_create_with_owners) @@ -1932,6 +1934,50 @@ Return a bool tuple indicating whether an owner has voted and if so, whether the + + + + +## Function `create_with_existing_account_call` + +Private entry function that creates a new multisig account on top of an existing account. + +This offers a migration path for an existing account with any type of auth key. + +Note that this does not revoke auth key-based control over the account. Owners should separately rotate the auth +key after they are fully migrated to the new multisig account. Alternatively, they can call +create_with_existing_account_and_revoke_auth_key_call instead. + + +
entry fun create_with_existing_account_call(multisig_account: &signer, owners: vector<address>, num_signatures_required: u64, metadata_keys: vector<string::String>, metadata_values: vector<vector<u8>>)
+
+ + + +
+Implementation + + +
entry fun create_with_existing_account_call(
+    multisig_account: &signer,
+    owners: vector<address>,
+    num_signatures_required: u64,
+    metadata_keys: vector<String>,
+    metadata_values: vector<vector<u8>>,
+) acquires MultisigAccount {
+    create_with_owners_internal(
+        multisig_account,
+        owners,
+        num_signatures_required,
+        option::none<SignerCapability>(),
+        metadata_keys,
+        metadata_values,
+    );
+}
+
+ + +
@@ -2002,6 +2048,61 @@ create_with_existing_account_and_revoke_auth_key instead. + + + + +## Function `create_with_existing_account_and_revoke_auth_key_call` + +Private entry function that creates a new multisig account on top of an existing account and immediately rotate +the origin auth key to 0x0. + +Note: If the original account is a resource account, this does not revoke all control over it as if any +SignerCapability of the resource account still exists, it can still be used to generate the signer for the +account. + + +
entry fun create_with_existing_account_and_revoke_auth_key_call(multisig_account: &signer, owners: vector<address>, num_signatures_required: u64, metadata_keys: vector<string::String>, metadata_values: vector<vector<u8>>)
+
+ + + +
+Implementation + + +
entry fun create_with_existing_account_and_revoke_auth_key_call(
+    multisig_account: &signer,
+    owners: vector<address>,
+    num_signatures_required: u64,
+    metadata_keys: vector<String>,
+    metadata_values:vector<vector<u8>>,
+) acquires MultisigAccount {
+    create_with_owners_internal(
+        multisig_account,
+        owners,
+        num_signatures_required,
+        option::none<SignerCapability>(),
+        metadata_keys,
+        metadata_values,
+    );
+
+    // Rotate the account's auth key to 0x0, which effectively revokes control via auth key.
+    let multisig_address = address_of(multisig_account);
+    account::rotate_authentication_key_internal(multisig_account, ZERO_AUTH_KEY);
+    // This also needs to revoke any signer capability or rotation capability that exists for the account to
+    // completely remove all access to the account.
+    if (account::is_signer_capability_offered(multisig_address)) {
+        account::revoke_any_signer_capability(multisig_account);
+    };
+    if (account::is_rotation_capability_offered(multisig_address)) {
+        account::revoke_any_rotation_capability(multisig_account);
+    };
+}
+
+ + +
diff --git a/aptos-move/framework/aptos-framework/sources/multisig_account.move b/aptos-move/framework/aptos-framework/sources/multisig_account.move index 1917e584d4a3b..80fffd93ecd59 100644 --- a/aptos-move/framework/aptos-framework/sources/multisig_account.move +++ b/aptos-move/framework/aptos-framework/sources/multisig_account.move @@ -497,6 +497,30 @@ module aptos_framework::multisig_account { ////////////////////////// Multisig account creation functions /////////////////////////////// + /// Private entry function that creates a new multisig account on top of an existing account. + /// + /// This offers a migration path for an existing account with any type of auth key. + /// + /// Note that this does not revoke auth key-based control over the account. Owners should separately rotate the auth + /// key after they are fully migrated to the new multisig account. Alternatively, they can call + /// create_with_existing_account_and_revoke_auth_key_call instead. + entry fun create_with_existing_account_call( + multisig_account: &signer, + owners: vector
, + num_signatures_required: u64, + metadata_keys: vector, + metadata_values: vector>, + ) acquires MultisigAccount { + create_with_owners_internal( + multisig_account, + owners, + num_signatures_required, + option::none(), + metadata_keys, + metadata_values, + ); + } + /// Creates a new multisig account on top of an existing account. /// /// This offers a migration path for an existing account with a multi-ed25519 auth key (native multisig account). @@ -547,6 +571,41 @@ module aptos_framework::multisig_account { ); } + /// Private entry function that creates a new multisig account on top of an existing account and immediately rotate + /// the origin auth key to 0x0. + /// + /// Note: If the original account is a resource account, this does not revoke all control over it as if any + /// SignerCapability of the resource account still exists, it can still be used to generate the signer for the + /// account. + entry fun create_with_existing_account_and_revoke_auth_key_call( + multisig_account: &signer, + owners: vector
, + num_signatures_required: u64, + metadata_keys: vector, + metadata_values:vector>, + ) acquires MultisigAccount { + create_with_owners_internal( + multisig_account, + owners, + num_signatures_required, + option::none(), + metadata_keys, + metadata_values, + ); + + // Rotate the account's auth key to 0x0, which effectively revokes control via auth key. + let multisig_address = address_of(multisig_account); + account::rotate_authentication_key_internal(multisig_account, ZERO_AUTH_KEY); + // This also needs to revoke any signer capability or rotation capability that exists for the account to + // completely remove all access to the account. + if (account::is_signer_capability_offered(multisig_address)) { + account::revoke_any_signer_capability(multisig_account); + }; + if (account::is_rotation_capability_offered(multisig_address)) { + account::revoke_any_rotation_capability(multisig_account); + }; + } + /// Creates a new multisig account on top of an existing account and immediately rotate the origin auth key to 0x0. /// /// Note: If the original account is a resource account, this does not revoke all control over it as if any @@ -1688,6 +1747,26 @@ module aptos_framework::multisig_account { ); } + #[test] + public entry fun test_create_multisig_account_on_top_of_existing_with_signer() + acquires MultisigAccount { + setup(); + + let multisig_address = @0xabc; + create_account(multisig_address); + + let expected_owners = vector[@0x123, @0x124, @0x125]; + create_with_existing_account_call( + &create_signer(multisig_address), + expected_owners, + 2, + vector[], + vector[], + ); + assert_multisig_account_exists(multisig_address); + assert!(owners(multisig_address) == expected_owners, 0); + } + #[test] public entry fun test_create_multisig_account_on_top_of_existing_multi_ed25519_account() acquires MultisigAccount { @@ -1721,6 +1800,34 @@ module aptos_framework::multisig_account { assert!(owners(multisig_address) == expected_owners, 0); } + #[test] + public entry fun test_create_multisig_account_on_top_of_existing_and_revoke_auth_key_with_signer() + acquires MultisigAccount { + setup(); + + let multisig_address = @0xabc; + create_account(multisig_address); + + // Create both a signer capability and rotation capability offers + account::set_rotation_capability_offer(multisig_address, @0x123); + account::set_signer_capability_offer(multisig_address, @0x123); + + let expected_owners = vector[@0x123, @0x124, @0x125]; + create_with_existing_account_and_revoke_auth_key_call( + &create_signer(multisig_address), + expected_owners, + 2, + vector[], + vector[], + ); + assert_multisig_account_exists(multisig_address); + assert!(owners(multisig_address) == expected_owners, 0); + assert!(account::get_authentication_key(multisig_address) == ZERO_AUTH_KEY, 1); + // Verify that all capability offers have been wiped. + assert!(!account::is_rotation_capability_offered(multisig_address), 2); + assert!(!account::is_signer_capability_offered(multisig_address), 3); + } + #[test] public entry fun test_create_multisig_account_on_top_of_existing_multi_ed25519_account_and_revoke_auth_key() acquires MultisigAccount { diff --git a/aptos-move/framework/cached-packages/src/aptos_framework_sdk_builder.rs b/aptos-move/framework/cached-packages/src/aptos_framework_sdk_builder.rs index aa41aa6ea1fd3..71987dc605f9d 100644 --- a/aptos-move/framework/cached-packages/src/aptos_framework_sdk_builder.rs +++ b/aptos-move/framework/cached-packages/src/aptos_framework_sdk_builder.rs @@ -617,6 +617,33 @@ pub enum EntryFunctionCall { metadata_values: Vec>, }, + /// Private entry function that creates a new multisig account on top of an existing account and immediately rotate + /// the origin auth key to 0x0. + /// + /// Note: If the original account is a resource account, this does not revoke all control over it as if any + /// SignerCapability of the resource account still exists, it can still be used to generate the signer for the + /// account. + MultisigAccountCreateWithExistingAccountAndRevokeAuthKeyCall { + owners: Vec, + num_signatures_required: u64, + metadata_keys: Vec>, + metadata_values: Vec>, + }, + + /// Private entry function that creates a new multisig account on top of an existing account. + /// + /// This offers a migration path for an existing account with any type of auth key. + /// + /// Note that this does not revoke auth key-based control over the account. Owners should separately rotate the auth + /// key after they are fully migrated to the new multisig account. Alternatively, they can call + /// create_with_existing_account_and_revoke_auth_key_call instead. + MultisigAccountCreateWithExistingAccountCall { + owners: Vec, + num_signatures_required: u64, + metadata_keys: Vec>, + metadata_values: Vec>, + }, + /// Creates a new multisig account with the specified additional owner list and signatures required. /// /// @param additional_owners The owner account who calls this function cannot be in the additional_owners and there @@ -1418,6 +1445,28 @@ impl EntryFunctionCall { metadata_keys, metadata_values, ), + MultisigAccountCreateWithExistingAccountAndRevokeAuthKeyCall { + owners, + num_signatures_required, + metadata_keys, + metadata_values, + } => multisig_account_create_with_existing_account_and_revoke_auth_key_call( + owners, + num_signatures_required, + metadata_keys, + metadata_values, + ), + MultisigAccountCreateWithExistingAccountCall { + owners, + num_signatures_required, + metadata_keys, + metadata_values, + } => multisig_account_create_with_existing_account_call( + owners, + num_signatures_required, + metadata_keys, + metadata_values, + ), MultisigAccountCreateWithOwners { additional_owners, num_signatures_required, @@ -3282,6 +3331,69 @@ pub fn multisig_account_create_with_existing_account_and_revoke_auth_key( )) } +/// Private entry function that creates a new multisig account on top of an existing account and immediately rotate +/// the origin auth key to 0x0. +/// +/// Note: If the original account is a resource account, this does not revoke all control over it as if any +/// SignerCapability of the resource account still exists, it can still be used to generate the signer for the +/// account. +pub fn multisig_account_create_with_existing_account_and_revoke_auth_key_call( + owners: Vec, + num_signatures_required: u64, + metadata_keys: Vec>, + metadata_values: Vec>, +) -> TransactionPayload { + TransactionPayload::EntryFunction(EntryFunction::new( + ModuleId::new( + AccountAddress::new([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, + ]), + ident_str!("multisig_account").to_owned(), + ), + ident_str!("create_with_existing_account_and_revoke_auth_key_call").to_owned(), + vec![], + vec![ + bcs::to_bytes(&owners).unwrap(), + bcs::to_bytes(&num_signatures_required).unwrap(), + bcs::to_bytes(&metadata_keys).unwrap(), + bcs::to_bytes(&metadata_values).unwrap(), + ], + )) +} + +/// Private entry function that creates a new multisig account on top of an existing account. +/// +/// This offers a migration path for an existing account with any type of auth key. +/// +/// Note that this does not revoke auth key-based control over the account. Owners should separately rotate the auth +/// key after they are fully migrated to the new multisig account. Alternatively, they can call +/// create_with_existing_account_and_revoke_auth_key_call instead. +pub fn multisig_account_create_with_existing_account_call( + owners: Vec, + num_signatures_required: u64, + metadata_keys: Vec>, + metadata_values: Vec>, +) -> TransactionPayload { + TransactionPayload::EntryFunction(EntryFunction::new( + ModuleId::new( + AccountAddress::new([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, + ]), + ident_str!("multisig_account").to_owned(), + ), + ident_str!("create_with_existing_account_call").to_owned(), + vec![], + vec![ + bcs::to_bytes(&owners).unwrap(), + bcs::to_bytes(&num_signatures_required).unwrap(), + bcs::to_bytes(&metadata_keys).unwrap(), + bcs::to_bytes(&metadata_values).unwrap(), + ], + )) +} + /// Creates a new multisig account with the specified additional owner list and signatures required. /// /// @param additional_owners The owner account who calls this function cannot be in the additional_owners and there @@ -5666,6 +5778,40 @@ mod decoder { } } + pub fn multisig_account_create_with_existing_account_and_revoke_auth_key_call( + payload: &TransactionPayload, + ) -> Option { + if let TransactionPayload::EntryFunction(script) = payload { + Some( + EntryFunctionCall::MultisigAccountCreateWithExistingAccountAndRevokeAuthKeyCall { + owners: bcs::from_bytes(script.args().get(0)?).ok()?, + num_signatures_required: bcs::from_bytes(script.args().get(1)?).ok()?, + metadata_keys: bcs::from_bytes(script.args().get(2)?).ok()?, + metadata_values: bcs::from_bytes(script.args().get(3)?).ok()?, + }, + ) + } else { + None + } + } + + pub fn multisig_account_create_with_existing_account_call( + payload: &TransactionPayload, + ) -> Option { + if let TransactionPayload::EntryFunction(script) = payload { + Some( + EntryFunctionCall::MultisigAccountCreateWithExistingAccountCall { + owners: bcs::from_bytes(script.args().get(0)?).ok()?, + num_signatures_required: bcs::from_bytes(script.args().get(1)?).ok()?, + metadata_keys: bcs::from_bytes(script.args().get(2)?).ok()?, + metadata_values: bcs::from_bytes(script.args().get(3)?).ok()?, + }, + ) + } else { + None + } + } + pub fn multisig_account_create_with_owners( payload: &TransactionPayload, ) -> Option { @@ -6849,6 +6995,16 @@ static SCRIPT_FUNCTION_DECODER_MAP: once_cell::sync::Lazy Date: Sat, 28 Sep 2024 07:39:35 +0000 Subject: [PATCH 2/4] add an integration test --- api/src/tests/multisig_transactions_test.rs | 60 +++++++++++++++++++++ api/test-context/src/test_context.rs | 20 +++++++ sdk/src/transaction_builder.rs | 26 +++++++++ 3 files changed, 106 insertions(+) diff --git a/api/src/tests/multisig_transactions_test.rs b/api/src/tests/multisig_transactions_test.rs index 9cd59d00e41ee..a716b9d0091b7 100644 --- a/api/src/tests/multisig_transactions_test.rs +++ b/api/src/tests/multisig_transactions_test.rs @@ -50,6 +50,66 @@ async fn test_multisig_transaction_with_payload_succeeds() { assert_eq!(0, context.get_apt_balance(multisig_account).await); } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_multisig_transaction_with_existing_account() { + let mut context = new_test_context(current_function_name!()); + let multisig_account = &mut context.create_account().await; + let owner_account_1 = &mut context.create_account().await; + let owner_account_2 = &mut context.create_account().await; + let owner_account_3 = &mut context.create_account().await; + let owners = vec![ + owner_account_1.address(), + owner_account_2.address(), + owner_account_3.address(), + ]; + context + .create_multisig_account_with_existing_account(multisig_account, owners.clone(), 2, 1000) + .await; + assert_owners(&context, multisig_account.address(), owners).await; + assert_signature_threshold(&context, multisig_account.address(), 2).await; + + let multisig_payload = construct_multisig_txn_transfer_payload(owner_account_1.address(), 1000); + context + .create_multisig_transaction( + owner_account_1, + multisig_account.address(), + multisig_payload.clone(), + ) + .await; + // Owner 2 approves and owner 3 rejects. There are still 2 approvals total (owners 1 and 2) so + // the transaction can still be executed. + context + .approve_multisig_transaction(owner_account_2, multisig_account.address(), 1) + .await; + context + .reject_multisig_transaction(owner_account_3, multisig_account.address(), 1) + .await; + + let org_multisig_balance = context.get_apt_balance(multisig_account.address()).await; + let org_owner_1_balance = context.get_apt_balance(owner_account_1.address()).await; + + context + .execute_multisig_transaction(owner_account_2, multisig_account.address(), 202) + .await; + + // The multisig tx that transfers away 1000 APT should have succeeded. + assert_multisig_tx_executed( + &mut context, + multisig_account.address(), + multisig_payload, + 1, + ) + .await; + assert_eq!( + org_multisig_balance - 1000, + context.get_apt_balance(multisig_account.address()).await + ); + assert_eq!( + org_owner_1_balance + 1000, + context.get_apt_balance(owner_account_1.address()).await + ); +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_multisig_transaction_to_update_owners() { let mut context = new_test_context(current_function_name!()); diff --git a/api/test-context/src/test_context.rs b/api/test-context/src/test_context.rs index 45452d7311d15..759358708bd8d 100644 --- a/api/test-context/src/test_context.rs +++ b/api/test-context/src/test_context.rs @@ -451,6 +451,26 @@ impl TestContext { multisig_address } + pub async fn create_multisig_account_with_existing_account( + &mut self, + account: &mut LocalAccount, + owners: Vec, + signatures_required: u64, + initial_balance: u64, + ) { + let factory = self.transaction_factory(); + let txn = account.sign_with_transaction_builder( + factory + .create_multisig_account_with_existing_account(owners, signatures_required) + .expiration_timestamp_secs(u64::MAX), + ); + self.commit_block(&vec![ + txn, + self.account_transfer_to(account, account.address(), initial_balance), + ]) + .await; + } + pub async fn create_multisig_transaction( &mut self, owner: &mut LocalAccount, diff --git a/sdk/src/transaction_builder.rs b/sdk/src/transaction_builder.rs index 9f5e958fef9b8..7f257dffedb7f 100644 --- a/sdk/src/transaction_builder.rs +++ b/sdk/src/transaction_builder.rs @@ -187,6 +187,32 @@ impl TransactionFactory { )) } + pub fn create_multisig_account_with_existing_account( + &self, + additional_owners: Vec, + signatures_required: u64, + ) -> TransactionBuilder { + self.payload(aptos_stdlib::multisig_account_create_with_existing_account_call( + additional_owners, + signatures_required, + vec![], + vec![], + )) + } + + pub fn create_multisig_account_with_existing_account_and_revoke_auth_key( + &self, + additional_owners: Vec, + signatures_required: u64, + ) -> TransactionBuilder { + self.payload(aptos_stdlib::multisig_account_create_with_existing_account_and_revoke_auth_key_call( + additional_owners, + signatures_required, + vec![], + vec![], + )) + } + pub fn create_multisig_transaction( &self, multisig_account: AccountAddress, From 524333a7b0083c4299d8c4a8d2ff5cc50d0b21af Mon Sep 17 00:00:00 2001 From: Ye Park Date: Sat, 28 Sep 2024 09:14:17 +0000 Subject: [PATCH 3/4] fix arg names --- sdk/src/transaction_builder.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sdk/src/transaction_builder.rs b/sdk/src/transaction_builder.rs index 7f257dffedb7f..b13daaedf96af 100644 --- a/sdk/src/transaction_builder.rs +++ b/sdk/src/transaction_builder.rs @@ -189,11 +189,11 @@ impl TransactionFactory { pub fn create_multisig_account_with_existing_account( &self, - additional_owners: Vec, + owners: Vec, signatures_required: u64, ) -> TransactionBuilder { self.payload(aptos_stdlib::multisig_account_create_with_existing_account_call( - additional_owners, + owners, signatures_required, vec![], vec![], @@ -202,11 +202,11 @@ impl TransactionFactory { pub fn create_multisig_account_with_existing_account_and_revoke_auth_key( &self, - additional_owners: Vec, + owners: Vec, signatures_required: u64, ) -> TransactionBuilder { self.payload(aptos_stdlib::multisig_account_create_with_existing_account_and_revoke_auth_key_call( - additional_owners, + owners, signatures_required, vec![], vec![], From 27ff60d41b8cbd1110b9218abcf3c8e754e30ea9 Mon Sep 17 00:00:00 2001 From: Ye Park Date: Tue, 1 Oct 2024 04:16:08 +0000 Subject: [PATCH 4/4] fix lint --- sdk/src/transaction_builder.rs | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/sdk/src/transaction_builder.rs b/sdk/src/transaction_builder.rs index b13daaedf96af..3ba9c981735d1 100644 --- a/sdk/src/transaction_builder.rs +++ b/sdk/src/transaction_builder.rs @@ -192,12 +192,14 @@ impl TransactionFactory { owners: Vec, signatures_required: u64, ) -> TransactionBuilder { - self.payload(aptos_stdlib::multisig_account_create_with_existing_account_call( - owners, - signatures_required, - vec![], - vec![], - )) + self.payload( + aptos_stdlib::multisig_account_create_with_existing_account_call( + owners, + signatures_required, + vec![], + vec![], + ), + ) } pub fn create_multisig_account_with_existing_account_and_revoke_auth_key( @@ -205,12 +207,14 @@ impl TransactionFactory { owners: Vec, signatures_required: u64, ) -> TransactionBuilder { - self.payload(aptos_stdlib::multisig_account_create_with_existing_account_and_revoke_auth_key_call( - owners, - signatures_required, - vec![], - vec![], - )) + self.payload( + aptos_stdlib::multisig_account_create_with_existing_account_and_revoke_auth_key_call( + owners, + signatures_required, + vec![], + vec![], + ), + ) } pub fn create_multisig_transaction(