Skip to content

Commit

Permalink
[abstraction] Add support for AccountAbstraction (#622)
Browse files Browse the repository at this point in the history
* Fix FA tests

* Initial work for AccountAbstraction

* Add tests

* Update CHANGELOG.md

* Fix dependency cycle

* Add permissioned delegation tests

* Update tests

* Update with new abstraction function names

* Remove any_authenticator example for complex public key delegation example
  • Loading branch information
GhostWalker562 authored Feb 6, 2025
1 parent 5d60650 commit e48a65f
Show file tree
Hide file tree
Showing 29 changed files with 1,218 additions and 30 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ All notable changes to the Aptos TypeScript SDK will be captured in this file. T
- Add new `scriptComposer` api in transactionSubmission api to allow SDK callers to invoke multiple Move functions inside a same transaction and compose the calls dynamically.
- Add support for vectors as string as a valid argument

- Add `AbstractedAccount` class to support account abstraction with custom signers.
- Add `aptos.abstraction` namespace to support account abstraction APIs. Notable functions are: `isAccountAbstractionEnabled`, `enableAccountAbstractionTransaction`, and `disableAccountAbstractionTransaction`.

# 1.33.2 (2025-01-22)

- [`Fix`] Fixes pagination for GetAccountModules and GetAccountResources. Also, adds more appropriate documentation on offset.
Expand Down
3 changes: 2 additions & 1 deletion examples/typescript/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
move/moonCoin/moonCoin.json
move/moonCoin/test-package.json
move/facoin/facoin.json
move/facoin/facoin.json
move/account_abstraction/*.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/* eslint-disable no-console */

import { Account, AbstractedAccount, Aptos, Network, AptosConfig, UserTransactionResponse } from "@aptos-labs/ts-sdk";
import { compilePackage, getPackageBytesToPublish } from "./utils";

const aptos = new Aptos(new AptosConfig({ network: Network.DEVNET }));

const main = async () => {
const alice = Account.generate();

console.log("\n=== Addresses ===");
console.log(`Alice: ${alice.accountAddress.toString()}`);

console.log("\n=== Funding Accounts ===");
await aptos.fundAccount({ accountAddress: alice.accountAddress, amount: 1000000000000000 });
console.log("Finished funding accounts!");

console.log("\n=== Compiling hello_world_authenticator package locally ===");
compilePackage(
"move/account_abstraction",
"move/account_abstraction/hello_world_authenticator.json",
[{ name: "deployer", address: alice.accountAddress }],
["--move-2"],
);
const { metadataBytes, byteCode } = getPackageBytesToPublish(
"move/account_abstraction/hello_world_authenticator.json",
);
console.log(`\n=== Publishing hello_world_authenticator package to ${aptos.config.network} network ===`);
const publishTxn = await aptos.publishPackageTransaction({
account: alice.accountAddress,
metadataBytes,
moduleBytecode: byteCode,
});
const pendingPublishTxn = await aptos.signAndSubmitTransaction({ signer: alice, transaction: publishTxn });
console.log(`Publish package transaction hash: ${pendingPublishTxn.hash}`);
await aptos.waitForTransaction({ transactionHash: pendingPublishTxn.hash });

console.log("\n=== Dispatchable authentication function info ===");

const authenticationFunction = `${alice.accountAddress}::hello_world_authenticator::authenticate`;
const [moduleAddress, moduleName, functionName] = authenticationFunction.split("::");

console.log(`Module address: ${moduleAddress}`);
console.log(`Module name: ${moduleName}`);
console.log(`Function name: ${functionName}`);

console.log(
`\n=== Changing ${alice.accountAddress.toString()} to use any_authenticator's AccountAbstraction function ===`,
);
const enableAccountAbstractionTransaction = await aptos.abstraction.enableAccountAbstractionTransaction({
accountAddress: alice.accountAddress,
authenticationFunction,
});
const pendingEnableAccountAbstractionTransaction = await aptos.signAndSubmitTransaction({
signer: alice,
transaction: enableAccountAbstractionTransaction,
});
console.log(`Enable account abstraction transaction hash: ${pendingEnableAccountAbstractionTransaction.hash}`);
await aptos.waitForTransaction({ transactionHash: pendingEnableAccountAbstractionTransaction.hash });

console.log("\n=== Signing a transaction with the abstracted account ===");

const abstractedAccount = new AbstractedAccount({
accountAddress: alice.accountAddress,
signer: () => new TextEncoder().encode("hello world"),
authenticationFunction,
});
const pendingTransferTxn = await aptos.signAndSubmitTransaction({
signer: abstractedAccount,
transaction: await aptos.transferCoinTransaction({
sender: abstractedAccount.accountAddress,
recipient: abstractedAccount.accountAddress,
amount: 100,
}),
});

const response = await aptos.waitForTransaction({ transactionHash: pendingTransferTxn.hash });
console.log(`Committed transaction: ${response.hash}`);

const txn = (await aptos.getTransactionByHash({
transactionHash: pendingTransferTxn.hash,
})) as UserTransactionResponse;
console.log(`Transaction Signature: ${JSON.stringify(txn.signature, null, 2)}`);
};

main();
10 changes: 10 additions & 0 deletions examples/typescript/move/account_abstraction/Move.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[package]
name = "account_abstraction"
version = "0.0.0"

[addresses]
deployer = "_"

[dependencies]
AptosFramework = { git = "https://github.com/aptos-labs/aptos-framework.git", subdir = "aptos-framework", rev = "devnet" }
AptosStdlib = { git = "https://github.com/aptos-labs/aptos-framework.git", subdir = "aptos-stdlib", rev = "devnet" }
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module deployer::hello_world_authenticator {
use aptos_framework::auth_data::{Self, AbstractionAuthData};

const EINVALID_SIGNATURE: u64 = 1;

public fun authenticate(
account: signer,
signing_data: AbstractionAuthData,
): signer {
let authenticator = *auth_data::authenticator(&signing_data); // Dereference to get owned vector
assert!(authenticator == b"hello world", EINVALID_SIGNATURE);
account
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
module deployer::public_key_authenticator {
use std::signer;
use aptos_std::smart_table::{Self, SmartTable};
use aptos_std::ed25519::{
Self,
new_signature_from_bytes,
new_unvalidated_public_key_from_bytes,
unvalidated_public_key_to_bytes
};
use aptos_framework::bcs_stream::{Self, deserialize_u8};
use aptos_framework::auth_data::{Self, AbstractionAuthData};

// ====== Error Codes ====== //

const EINVALID_PUBLIC_KEY: u64 = 0x20000;
const EPUBLIC_KEY_NOT_PERMITTED: u64 = 0x20001;
const EENTRY_ALREADY_EXISTS: u64 = 0x20002;
const ENO_PERMISSIONS: u64 = 0x20003;
const EINVALID_SIGNATURE: u64 = 0x20004;

// ====== Data Structures ====== //

struct PublicKeyPermissions has key {
public_key_table: SmartTable<vector<u8>, bool>,
}

// ====== Authenticator ====== //

public fun authenticate(
account: signer,
auth_data: AbstractionAuthData
): signer acquires PublicKeyPermissions {
let account_addr = signer::address_of(&account);
assert!(exists<PublicKeyPermissions>(account_addr), ENO_PERMISSIONS);
let permissions = borrow_global<PublicKeyPermissions>(account_addr);

// Extract the public key and signature from the authenticator
let authenticator = *auth_data::authenticator(&auth_data);
let stream = bcs_stream::new(authenticator);
let public_key = new_unvalidated_public_key_from_bytes(
bcs_stream::deserialize_vector<u8>(&mut stream, |x| deserialize_u8(x))
);
let signature = new_signature_from_bytes(
bcs_stream::deserialize_vector<u8>(&mut stream, |x| deserialize_u8(x))
);

// Check if the public key is permitted
assert!(smart_table::contains(&permissions.public_key_table, unvalidated_public_key_to_bytes(&public_key)), EPUBLIC_KEY_NOT_PERMITTED);

// Verify the signature
let digest = *auth_data::digest(&auth_data);
assert!(ed25519::signature_verify_strict(&signature, &public_key, digest), EINVALID_SIGNATURE);

account
}

// ====== Core Functionality ====== //

public entry fun permit_public_key(
signer: &signer,
public_key: vector<u8>
) acquires PublicKeyPermissions {
let account_addr = signer::address_of(signer);
assert!(std::vector::length(&public_key) == 32, EINVALID_PUBLIC_KEY);

if (!exists<PublicKeyPermissions>(account_addr)) {
move_to(signer, PublicKeyPermissions {
public_key_table: smart_table::new(),
});
};

let permissions = borrow_global_mut<PublicKeyPermissions>(account_addr);
assert!(
!smart_table::contains(&permissions.public_key_table, public_key),
EENTRY_ALREADY_EXISTS
);

smart_table::add(&mut permissions.public_key_table, public_key, true);

}

public entry fun revoke_public_key(
signer: &signer,
public_key: vector<u8>
) acquires PublicKeyPermissions {
let account_addr = signer::address_of(signer);

assert!(exists<PublicKeyPermissions>(account_addr), ENO_PERMISSIONS);

let permissions = borrow_global_mut<PublicKeyPermissions>(account_addr);
smart_table::remove(&mut permissions.public_key_table, public_key);
}

}
2 changes: 2 additions & 0 deletions examples/typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
"keyless": "ts-node keyless.ts",
"keyless_mainnet": "ts-node keyless_mainnet.ts",
"local_node": "ts-node local_node.ts",
"public_key_authenticator_account_abstraction": "ts-node public_key_authenticator_account_abstraction.ts",
"hello_world_authenticator_account_abstraction": "ts-node hello_world_authenticator_account_abstraction.ts",
"test": "run-s simple_transfer multi_agent_transfer simple_sponsored_transaction transfer_coin custom_client publish_package_from_filepath external_signing sign_struct publish_package_from_filepath external_signing your_coin your_fungible_asset"
},
"keywords": [],
Expand Down
114 changes: 114 additions & 0 deletions examples/typescript/public_key_authenticator_account_abstraction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/* eslint-disable no-console */

import {
Account,
AbstractedAccount,
Aptos,
Network,
AptosConfig,
UserTransactionResponse,
Serializer,
} from "@aptos-labs/ts-sdk";
import { compilePackage, getPackageBytesToPublish } from "./utils";

const aptos = new Aptos(new AptosConfig({ network: Network.DEVNET }));

const main = async () => {
const alice = Account.generate();
const bob = Account.generate();

console.log("\n=== Addresses ===");
console.log(`Alice: ${alice.accountAddress.toString()}`);
console.log(`Bob: ${bob.accountAddress.toString()}`);
console.log("\n=== Funding Accounts ===");
await aptos.fundAccount({ accountAddress: alice.accountAddress, amount: 1000000000000000 });
console.log("Finished funding accounts!");

console.log("\n=== Compiling public_key_authenticator package locally ===");
compilePackage(
"move/account_abstraction",
"move/account_abstraction/public_key_authenticator.json",
[{ name: "deployer", address: alice.accountAddress }],
["--move-2"],
);
const { metadataBytes, byteCode } = getPackageBytesToPublish(
"move/account_abstraction/public_key_authenticator.json",
);
console.log(`\n=== Publishing public_key_authenticator package to ${aptos.config.network} network ===`);
const publishTxn = await aptos.publishPackageTransaction({
account: alice.accountAddress,
metadataBytes,
moduleBytecode: byteCode,
});
const pendingPublishTxn = await aptos.signAndSubmitTransaction({ signer: alice, transaction: publishTxn });
console.log(`Publish package transaction hash: ${pendingPublishTxn.hash}`);
await aptos.waitForTransaction({ transactionHash: pendingPublishTxn.hash });

console.log("\n=== Dispatchable authentication function info ===");

const authenticationFunction = `${alice.accountAddress}::public_key_authenticator::authenticate`;
const [moduleAddress, moduleName, functionName] = authenticationFunction.split("::");

console.log(`Module address: ${moduleAddress}`);
console.log(`Module name: ${moduleName}`);
console.log(`Function name: ${functionName}`);

console.log(`\n=== Enabling account abstraction for ${alice.accountAddress.toString()} ===`);
const enableAccountAbstractionTransaction = await aptos.abstraction.enableAccountAbstractionTransaction({
accountAddress: alice.accountAddress,
authenticationFunction,
});
const pendingEnableAccountAbstractionTransaction = await aptos.signAndSubmitTransaction({
signer: alice,
transaction: enableAccountAbstractionTransaction,
});
console.log(`Enable account abstraction transaction hash: ${pendingEnableAccountAbstractionTransaction.hash}`);
await aptos.waitForTransaction({ transactionHash: pendingEnableAccountAbstractionTransaction.hash });

console.log("\n=== Permitting Bob's public key to sign on behalf of Alice");
const enableBobPublicKeyTransaction = await aptos.transaction.build.simple({
sender: alice.accountAddress,
data: {
function: `${alice.accountAddress}::public_key_authenticator::permit_public_key`,
typeArguments: [],
functionArguments: [bob.publicKey.toUint8Array()],
},
});
const pendingEnableBobPublicKeyTransaction = await aptos.signAndSubmitTransaction({
signer: alice,
transaction: enableBobPublicKeyTransaction,
});
console.log(`Enable Bob's public key transaction hash: ${pendingEnableBobPublicKeyTransaction.hash}`);
await aptos.waitForTransaction({ transactionHash: pendingEnableBobPublicKeyTransaction.hash });

console.log("\n=== Signing a transaction with the abstracted account using Bob's signer ===");

const abstractedAccount = new AbstractedAccount({
accountAddress: alice.accountAddress,
signer: (digest) => {
const serializer = new Serializer();
bob.publicKey.serialize(serializer);
bob.sign(digest).serialize(serializer);
return serializer.toUint8Array();
},
authenticationFunction,
});
const pendingTransferTxn = await aptos.signAndSubmitTransaction({
signer: abstractedAccount,
transaction: await aptos.transferCoinTransaction({
sender: abstractedAccount.accountAddress,
recipient: abstractedAccount.accountAddress,
amount: 100,
}),
});

const response = await aptos.waitForTransaction({ transactionHash: pendingTransferTxn.hash });
console.log(`Committed transaction: ${response.hash}`);

const txn = (await aptos.getTransactionByHash({
transactionHash: pendingTransferTxn.hash,
})) as UserTransactionResponse;
console.log(`Transaction Signature: ${JSON.stringify(txn.signature, null, 2)}`);
};

main();
5 changes: 4 additions & 1 deletion examples/typescript/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export function compilePackage(
packageDir: string,
outputFile: string,
namedAddresses: Array<{ name: string; address: AccountAddress }>,
args?: string[],
) {
console.log("In order to run compilation, you must have the `aptos` CLI installed.");
try {
Expand All @@ -27,7 +28,9 @@ export function compilePackage(
const addressArg = namedAddresses.map(({ name, address }) => `${name}=${address}`).join(" ");

// Assume-yes automatically overwrites the previous compiled version, only do this if you are sure you want to overwrite the previous version.
const compileCommand = `aptos move build-publish-payload --json-output-file ${outputFile} --package-dir ${packageDir} --named-addresses ${addressArg} --assume-yes`;
let compileCommand = `aptos move build-publish-payload --json-output-file ${outputFile} --package-dir ${packageDir} --named-addresses ${addressArg} --assume-yes`;
if (args) compileCommand += ` ${args.join(" ")}`;

console.log("Running the compilation locally, in a real situation you may want to compile this ahead of time.");
console.log(compileCommand);
execSync(compileCommand);
Expand Down
Loading

0 comments on commit e48a65f

Please sign in to comment.