Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[abstraction] Add support for AccountAbstraction #622

Merged
merged 9 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ All notable changes to the Aptos TypeScript SDK will be captured in this file. T
# Unreleased
- 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 `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
Loading