From 80c0e36c9458fd5968bd07ea5a13f5177aada703 Mon Sep 17 00:00:00 2001 From: runtianz Date: Wed, 30 Oct 2024 14:25:02 -0700 Subject: [PATCH] add intent code --- .gitignore | 1 + aptos.nix | 37 +++++ intent-framework/Move.toml | 17 +++ intent-framework/shell.nix | 28 ++++ .../sources/fungible_asset_intent.move | 134 ++++++++++++++++++ intent-framework/sources/intent.move | 113 +++++++++++++++ 6 files changed, 330 insertions(+) create mode 100644 .gitignore create mode 100644 aptos.nix create mode 100644 intent-framework/Move.toml create mode 100644 intent-framework/shell.nix create mode 100644 intent-framework/sources/fungible_asset_intent.move create mode 100644 intent-framework/sources/intent.move diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8561411 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +intent-framework/build/ \ No newline at end of file diff --git a/aptos.nix b/aptos.nix new file mode 100644 index 0000000..71839e4 --- /dev/null +++ b/aptos.nix @@ -0,0 +1,37 @@ +{ stdenv, fetchurl, lib, unzip }: + +let + os = + if stdenv.isDarwin then "MacOSX" + else if stdenv.isLinux then "Ubuntu" + else throw "Unsupported platform ${stdenv.system}"; + + sha256 = if os == "MacOSX" then "sha256-6uVxfgcFgxgwF9vwQwaTt23IbV5vNACmjN9BO6TSQ20=" + else "sha256-L/rpf/94WcK4Im5mgfFXypD16U+iVqkGmeOShx7MrH0="; + +in stdenv.mkDerivation rec { + pname = "aptos-cli"; + version = "3.1.0"; + + src = fetchurl { + url = "https://github.com/aptos-labs/aptos-core/releases/download/${pname}-v${version}/${pname}-${version}-${os}-x86_64.zip"; + sha256 = sha256; + }; + + buildInputs = [ unzip ]; + + unpackPhase = '' + unzip $src + ''; + + installPhase = '' + mkdir -p $out/bin + cp aptos $out/bin/aptos + ''; + + meta = with lib; { + description = "Aptos CLI"; + homepage = "https://github.com/aptos-labs/aptos-core"; + platforms = [ "x86_64-darwin" "aarch64-darwin" "x86_64-linux" "aarch64-linux" ]; + }; +} \ No newline at end of file diff --git a/intent-framework/Move.toml b/intent-framework/Move.toml new file mode 100644 index 0000000..661e7bf --- /dev/null +++ b/intent-framework/Move.toml @@ -0,0 +1,17 @@ +[package] +name = "aptos-intent" +version = "1.0.0" +authors = [] + +[addresses] +aptos_intent = "_" + +[dev-addresses] +aptos_intent = "0x123" + +[dependencies.AptosFramework] +git = "https://github.com/aptos-labs/aptos-core.git" +rev = "main" +subdir = "aptos-move/framework/aptos-framework" + +[dev-dependencies] diff --git a/intent-framework/shell.nix b/intent-framework/shell.nix new file mode 100644 index 0000000..6e18426 --- /dev/null +++ b/intent-framework/shell.nix @@ -0,0 +1,28 @@ +with import { }; + +pkgs.mkShell { + buildInputs = [ + jq + nodePackages.nodemon + nodejs_18 + (callPackage ../aptos.nix { }) + ]; + + shellHook = '' + alias gen="aptos init" + + test() { + nodemon \ + --ignore build/* \ + --ext move \ + --exec "aptos move test --dev --skip-fetch-latest-git-deps;" + } + + pub() { + local intent=0x$(aptos config show-profiles | jq -r '.Result.default.account') + aptos move publish \ + --named-addresses aptos_intent=$intent \ + --skip-fetch-latest-git-deps + } + ''; +} \ No newline at end of file diff --git a/intent-framework/sources/fungible_asset_intent.move b/intent-framework/sources/fungible_asset_intent.move new file mode 100644 index 0000000..12404cc --- /dev/null +++ b/intent-framework/sources/fungible_asset_intent.move @@ -0,0 +1,134 @@ +module aptos_intent::fungible_asset_intent { + use std::error; + use aptos_framework::fungible_asset::{Self, FungibleAsset, Metadata, FungibleStore}; + use aptos_intent::intent::{Self, TradeSession, TradeIntent}; + use aptos_framework::object::{Self, DeleteRef, ExtendRef, Object}; + use aptos_framework::primary_fungible_store; + + /// The token offered is not the desired fungible asset. + const ENOT_DESIRED_TOKEN: u64 = 0; + + /// The token offered does not meet amount requirement. + const EAMOUNT_NOT_MEET: u64 = 1; + + struct FungibleStoreManager has store { + extend_ref: ExtendRef, + delete_ref: DeleteRef, + } + + struct FungibleAssetLimitOrder has store, drop { + desired_metadata: Object, + desired_amount: u64, + issuer: address, + } + + struct FungibleAssetRecipientWitness has drop {} + + public fun create_fa_to_fa_intent( + source_fungible_asset: FungibleAsset, + desired_metadata: Object, + desired_amount: u64, + expiry_time: u64, + issuer: address, + ): Object> { + let coin_store_ref = object::create_object(issuer); + let extend_ref = object::generate_extend_ref(&coin_store_ref); + let delete_ref = object::generate_delete_ref(&coin_store_ref); + let transfer_ref = object::generate_transfer_ref(&coin_store_ref); + let linear_ref = object::generate_linear_transfer_ref(&transfer_ref); + object::transfer_with_ref(linear_ref, object::address_from_constructor_ref(&coin_store_ref)); + + fungible_asset::create_store(&coin_store_ref, fungible_asset::metadata_from_asset(&source_fungible_asset)); + fungible_asset::deposit( + object::object_from_constructor_ref(&coin_store_ref), + source_fungible_asset + ); + intent::create_intent( + FungibleStoreManager { extend_ref, delete_ref}, + FungibleAssetLimitOrder { desired_metadata, desired_amount, issuer }, + expiry_time, + issuer, + FungibleAssetRecipientWitness {}, + ) + } + + public fun start_fa_offering_session( + intent: Object> + ): (FungibleAsset, TradeSession) { + let (store_manager, session) = intent::start_intent_session(intent); + let FungibleStoreManager { extend_ref, delete_ref } = store_manager; + let store_signer = object::generate_signer_for_extending(&extend_ref); + let fa_store = object::object_from_delete_ref(&delete_ref); + let fa = fungible_asset::withdraw(&store_signer, fa_store, fungible_asset::balance(fa_store)); + fungible_asset::remove_store(&delete_ref); + object::delete(delete_ref); + (fa, session) + } + + public fun finish_fa_receiving_session( + session: TradeSession, + received_fa: FungibleAsset, + ) { + let argument = intent::get_argument(&session); + assert!( + fungible_asset::metadata_from_asset(&received_fa) == argument.desired_metadata, + error::invalid_argument(ENOT_DESIRED_TOKEN) + ); + assert!( + fungible_asset::amount(&received_fa) >= argument.desired_amount, + error::invalid_argument(EAMOUNT_NOT_MEET), + ); + + primary_fungible_store::deposit(argument.issuer, received_fa); + intent::finish_intent_session(session, FungibleAssetRecipientWitness {}) + } + + #[test( + aptos_framework = @0x1, + creator1 = @0xcafe, + creator2 = @0xcaff, + aaron = @0xface, + offerer = @0xbadd + )] + fun test_e2e_basic_flow( + aptos_framework: &signer, + creator1: &signer, + creator2: &signer, + aaron: &signer, + ) { + use aptos_framework::timestamp; + use aptos_framework::signer; + use aptos_framework::primary_fungible_store; + + timestamp::set_time_has_started_for_testing(aptos_framework); + let (mint_ref_1, _, burn_ref_1, _, test_token_1) = fungible_asset::create_fungible_asset(creator1); + let (creator_ref, metadata) = fungible_asset::create_test_token(creator2); + primary_fungible_store::init_test_metadata_with_primary_store_enabled(&creator_ref); + let mint_ref_2 = fungible_asset::generate_mint_ref(&creator_ref); + let test_token_2 = object::convert(metadata); + + let fa1 = fungible_asset::mint(&mint_ref_1, 10); + // Register intent to trade 10 FA1 into 5 FA2. + let intent = create_fa_to_fa_intent( + fa1, + test_token_2, + 5, + 1, + signer::address_of(aaron), + ); + + let (fa1, session) = start_fa_offering_session(intent); + + assert!(fungible_asset::metadata_from_asset(&fa1) == test_token_1, 1); + assert!(fungible_asset::amount(&fa1) == 10, 1); + + // Mint FA2 for the sake of testing. In the real life we expect a DeFi app to perform the trade. + let fa2 = fungible_asset::mint(&mint_ref_2, 5); + fungible_asset::burn(&burn_ref_1, fa1); + + // Trade FA1 for 5 FA2 + finish_fa_receiving_session(session, fa2); + + assert!(primary_fungible_store::balance(signer::address_of(aaron), test_token_2) == 5, 1); + } +} diff --git a/intent-framework/sources/intent.move b/intent-framework/sources/intent.move new file mode 100644 index 0000000..2d8306d --- /dev/null +++ b/intent-framework/sources/intent.move @@ -0,0 +1,113 @@ +module aptos_intent::intent { + use std::error; + use std::signer; + use aptos_framework::object::{Self, DeleteRef, Object}; + use aptos_framework::timestamp; + use aptos_framework::type_info::{Self, TypeInfo}; + + /// The offered intent has expired + const EINTENT_EXPIRED: u64 = 0; + + /// The registered hook function for consuming resource doesn't match the type requirement. + const ECONSUMPTION_FUNCTION_TYPE_MISMATCH: u64 = 1; + + /// Only owner can revoke an intent. + const ENOT_OWNER: u64 = 2; + + /// Provided wrong witness to complete intent. + const EINVALID_WITNESS: u64 = 3; + + struct TradeIntent has key { + offered_resource: Source, + argument: Args, + self_delete_ref: DeleteRef, + expiry_time: u64, + witness_type: TypeInfo, + } + + struct TradeSession { + argument: Args, + witness_type: TypeInfo, + } + + // Core offering logic + + public fun create_intent( + offered_resource: Source, + argument: Args, + expiry_time: u64, + issuer: address, + _witness: Witness, + ): Object> { + let constructor_ref = object::create_object(issuer); + let object_signer = object::generate_signer(&constructor_ref); + let self_delete_ref = object::generate_delete_ref(&constructor_ref); + + move_to>( + &object_signer, + TradeIntent { + offered_resource, + argument, + expiry_time, + self_delete_ref, + witness_type: type_info::type_of(), + } + ); + object::object_from_constructor_ref(&constructor_ref) + } + + public fun start_intent_session( + intent: Object>, + ): (Source, TradeSession) acquires TradeIntent { + let intent_ref = borrow_global>(object::object_address(&intent)); + assert!(timestamp::now_seconds() <= intent_ref.expiry_time, error::permission_denied(EINTENT_EXPIRED)); + + let TradeIntent { + offered_resource, + argument, + expiry_time: _, + self_delete_ref, + witness_type, + } = move_from>(object::object_address(&intent)); + + object::delete(self_delete_ref); + + return (offered_resource, TradeSession { + argument, + witness_type, + }) + } + + public fun get_argument(session: &TradeSession): &Args { + &session.argument + } + + public fun finish_intent_session( + session: TradeSession, + _witness: Witness, + ) { + let TradeSession { + argument:_ , + witness_type, + } = session; + + assert!(type_info::type_of() == witness_type, error::permission_denied(EINVALID_WITNESS)); + } + + public fun revoke_intent( + issuer: &signer, + intent: Object>, + ): Source acquires TradeIntent { + assert!(object::owner(intent) == signer::address_of(issuer), error::permission_denied(ENOT_OWNER)); + let TradeIntent { + offered_resource, + argument: _, + expiry_time: _, + self_delete_ref, + witness_type: _, + } = move_from>(object::object_address(&intent)); + + object::delete(self_delete_ref); + offered_resource + } +}