From 77e7a38a77079a803f5b91a995e076bb757227ef Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 14 Dec 2022 07:42:05 +0700 Subject: [PATCH 1/3] [Aptos Framework][Coin] Switch to an explicit opt-in model for coin transfers --- .../framework/aptos-framework/doc/account.md | 6 +- .../aptos-framework/doc/aptos_account.md | 231 ++++++++++++++++++ .../aptos-framework/sources/account.move | 2 +- .../sources/aptos_account.move | 181 ++++++++++++-- .../src/aptos_framework_sdk_builder.rs | 94 +++++++ 5 files changed, 496 insertions(+), 18 deletions(-) diff --git a/aptos-move/framework/aptos-framework/doc/account.md b/aptos-move/framework/aptos-framework/doc/account.md index b51fc35cbdc71..9cf42dd9990a0 100644 --- a/aptos-move/framework/aptos-framework/doc/account.md +++ b/aptos-move/framework/aptos-framework/doc/account.md @@ -692,7 +692,7 @@ Scheme identifier for MultiEd25519 signatures used to derive authentication keys -
fun create_signer(addr: address): signer
+
public(friend) fun create_signer(addr: address): signer
 
@@ -701,7 +701,7 @@ Scheme identifier for MultiEd25519 signatures used to derive authentication keys Implementation -
native fun create_signer(addr: address): signer;
+
public(friend) native fun create_signer(addr: address): signer;
 
@@ -1527,7 +1527,7 @@ Capability based functions for efficient use. ### Function `create_signer` -
fun create_signer(addr: address): signer
+
public(friend) fun create_signer(addr: address): signer
 
diff --git a/aptos-move/framework/aptos-framework/doc/aptos_account.md b/aptos-move/framework/aptos-framework/doc/aptos_account.md index eff1a3d3fb7fa..1a8adbf36c008 100644 --- a/aptos-move/framework/aptos-framework/doc/aptos_account.md +++ b/aptos-move/framework/aptos-framework/doc/aptos_account.md @@ -5,11 +5,17 @@ +- [Resource `DirectTransferConfig`](#0x1_aptos_account_DirectTransferConfig) +- [Struct `DirectCoinTransferConfigUpdatedEvent`](#0x1_aptos_account_DirectCoinTransferConfigUpdatedEvent) - [Constants](#@Constants_0) - [Function `create_account`](#0x1_aptos_account_create_account) - [Function `transfer`](#0x1_aptos_account_transfer) +- [Function `transfer_coins`](#0x1_aptos_account_transfer_coins) +- [Function `deposit_coins`](#0x1_aptos_account_deposit_coins) - [Function `assert_account_exists`](#0x1_aptos_account_assert_account_exists) - [Function `assert_account_is_registered_for_apt`](#0x1_aptos_account_assert_account_is_registered_for_apt) +- [Function `set_allow_direct_coin_transfers`](#0x1_aptos_account_set_allow_direct_coin_transfers) +- [Function `can_receive_direct_coin_transfers`](#0x1_aptos_account_can_receive_direct_coin_transfers) - [Specification](#@Specification_1) - [Function `create_account`](#@Specification_1_create_account) - [Function `transfer`](#@Specification_1_transfer) @@ -21,15 +27,101 @@ use 0x1::aptos_coin; use 0x1::coin; use 0x1::error; +use 0x1::event; +use 0x1::signer;
+ + +## Resource `DirectTransferConfig` + +Configuration for whether an account can receive direct transfers of coins that they have not registered. + +By default, this is enabled. Users can opt-out by disabling at any time. + + +
struct DirectTransferConfig has key
+
+ + + +
+Fields + + +
+
+allow_arbitrary_coin_transfers: bool +
+
+ +
+
+update_coin_transfer_events: event::EventHandle<aptos_account::DirectCoinTransferConfigUpdatedEvent> +
+
+ +
+
+ + +
+ + + +## Struct `DirectCoinTransferConfigUpdatedEvent` + +Event emitted when an account's direct coins transfer config is updated. + + +
struct DirectCoinTransferConfigUpdatedEvent has drop, store
+
+ + + +
+Fields + + +
+
+new_allow_direct_transfers: bool +
+
+ +
+
+ + +
+ ## Constants + + +Account opted out of receiving coins that they did not register to receive. + + +
const EACCOUNT_DOES_NOT_ACCEPT_DIRECT_COIN_TRANSFERS: u64 = 3;
+
+ + + + + +Account opted out of directly receiving NFT tokens. + + +
const EACCOUNT_DOES_NOT_ACCEPT_DIRECT_TOKEN_TRANSFERS: u64 = 4;
+
+ + + Account does not exist. @@ -80,6 +172,8 @@ Basic account creation methods. ## Function `transfer` +Convenient function to transfer APT to a recipient account that might not exist. +This would create the recipient account first, which also registers it to receive APT, before transferring.
public entry fun transfer(source: &signer, to: address, amount: u64)
@@ -101,6 +195,68 @@ Basic account creation methods.
 
 
 
+
+
+
+
+## Function `transfer_coins`
+
+Convenient function to transfer a custom CoinType to a recipient account that might not exist.
+This would create the recipient account first and register it to receive the CoinType, before transferring.
+
+
+
public entry fun transfer_coins<CoinType>(from: &signer, to: address, amount: u64)
+
+ + + +
+Implementation + + +
public entry fun transfer_coins<CoinType>(from: &signer, to: address, amount: u64) acquires DirectTransferConfig {
+    deposit_coins(to, coin::withdraw<CoinType>(from, amount));
+}
+
+ + + +
+ + + +## Function `deposit_coins` + +Convenient function to deposit a custom CoinType into a recipient account that might not exist. +This would create the recipient account first and register it to receive the CoinType, before transferring. + + +
public fun deposit_coins<CoinType>(to: address, coins: coin::Coin<CoinType>)
+
+ + + +
+Implementation + + +
public fun deposit_coins<CoinType>(to: address, coins: Coin<CoinType>) acquires DirectTransferConfig {
+    if (!account::exists_at(to)) {
+        create_account(to);
+    };
+    if (!coin::is_account_registered<CoinType>(to)) {
+        assert!(
+            can_receive_direct_coin_transfers(to),
+            error::permission_denied(EACCOUNT_DOES_NOT_ACCEPT_DIRECT_COIN_TRANSFERS),
+        );
+        coin::register<CoinType>(&account::create_signer(to));
+    };
+    coin::deposit<CoinType>(to, coins)
+}
+
+ + +
@@ -150,6 +306,81 @@ Basic account creation methods. + + + + +## Function `set_allow_direct_coin_transfers` + +Set whether account can receive direct transfers of coins that they have not explicitly registered to receive. + + +
public entry fun set_allow_direct_coin_transfers(account: &signer, allow: bool)
+
+ + + +
+Implementation + + +
public entry fun set_allow_direct_coin_transfers(account: &signer, allow: bool) acquires DirectTransferConfig {
+    let addr = signer::address_of(account);
+    if (exists<DirectTransferConfig>(addr)) {
+        let direct_transfer_config = borrow_global_mut<DirectTransferConfig>(addr);
+        // Short-circuit to avoid emitting an event if direct transfer config is not changing.
+        if (direct_transfer_config.allow_arbitrary_coin_transfers != allow) {
+            return
+        };
+
+        direct_transfer_config.allow_arbitrary_coin_transfers = allow;
+        emit_event(
+            &mut direct_transfer_config.update_coin_transfer_events,
+            DirectCoinTransferConfigUpdatedEvent { new_allow_direct_transfers: allow });
+    } else {
+        let direct_transfer_config = DirectTransferConfig {
+            allow_arbitrary_coin_transfers: allow,
+            update_coin_transfer_events: new_event_handle<DirectCoinTransferConfigUpdatedEvent>(account),
+        };
+        emit_event(
+            &mut direct_transfer_config.update_coin_transfer_events,
+            DirectCoinTransferConfigUpdatedEvent { new_allow_direct_transfers: allow });
+        move_to(account, direct_transfer_config);
+    };
+}
+
+ + + +
+ + + +## Function `can_receive_direct_coin_transfers` + +Return true if account can receive direct transfers of coins that they have not explicitly registered to +receive. + +By default, this returns true if an account has not explicitly set whether the can receive direct transfers. + + +
public fun can_receive_direct_coin_transfers(account: address): bool
+
+ + + +
+Implementation + + +
public fun can_receive_direct_coin_transfers(account: address): bool acquires DirectTransferConfig {
+    !exists<DirectTransferConfig>(account) ||
+        borrow_global<DirectTransferConfig>(account).allow_arbitrary_coin_transfers
+}
+
+ + +
diff --git a/aptos-move/framework/aptos-framework/sources/account.move b/aptos-move/framework/aptos-framework/sources/account.move index 89631984f031e..91e735bcbabc2 100644 --- a/aptos-move/framework/aptos-framework/sources/account.move +++ b/aptos-move/framework/aptos-framework/sources/account.move @@ -127,7 +127,7 @@ module aptos_framework::account { /// An attempt to create a resource account on an account that has a committed transaction const EACCOUNT_ALREADY_USED: u64 = 16; - native fun create_signer(addr: address): signer; + public(friend) native fun create_signer(addr: address): signer; #[test_only] /// Create signer for testing, independently of an Aptos-style `Account`. diff --git a/aptos-move/framework/aptos-framework/sources/aptos_account.move b/aptos-move/framework/aptos-framework/sources/aptos_account.move index fef7272bf9cf0..6d1d8b9020c5b 100644 --- a/aptos-move/framework/aptos-framework/sources/aptos_account.move +++ b/aptos-move/framework/aptos-framework/sources/aptos_account.move @@ -1,9 +1,10 @@ module aptos_framework::aptos_account { - use std::error; - - use aptos_framework::account; + use aptos_framework::account::{Self, new_event_handle}; use aptos_framework::aptos_coin::AptosCoin; use aptos_framework::coin; + use aptos_framework::event::{EventHandle, emit_event}; + use std::error; + use std::signer; friend aptos_framework::genesis; friend aptos_framework::resource_account; @@ -12,6 +13,23 @@ module aptos_framework::aptos_account { const EACCOUNT_NOT_FOUND: u64 = 1; /// Account is not registered to receive APT. const EACCOUNT_NOT_REGISTERED_FOR_APT: u64 = 2; + /// Account opted out of receiving coins that they did not register to receive. + const EACCOUNT_DOES_NOT_ACCEPT_DIRECT_COIN_TRANSFERS: u64 = 3; + /// Account opted out of directly receiving NFT tokens. + const EACCOUNT_DOES_NOT_ACCEPT_DIRECT_TOKEN_TRANSFERS: u64 = 4; + + /// Configuration for whether an account can receive direct transfers of coins that they have not registered. + /// + /// By default, this is enabled. Users can opt-out by disabling at any time. + struct DirectTransferConfig has key { + allow_arbitrary_coin_transfers: bool, + update_coin_transfer_events: EventHandle, + } + + /// Event emitted when an account's direct coins transfer config is updated. + struct DirectCoinTransferConfigUpdatedEvent has drop, store { + new_allow_direct_transfers: bool, + } /////////////////////////////////////////////////////////////////////////// /// Basic account creation methods. @@ -22,6 +40,8 @@ module aptos_framework::aptos_account { coin::register(&signer); } + /// Convenient function to transfer APT to a recipient account that might not exist. + /// This would create the recipient account first, which also registers it to receive APT, before transferring. public entry fun transfer(source: &signer, to: address, amount: u64) { if (!account::exists_at(to)) { create_account(to) @@ -29,6 +49,28 @@ module aptos_framework::aptos_account { coin::transfer(source, to, amount) } + /// Convenient function to transfer a custom CoinType to a recipient account that might not exist. + /// This would create the recipient account first and register it to receive the CoinType, before transferring. + public entry fun transfer_coins(from: &signer, to: address, amount: u64) acquires DirectTransferConfig { + deposit_coins(to, coin::withdraw(from, amount)); + } + + /// Convenient function to deposit a custom CoinType into a recipient account that might not exist. + /// This would create the recipient account first and register it to receive the CoinType, before transferring. + public fun deposit_coins(to: address, coins: Coin) acquires DirectTransferConfig { + if (!account::exists_at(to)) { + create_account(to); + }; + if (!coin::is_account_registered(to)) { + assert!( + can_receive_direct_coin_transfers(to), + error::permission_denied(EACCOUNT_DOES_NOT_ACCEPT_DIRECT_COIN_TRANSFERS), + ); + coin::register(&account::create_signer(to)); + }; + coin::deposit(to, coins) + } + public fun assert_account_exists(addr: address) { assert!(account::exists_at(addr), error::not_found(EACCOUNT_NOT_FOUND)); } @@ -38,26 +80,137 @@ module aptos_framework::aptos_account { assert!(coin::is_account_registered(addr), error::not_found(EACCOUNT_NOT_REGISTERED_FOR_APT)); } - #[test(alice = @0xa11ce, core = @0x1)] - public fun test_transfer(alice: signer, core: signer) { - use std::signer; - use aptos_std::from_bcs; + /// Set whether `account` can receive direct transfers of coins that they have not explicitly registered to receive. + public entry fun set_allow_direct_coin_transfers(account: &signer, allow: bool) acquires DirectTransferConfig { + let addr = signer::address_of(account); + if (exists(addr)) { + let direct_transfer_config = borrow_global_mut(addr); + // Short-circuit to avoid emitting an event if direct transfer config is not changing. + if (direct_transfer_config.allow_arbitrary_coin_transfers != allow) { + return + }; + + direct_transfer_config.allow_arbitrary_coin_transfers = allow; + emit_event( + &mut direct_transfer_config.update_coin_transfer_events, + DirectCoinTransferConfigUpdatedEvent { new_allow_direct_transfers: allow }); + } else { + let direct_transfer_config = DirectTransferConfig { + allow_arbitrary_coin_transfers: allow, + update_coin_transfer_events: new_event_handle(account), + }; + emit_event( + &mut direct_transfer_config.update_coin_transfer_events, + DirectCoinTransferConfigUpdatedEvent { new_allow_direct_transfers: allow }); + move_to(account, direct_transfer_config); + }; + } + + /// Return true if `account` can receive direct transfers of coins that they have not explicitly registered to + /// receive. + /// + /// By default, this returns true if an account has not explicitly set whether the can receive direct transfers. + public fun can_receive_direct_coin_transfers(account: address): bool acquires DirectTransferConfig { + !exists(account) || + borrow_global(account).allow_arbitrary_coin_transfers + } + + #[test_only] + use aptos_std::from_bcs; + #[test_only] + use std::string::utf8; + use aptos_framework::coin::Coin; + #[test_only] + use aptos_framework::account::create_account_for_test; + + #[test_only] + struct FakeCoin {} + #[test(alice = @0xa11ce, core = @0x1)] + public fun test_transfer(alice: &signer, core: &signer) { let bob = from_bcs::to_address(x"0000000000000000000000000000000000000000000000000000000000000b0b"); let carol = from_bcs::to_address(x"00000000000000000000000000000000000000000000000000000000000ca501"); - let (burn_cap, mint_cap) = aptos_framework::aptos_coin::initialize_for_test(&core); - create_account(signer::address_of(&alice)); - coin::deposit(signer::address_of(&alice), coin::mint(10000, &mint_cap)); - transfer(&alice, bob, 500); + let (burn_cap, mint_cap) = aptos_framework::aptos_coin::initialize_for_test(core); + create_account(signer::address_of(alice)); + coin::deposit(signer::address_of(alice), coin::mint(10000, &mint_cap)); + transfer(alice, bob, 500); assert!(coin::balance(bob) == 500, 0); - transfer(&alice, carol, 500); + transfer(alice, carol, 500); assert!(coin::balance(carol) == 500, 1); - transfer(&alice, carol, 1500); + transfer(alice, carol, 1500); assert!(coin::balance(carol) == 2000, 2); coin::destroy_burn_cap(burn_cap); coin::destroy_mint_cap(mint_cap); - let _bob = bob; + } + + #[test(from = @0x1, to = @0x12)] + public fun test_direct_coin_transfers(from: &signer, to: &signer) acquires DirectTransferConfig { + let (burn_cap, freeze_cap, mint_cap) = coin::initialize( + from, + utf8(b"FC"), + utf8(b"FC"), + 10, + true, + ); + create_account_for_test(signer::address_of(from)); + create_account_for_test(signer::address_of(to)); + deposit_coins(signer::address_of(from), coin::mint(1000, &mint_cap)); + // Recipient account did not explicit register for the coin. + let to_addr = signer::address_of(to); + transfer_coins(from, to_addr, 500); + assert!(coin::balance(to_addr) == 500, 0); + + coin::destroy_burn_cap(burn_cap); + coin::destroy_mint_cap(mint_cap); + coin::destroy_freeze_cap(freeze_cap); + } + + #[test(from = @0x1, to = @0x12)] + public fun test_direct_coin_transfers_with_explicit_direct_coin_transfer_config( + from: &signer, to: &signer) acquires DirectTransferConfig { + let (burn_cap, freeze_cap, mint_cap) = coin::initialize( + from, + utf8(b"FC"), + utf8(b"FC"), + 10, + true, + ); + create_account_for_test(signer::address_of(from)); + create_account_for_test(signer::address_of(to)); + set_allow_direct_coin_transfers(from, true); + deposit_coins(signer::address_of(from), coin::mint(1000, &mint_cap)); + // Recipient account did not explicit register for the coin. + let to_addr = signer::address_of(to); + transfer_coins(from, to_addr, 500); + assert!(coin::balance(to_addr) == 500, 0); + + coin::destroy_burn_cap(burn_cap); + coin::destroy_mint_cap(mint_cap); + coin::destroy_freeze_cap(freeze_cap); + } + + #[test(from = @0x1, to = @0x12)] + #[expected_failure(abort_code = 0x50003, location = Self)] + public fun test_direct_coin_transfers_fail_if_recipient_opted_out( + from: &signer, to: &signer) acquires DirectTransferConfig { + let (burn_cap, freeze_cap, mint_cap) = coin::initialize( + from, + utf8(b"FC"), + utf8(b"FC"), + 10, + true, + ); + create_account_for_test(signer::address_of(from)); + create_account_for_test(signer::address_of(to)); + set_allow_direct_coin_transfers(from, false); + deposit_coins(signer::address_of(from), coin::mint(1000, &mint_cap)); + // This should fail as the to account has explicitly opted out of receiving arbitrary coins. + transfer_coins(from, signer::address_of(to), 500); + + coin::destroy_burn_cap(burn_cap); + coin::destroy_mint_cap(mint_cap); + coin::destroy_freeze_cap(freeze_cap); } } 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 bda94e758d985..21425d0980e4a 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 @@ -81,11 +81,26 @@ pub enum EntryFunctionCall { auth_key: AccountAddress, }, + /// Set whether `account` can receive direct transfers of coins that they have not explicitly registered to receive. + AptosAccountSetAllowDirectCoinTransfers { + allow: bool, + }, + + /// Convenient function to transfer APT to a recipient account that might not exist. + /// This would create the recipient account first, which also registers it to receive APT, before transferring. AptosAccountTransfer { to: AccountAddress, amount: u64, }, + /// Convenient function to transfer a custom CoinType to a recipient account that might not exist. + /// This would create the recipient account first and register it to receive the CoinType, before transferring. + AptosAccountTransferCoins { + coin_type: TypeTag, + to: AccountAddress, + amount: u64, + }, + /// Only callable in tests and testnets where the core resources account exists. /// Claim the delegated mint capability and destroy the delegated token. AptosCoinClaimMintCapability {}, @@ -508,7 +523,15 @@ impl EntryFunctionCall { cap_update_table, ), AptosAccountCreateAccount { auth_key } => aptos_account_create_account(auth_key), + AptosAccountSetAllowDirectCoinTransfers { allow } => { + aptos_account_set_allow_direct_coin_transfers(allow) + } AptosAccountTransfer { to, amount } => aptos_account_transfer(to, amount), + AptosAccountTransferCoins { + coin_type, + to, + amount, + } => aptos_account_transfer_coins(coin_type, to, amount), AptosCoinClaimMintCapability {} => aptos_coin_claim_mint_capability(), AptosCoinDelegateMintCapability { to } => aptos_coin_delegate_mint_capability(to), AptosCoinMint { dst_addr, amount } => aptos_coin_mint(dst_addr, amount), @@ -874,6 +897,24 @@ pub fn aptos_account_create_account(auth_key: AccountAddress) -> TransactionPayl )) } +/// Set whether `account` can receive direct transfers of coins that they have not explicitly registered to receive. +pub fn aptos_account_set_allow_direct_coin_transfers(allow: bool) -> 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!("aptos_account").to_owned(), + ), + ident_str!("set_allow_direct_coin_transfers").to_owned(), + vec![], + vec![bcs::to_bytes(&allow).unwrap()], + )) +} + +/// Convenient function to transfer APT to a recipient account that might not exist. +/// This would create the recipient account first, which also registers it to receive APT, before transferring. pub fn aptos_account_transfer(to: AccountAddress, amount: u64) -> TransactionPayload { TransactionPayload::EntryFunction(EntryFunction::new( ModuleId::new( @@ -889,6 +930,27 @@ pub fn aptos_account_transfer(to: AccountAddress, amount: u64) -> TransactionPay )) } +/// Convenient function to transfer a custom CoinType to a recipient account that might not exist. +/// This would create the recipient account first and register it to receive the CoinType, before transferring. +pub fn aptos_account_transfer_coins( + coin_type: TypeTag, + to: AccountAddress, + amount: u64, +) -> 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!("aptos_account").to_owned(), + ), + ident_str!("transfer_coins").to_owned(), + vec![coin_type], + vec![bcs::to_bytes(&to).unwrap(), bcs::to_bytes(&amount).unwrap()], + )) +} + /// Only callable in tests and testnets where the core resources account exists. /// Claim the delegated mint capability and destroy the delegated token. pub fn aptos_coin_claim_mint_capability() -> TransactionPayload { @@ -2201,6 +2263,18 @@ mod decoder { } } + pub fn aptos_account_set_allow_direct_coin_transfers( + payload: &TransactionPayload, + ) -> Option { + if let TransactionPayload::EntryFunction(script) = payload { + Some(EntryFunctionCall::AptosAccountSetAllowDirectCoinTransfers { + allow: bcs::from_bytes(script.args().get(0)?).ok()?, + }) + } else { + None + } + } + pub fn aptos_account_transfer(payload: &TransactionPayload) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::AptosAccountTransfer { @@ -2212,6 +2286,18 @@ mod decoder { } } + pub fn aptos_account_transfer_coins(payload: &TransactionPayload) -> Option { + if let TransactionPayload::EntryFunction(script) = payload { + Some(EntryFunctionCall::AptosAccountTransferCoins { + coin_type: script.ty_args().get(0)?.clone(), + to: bcs::from_bytes(script.args().get(0)?).ok()?, + amount: bcs::from_bytes(script.args().get(1)?).ok()?, + }) + } else { + None + } + } + pub fn aptos_coin_claim_mint_capability( payload: &TransactionPayload, ) -> Option { @@ -2979,10 +3065,18 @@ static SCRIPT_FUNCTION_DECODER_MAP: once_cell::sync::Lazy Date: Fri, 16 Dec 2022 15:05:32 +0700 Subject: [PATCH 2/3] [Aptos Framework] Add batch versions of aptos_coin::transfer and transfer_coins --- .../aptos-framework/doc/aptos_account.md | 87 +++++++++++++++ .../sources/aptos_account.move | 90 +++++++++++++++- .../src/aptos_framework_sdk_builder.rs | 100 ++++++++++++++++++ 3 files changed, 275 insertions(+), 2 deletions(-) diff --git a/aptos-move/framework/aptos-framework/doc/aptos_account.md b/aptos-move/framework/aptos-framework/doc/aptos_account.md index 1a8adbf36c008..b814ca1c9515c 100644 --- a/aptos-move/framework/aptos-framework/doc/aptos_account.md +++ b/aptos-move/framework/aptos-framework/doc/aptos_account.md @@ -9,7 +9,9 @@ - [Struct `DirectCoinTransferConfigUpdatedEvent`](#0x1_aptos_account_DirectCoinTransferConfigUpdatedEvent) - [Constants](#@Constants_0) - [Function `create_account`](#0x1_aptos_account_create_account) +- [Function `batch_transfer`](#0x1_aptos_account_batch_transfer) - [Function `transfer`](#0x1_aptos_account_transfer) +- [Function `batch_transfer_coins`](#0x1_aptos_account_batch_transfer_coins) - [Function `transfer_coins`](#0x1_aptos_account_transfer_coins) - [Function `deposit_coins`](#0x1_aptos_account_deposit_coins) - [Function `assert_account_exists`](#0x1_aptos_account_assert_account_exists) @@ -142,6 +144,16 @@ Account is not registered to receive APT. + + +The lengths of the recipients and amounts lists don't match. + + +
const EMISMATCHING_RECIPIENTS_AND_AMOUNTS_LENGTH: u64 = 5;
+
+ + + ## Function `create_account` @@ -166,6 +178,43 @@ Basic account creation methods. + + + + +## Function `batch_transfer` + +Batch version of APT transfer. + + +
public entry fun batch_transfer(source: &signer, recipients: vector<address>, amounts: vector<u64>)
+
+ + + +
+Implementation + + +
public entry fun batch_transfer(source: &signer, recipients: vector<address>, amounts: vector<u64>) {
+    let recipients_len = vector::length(&recipients);
+    assert!(
+        recipients_len == vector::length(&amounts),
+        error::invalid_argument(EMISMATCHING_RECIPIENTS_AND_AMOUNTS_LENGTH),
+    );
+
+    let i = 0;
+    while (i < recipients_len) {
+        let to = *vector::borrow(&recipients, i);
+        let amount = *vector::borrow(&amounts, i);
+        transfer(source, to, amount);
+        i = i + 1;
+    };
+}
+
+ + +
@@ -195,6 +244,44 @@ This would create the recipient account first, which also registers it to receiv + + + + +## Function `batch_transfer_coins` + +Batch version of transfer_coins. + + +
public entry fun batch_transfer_coins<CoinType>(from: &signer, recipients: vector<address>, amounts: vector<u64>)
+
+ + + +
+Implementation + + +
public entry fun batch_transfer_coins<CoinType>(
+    from: &signer, recipients: vector<address>, amounts: vector<u64>) acquires DirectTransferConfig {
+    let recipients_len = vector::length(&recipients);
+    assert!(
+        recipients_len == vector::length(&amounts),
+        error::invalid_argument(EMISMATCHING_RECIPIENTS_AND_AMOUNTS_LENGTH),
+    );
+
+    let i = 0;
+    while (i < recipients_len) {
+        let to = *vector::borrow(&recipients, i);
+        let amount = *vector::borrow(&amounts, i);
+        transfer_coins<CoinType>(from, to, amount);
+        i = i + 1;
+    };
+}
+
+ + +
diff --git a/aptos-move/framework/aptos-framework/sources/aptos_account.move b/aptos-move/framework/aptos-framework/sources/aptos_account.move index 6d1d8b9020c5b..079e8742c982e 100644 --- a/aptos-move/framework/aptos-framework/sources/aptos_account.move +++ b/aptos-move/framework/aptos-framework/sources/aptos_account.move @@ -1,10 +1,11 @@ module aptos_framework::aptos_account { use aptos_framework::account::{Self, new_event_handle}; use aptos_framework::aptos_coin::AptosCoin; - use aptos_framework::coin; + use aptos_framework::coin::{Self, Coin}; use aptos_framework::event::{EventHandle, emit_event}; use std::error; use std::signer; + use std::vector; friend aptos_framework::genesis; friend aptos_framework::resource_account; @@ -17,6 +18,8 @@ module aptos_framework::aptos_account { const EACCOUNT_DOES_NOT_ACCEPT_DIRECT_COIN_TRANSFERS: u64 = 3; /// Account opted out of directly receiving NFT tokens. const EACCOUNT_DOES_NOT_ACCEPT_DIRECT_TOKEN_TRANSFERS: u64 = 4; + /// The lengths of the recipients and amounts lists don't match. + const EMISMATCHING_RECIPIENTS_AND_AMOUNTS_LENGTH: u64 = 5; /// Configuration for whether an account can receive direct transfers of coins that they have not registered. /// @@ -40,6 +43,23 @@ module aptos_framework::aptos_account { coin::register(&signer); } + /// Batch version of APT transfer. + public entry fun batch_transfer(source: &signer, recipients: vector
, amounts: vector) { + let recipients_len = vector::length(&recipients); + assert!( + recipients_len == vector::length(&amounts), + error::invalid_argument(EMISMATCHING_RECIPIENTS_AND_AMOUNTS_LENGTH), + ); + + let i = 0; + while (i < recipients_len) { + let to = *vector::borrow(&recipients, i); + let amount = *vector::borrow(&amounts, i); + transfer(source, to, amount); + i = i + 1; + }; + } + /// Convenient function to transfer APT to a recipient account that might not exist. /// This would create the recipient account first, which also registers it to receive APT, before transferring. public entry fun transfer(source: &signer, to: address, amount: u64) { @@ -49,6 +69,24 @@ module aptos_framework::aptos_account { coin::transfer(source, to, amount) } + /// Batch version of transfer_coins. + public entry fun batch_transfer_coins( + from: &signer, recipients: vector
, amounts: vector) acquires DirectTransferConfig { + let recipients_len = vector::length(&recipients); + assert!( + recipients_len == vector::length(&amounts), + error::invalid_argument(EMISMATCHING_RECIPIENTS_AND_AMOUNTS_LENGTH), + ); + + let i = 0; + while (i < recipients_len) { + let to = *vector::borrow(&recipients, i); + let amount = *vector::borrow(&amounts, i); + transfer_coins(from, to, amount); + i = i + 1; + }; + } + /// Convenient function to transfer a custom CoinType to a recipient account that might not exist. /// This would create the recipient account first and register it to receive the CoinType, before transferring. public entry fun transfer_coins(from: &signer, to: address, amount: u64) acquires DirectTransferConfig { @@ -119,7 +157,6 @@ module aptos_framework::aptos_account { use aptos_std::from_bcs; #[test_only] use std::string::utf8; - use aptos_framework::coin::Coin; #[test_only] use aptos_framework::account::create_account_for_test; @@ -145,6 +182,26 @@ module aptos_framework::aptos_account { coin::destroy_mint_cap(mint_cap); } + #[test(from = @0x123, core = @0x1, recipient_1 = @0x124, recipient_2 = @0x125)] + public fun test_batch_transfer(from: &signer, core: &signer, recipient_1: &signer, recipient_2: &signer) { + let (burn_cap, mint_cap) = aptos_framework::aptos_coin::initialize_for_test(core); + create_account(signer::address_of(from)); + let recipient_1_addr = signer::address_of(recipient_1); + let recipient_2_addr = signer::address_of(recipient_2); + create_account(recipient_1_addr); + create_account(recipient_2_addr); + coin::deposit(signer::address_of(from), coin::mint(10000, &mint_cap)); + batch_transfer( + from, + vector[recipient_1_addr, recipient_2_addr], + vector[100, 500], + ); + assert!(coin::balance(recipient_1_addr) == 100, 0); + assert!(coin::balance(recipient_2_addr) == 500, 1); + coin::destroy_burn_cap(burn_cap); + coin::destroy_mint_cap(mint_cap); + } + #[test(from = @0x1, to = @0x12)] public fun test_direct_coin_transfers(from: &signer, to: &signer) acquires DirectTransferConfig { let (burn_cap, freeze_cap, mint_cap) = coin::initialize( @@ -167,6 +224,35 @@ module aptos_framework::aptos_account { coin::destroy_freeze_cap(freeze_cap); } + #[test(from = @0x1, recipient_1 = @0x124, recipient_2 = @0x125)] + public fun test_batch_transfer_coins( + from: &signer, recipient_1: &signer, recipient_2: &signer) acquires DirectTransferConfig { + let (burn_cap, freeze_cap, mint_cap) = coin::initialize( + from, + utf8(b"FC"), + utf8(b"FC"), + 10, + true, + ); + create_account_for_test(signer::address_of(from)); + let recipient_1_addr = signer::address_of(recipient_1); + let recipient_2_addr = signer::address_of(recipient_2); + create_account_for_test(recipient_1_addr); + create_account_for_test(recipient_2_addr); + deposit_coins(signer::address_of(from), coin::mint(1000, &mint_cap)); + batch_transfer_coins( + from, + vector[recipient_1_addr, recipient_2_addr], + vector[100, 500], + ); + assert!(coin::balance(recipient_1_addr) == 100, 0); + assert!(coin::balance(recipient_2_addr) == 500, 1); + + coin::destroy_burn_cap(burn_cap); + coin::destroy_mint_cap(mint_cap); + coin::destroy_freeze_cap(freeze_cap); + } + #[test(from = @0x1, to = @0x12)] public fun test_direct_coin_transfers_with_explicit_direct_coin_transfer_config( from: &signer, to: &signer) acquires DirectTransferConfig { 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 21425d0980e4a..c1445729588bd 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 @@ -76,6 +76,19 @@ pub enum EntryFunctionCall { cap_update_table: Vec, }, + /// Batch version of APT transfer. + AptosAccountBatchTransfer { + recipients: Vec, + amounts: Vec, + }, + + /// Batch version of transfer_coins. + AptosAccountBatchTransferCoins { + coin_type: TypeTag, + recipients: Vec, + amounts: Vec, + }, + /// Basic account creation methods. AptosAccountCreateAccount { auth_key: AccountAddress, @@ -522,6 +535,15 @@ impl EntryFunctionCall { cap_rotate_key, cap_update_table, ), + AptosAccountBatchTransfer { + recipients, + amounts, + } => aptos_account_batch_transfer(recipients, amounts), + AptosAccountBatchTransferCoins { + coin_type, + recipients, + amounts, + } => aptos_account_batch_transfer_coins(coin_type, recipients, amounts), AptosAccountCreateAccount { auth_key } => aptos_account_create_account(auth_key), AptosAccountSetAllowDirectCoinTransfers { allow } => { aptos_account_set_allow_direct_coin_transfers(allow) @@ -881,6 +903,51 @@ pub fn account_rotate_authentication_key( )) } +/// Batch version of APT transfer. +pub fn aptos_account_batch_transfer( + recipients: Vec, + amounts: 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!("aptos_account").to_owned(), + ), + ident_str!("batch_transfer").to_owned(), + vec![], + vec![ + bcs::to_bytes(&recipients).unwrap(), + bcs::to_bytes(&amounts).unwrap(), + ], + )) +} + +/// Batch version of transfer_coins. +pub fn aptos_account_batch_transfer_coins( + coin_type: TypeTag, + recipients: Vec, + amounts: 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!("aptos_account").to_owned(), + ), + ident_str!("batch_transfer_coins").to_owned(), + vec![coin_type], + vec![ + bcs::to_bytes(&recipients).unwrap(), + bcs::to_bytes(&amounts).unwrap(), + ], + )) +} + /// Basic account creation methods. pub fn aptos_account_create_account(auth_key: AccountAddress) -> TransactionPayload { TransactionPayload::EntryFunction(EntryFunction::new( @@ -2253,6 +2320,31 @@ mod decoder { } } + pub fn aptos_account_batch_transfer(payload: &TransactionPayload) -> Option { + if let TransactionPayload::EntryFunction(script) = payload { + Some(EntryFunctionCall::AptosAccountBatchTransfer { + recipients: bcs::from_bytes(script.args().get(0)?).ok()?, + amounts: bcs::from_bytes(script.args().get(1)?).ok()?, + }) + } else { + None + } + } + + pub fn aptos_account_batch_transfer_coins( + payload: &TransactionPayload, + ) -> Option { + if let TransactionPayload::EntryFunction(script) = payload { + Some(EntryFunctionCall::AptosAccountBatchTransferCoins { + coin_type: script.ty_args().get(0)?.clone(), + recipients: bcs::from_bytes(script.args().get(0)?).ok()?, + amounts: bcs::from_bytes(script.args().get(1)?).ok()?, + }) + } else { + None + } + } + pub fn aptos_account_create_account(payload: &TransactionPayload) -> Option { if let TransactionPayload::EntryFunction(script) = payload { Some(EntryFunctionCall::AptosAccountCreateAccount { @@ -3061,6 +3153,14 @@ static SCRIPT_FUNCTION_DECODER_MAP: once_cell::sync::Lazy Date: Sun, 18 Dec 2022 08:50:45 +0700 Subject: [PATCH 3/3] update --- .../framework/aptos-framework/doc/aptos_account.md | 2 +- .../aptos-framework/sources/aptos_account.move | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/aptos-move/framework/aptos-framework/doc/aptos_account.md b/aptos-move/framework/aptos-framework/doc/aptos_account.md index b814ca1c9515c..446b3f062dee5 100644 --- a/aptos-move/framework/aptos-framework/doc/aptos_account.md +++ b/aptos-move/framework/aptos-framework/doc/aptos_account.md @@ -416,7 +416,7 @@ Set whether account can receiv if (exists<DirectTransferConfig>(addr)) { let direct_transfer_config = borrow_global_mut<DirectTransferConfig>(addr); // Short-circuit to avoid emitting an event if direct transfer config is not changing. - if (direct_transfer_config.allow_arbitrary_coin_transfers != allow) { + if (direct_transfer_config.allow_arbitrary_coin_transfers == allow) { return }; diff --git a/aptos-move/framework/aptos-framework/sources/aptos_account.move b/aptos-move/framework/aptos-framework/sources/aptos_account.move index 079e8742c982e..ff6bc25a3e53f 100644 --- a/aptos-move/framework/aptos-framework/sources/aptos_account.move +++ b/aptos-move/framework/aptos-framework/sources/aptos_account.move @@ -124,7 +124,7 @@ module aptos_framework::aptos_account { if (exists(addr)) { let direct_transfer_config = borrow_global_mut(addr); // Short-circuit to avoid emitting an event if direct transfer config is not changing. - if (direct_transfer_config.allow_arbitrary_coin_transfers != allow) { + if (direct_transfer_config.allow_arbitrary_coin_transfers == allow) { return }; @@ -253,6 +253,18 @@ module aptos_framework::aptos_account { coin::destroy_freeze_cap(freeze_cap); } + #[test(user = @0x123)] + public fun test_set_allow_direct_coin_transfers(user: &signer) acquires DirectTransferConfig { + let addr = signer::address_of(user); + create_account_for_test(addr); + set_allow_direct_coin_transfers(user, true); + assert!(can_receive_direct_coin_transfers(addr), 0); + set_allow_direct_coin_transfers(user, false); + assert!(!can_receive_direct_coin_transfers(addr), 1); + set_allow_direct_coin_transfers(user, true); + assert!(can_receive_direct_coin_transfers(addr), 2); + } + #[test(from = @0x1, to = @0x12)] public fun test_direct_coin_transfers_with_explicit_direct_coin_transfer_config( from: &signer, to: &signer) acquires DirectTransferConfig {