diff --git a/Makefile b/Makefile index de57e49..3b68f71 100644 --- a/Makefile +++ b/Makefile @@ -38,6 +38,7 @@ examples_cli: poetry run python -m examples.hello_blockchain # poetry run python -m examples.large_package_publisher CURRENTLY BROKEN -- OUT OF GAS poetry run python -m examples.multisig + poetry run python -m examples.object_code_deployment poetry run python -m examples.your_coin integration_test: diff --git a/aptos_sdk/package_publisher.py b/aptos_sdk/package_publisher.py index c436718..ac7fcad 100644 --- a/aptos_sdk/package_publisher.py +++ b/aptos_sdk/package_publisher.py @@ -2,7 +2,8 @@ # SPDX-License-Identifier: Apache-2.0 import os -from typing import List +from enum import Enum +from typing import List, Optional import tomli @@ -20,6 +21,15 @@ "0xfa3911d7715238b2e3bd5b26b6a35e11ffa16cff318bc11471e84eccee8bd291" ) +# Domain separator for the code object address derivation +OBJECT_CODE_DEPLOYMENT_DOMAIN_SEPARATOR = b"aptos_framework::object_code_deployment" + + +class PublishMode(Enum): + ACCOUNT_DEPLOY = "ACCOUNT_DEPLOY" + OBJECT_DEPLOY = "OBJECT_DEPLOY" + OBJECT_UPGRADE = "OBJECT_UPGRADE" + class PackagePublisher: """A wrapper around publishing packages.""" @@ -51,11 +61,62 @@ async def publish_package( ) return await self.client.submit_bcs_transaction(signed_transaction) + async def publish_package_to_object( + self, sender: Account, package_metadata: bytes, modules: List[bytes] + ) -> str: + transaction_arguments = [ + TransactionArgument(package_metadata, Serializer.to_bytes), + TransactionArgument( + modules, Serializer.sequence_serializer(Serializer.to_bytes) + ), + ] + + payload = EntryFunction.natural( + "0x1::object_code_deployment", + "publish", + [], + transaction_arguments, + ) + + signed_transaction = await self.client.create_bcs_signed_transaction( + sender, TransactionPayload(payload) + ) + return await self.client.submit_bcs_transaction(signed_transaction) + + async def upgrade_package_object( + self, + sender: Account, + package_metadata: bytes, + modules: List[bytes], + object_address: AccountAddress, + ) -> str: + transaction_arguments = [ + TransactionArgument(package_metadata, Serializer.to_bytes), + TransactionArgument( + modules, Serializer.sequence_serializer(Serializer.to_bytes) + ), + TransactionArgument(object_address, Serializer.struct), + ] + + payload = EntryFunction.natural( + "0x1::object_code_deployment", + "upgrade", + [], + transaction_arguments, + ) + + signed_transaction = await self.client.create_bcs_signed_transaction( + sender, TransactionPayload(payload) + ) + return await self.client.submit_bcs_transaction(signed_transaction) + async def publish_package_in_path( self, sender: Account, package_dir: str, large_package_address: AccountAddress = MODULE_ADDRESS, + publish_mode: PublishMode = PublishMode.ACCOUNT_DEPLOY, + code_object: Optional[AccountAddress] = None, ) -> List[str]: with open(os.path.join(package_dir, "Move.toml"), "rb") as f: data = tomli.load(f) @@ -76,16 +137,55 @@ async def publish_package_in_path( metadata_path = os.path.join(package_build_dir, "package-metadata.bcs") with open(metadata_path, "rb") as f: metadata = f.read() - return await self.publish_package_experimental( - sender, metadata, modules, large_package_address + + # If the package size is larger than a single transaction limit, use chunked publish. + if self.is_large_package(metadata, modules): + return await self.chunked_package_publish( + sender, metadata, modules, large_package_address, publish_mode + ) + + # If the deployment can fit into a single transaction, use the normal package publisher + if publish_mode == PublishMode.ACCOUNT_DEPLOY: + txn_hash = await self.publish_package(sender, metadata, modules) + elif publish_mode == PublishMode.OBJECT_DEPLOY: + txn_hash = await self.publish_package_to_object(sender, metadata, modules) + elif publish_mode == PublishMode.OBJECT_UPGRADE: + if code_object is None: + raise ValueError("code_object must be provided for OBJECT_UPGRADE mode") + txn_hash = await self.upgrade_package_object( + sender, metadata, modules, code_object + ) + else: + raise ValueError(f"Unexpected publish mode: {publish_mode}") + + return [txn_hash] + + async def derive_object_address( + self, publisher_address: AccountAddress + ) -> AccountAddress: + sequence_number = await self.client.account_sequence_number(publisher_address) + return self.create_object_deployment_address( + publisher_address, sequence_number + 1 ) - async def publish_package_experimental( + @staticmethod + def create_object_deployment_address( + creator_address: AccountAddress, creator_sequence_number: int + ) -> AccountAddress: + ser = Serializer() + ser.to_bytes(OBJECT_CODE_DEPLOYMENT_DOMAIN_SEPARATOR) + ser.u64(creator_sequence_number) + seed = ser.output() + + return AccountAddress.for_named_object(creator_address, seed) + + async def chunked_package_publish( self, sender: Account, package_metadata: bytes, modules: List[bytes], large_package_address: AccountAddress = MODULE_ADDRESS, + publish_mode: PublishMode = PublishMode.ACCOUNT_DEPLOY, ) -> List[str]: """ Chunks the package_metadata and modules across as many transactions as necessary. @@ -94,13 +194,6 @@ async def publish_package_experimental( optimistic transaction batching. The batching tries to place as much data in a transaction before moving to the chunk to the next transaction. """ - # If this can fit into a single transaction, use the normal package publisher - total_size = len(package_metadata) - for module in modules: - total_size += len(module) - if total_size < MAX_TRANSACTION_SIZE: - txn_hash = await self.publish_package(sender, package_metadata, modules) - return [txn_hash] # Chunk the metadata and insert it into payloads. The last chunk may be small enough # to be placed with other data. This may also be the only chunk. @@ -194,6 +287,17 @@ def create_large_package_publishing_payload( return TransactionPayload(payload) + @staticmethod + def is_large_package( + package_metadata: bytes, + modules: List[bytes], + ) -> bool: + total_size = len(package_metadata) + for module in modules: + total_size += len(module) + + return total_size >= MAX_TRANSACTION_SIZE + @staticmethod def create_chunks(data: bytes) -> List[bytes]: chunks: List[bytes] = [] diff --git a/examples/object_code_deployment.py b/examples/object_code_deployment.py new file mode 100644 index 0000000..c6c467e --- /dev/null +++ b/examples/object_code_deployment.py @@ -0,0 +1,79 @@ +# Copyright © Aptos Foundation +# SPDX-License-Identifier: Apache-2.0 + +import asyncio +import os +import sys + +from aptos_sdk.account import Account +from aptos_sdk.aptos_cli_wrapper import AptosCLIWrapper +from aptos_sdk.async_client import FaucetClient, RestClient +from aptos_sdk.package_publisher import MODULE_ADDRESS, PackagePublisher, PublishMode + +from .common import APTOS_CORE_PATH, FAUCET_URL, NODE_URL + + +async def main(package_dir): + rest_client = RestClient(NODE_URL) + faucet_client = FaucetClient(FAUCET_URL, rest_client) + package_publisher = PackagePublisher(rest_client) + alice = Account.generate() + + print("\n=== Publisher Address ===") + print(f"Alice: {alice.address()}") + + await faucet_client.fund_account(alice.address(), 100_000_000) + + print("\n=== Initial Coin Balance ===") + alice_balance = await rest_client.account_balance(alice.address()) + print(f"Alice: {alice_balance}") + + # The object address is derived from publisher's address and sequence number. + code_object_address = await package_publisher.derive_object_address(alice.address()) + module_name = "hello_blockchain" + + print("\nCompiling package...") + if AptosCLIWrapper.does_cli_exist(): + AptosCLIWrapper.compile_package(package_dir, {module_name: code_object_address}) + else: + print(f"Address of the object to be created: {code_object_address}") + input( + "\nUpdate the module with the derived code object address, compile, and press enter." + ) + + # Deploy package to code object. + print("\n=== Object Code Deployment ===") + deploy_txn_hash = await package_publisher.publish_package_in_path( + alice, package_dir, MODULE_ADDRESS, publish_mode=PublishMode.OBJECT_DEPLOY + ) + + print(f"Tx submitted: {deploy_txn_hash[0]}") + await rest_client.wait_for_transaction(deploy_txn_hash[0]) + print(f"Package deployed to object {code_object_address}") + + print("\n=== Object Code Upgrade ===") + upgrade_txn_hash = await package_publisher.publish_package_in_path( + alice, + package_dir, + MODULE_ADDRESS, + publish_mode=PublishMode.OBJECT_UPGRADE, + code_object=code_object_address, + ) + print(f"Tx submitted: {upgrade_txn_hash[0]}") + await rest_client.wait_for_transaction(upgrade_txn_hash[0]) + print(f"Package in object {code_object_address} upgraded") + await rest_client.close() + + +if __name__ == "__main__": + if len(sys.argv) == 2: + package_dir = sys.argv[1] + else: + package_dir = os.path.join( + APTOS_CORE_PATH, + "aptos-move", + "move-examples", + "hello_blockchain", + ) + + asyncio.run(main(package_dir))