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

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 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 @@ -4,6 +4,9 @@ All notable changes to the Aptos TypeScript SDK will be captured in this file. T

# Unreleased

- 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
92 changes: 92 additions & 0 deletions examples/typescript/any_authenticator_account_abstraction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/* eslint-disable no-console */
GhostWalker562 marked this conversation as resolved.
Show resolved Hide resolved

import {
Account,
AbstractedAccount,
Aptos,
Network,
AptosConfig,
UserTransactionResponse,
Hex,
} 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 any_authenticator package locally ===");
compilePackage(
"move/account_abstraction",
"move/account_abstraction/any_authenticator.json",
[{ name: "deployer", address: alice.accountAddress }],
["--move-2"],
);
const { metadataBytes, byteCode } = getPackageBytesToPublish("move/account_abstraction/any_authenticator.json");
console.log(`\n=== Publishing any_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}::any_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({
signer: () => Hex.fromHexString("0x01").toUint8Array(),
accountAddress: alice.accountAddress,
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();
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,11 @@
module deployer::any_authenticator {
use aptos_framework::auth_data::{AbstractionAuthData};

public fun authenticate(
account: signer,
_signing_data: AbstractionAuthData,
): signer {
account
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module deployer::hello_world_authenticator {
use aptos_framework::auth_data::{Self, AbstractionAuthData};
use std::bcs;

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
}
}
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",
"any_authenticator_account_abstraction": "ts-node any_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
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
105 changes: 105 additions & 0 deletions src/account/AbstractedAccount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { sha3_256 } from "@noble/hashes/sha3";
import { AccountAddress, AccountPublicKey } from "../core";
import { AbstractPublicKey, AbstractSignature } from "../core/crypto/abstraction";
import { SigningScheme, HexInput } from "../types";
import { Account } from "./Account";
import { AnyRawTransaction } from "../transactions/types";
import { generateSigningMessageForTransaction } from "../transactions/transactionBuilder/signingMessage";
import { AccountAuthenticatorAbstraction } from "../transactions/authenticator/account";
import { Ed25519Account } from "./Ed25519Account";
import { Serializer } from "../bcs/serializer";
import { isValidFunctionInfo } from "../utils/helpers";

type AbstractedAccountConstructorArgs = {
/**
* The account address of the account.
*/
accountAddress: AccountAddress;
/**
* The signer function signs transactions and returns the `authenticator` bytes in the `AbstractionAuthData`.
*
* @param digest - The SHA256 hash of the transaction signing message
* @returns The `authenticator` bytes that can be used to verify the signature.
*/
signer: (digest: HexInput) => HexInput;
/**
* The authentication function that will be used to verify the signature.
*
* @example
* ```ts
* const authenticationFunction = `${accountAddress}::permissioned_delegation::authenticate`;
* ```
*/
authenticationFunction: string;
};

export class AbstractedAccount extends Account {
public readonly publicKey: AccountPublicKey;
GhostWalker562 marked this conversation as resolved.
Show resolved Hide resolved

readonly accountAddress: AccountAddress;

readonly authenticationFunction: string;

readonly signingScheme = SigningScheme.SingleKey;

constructor({ signer, accountAddress, authenticationFunction }: AbstractedAccountConstructorArgs) {
super();

if (!isValidFunctionInfo(authenticationFunction)) {
throw new Error(`Invalid authentication function ${authenticationFunction} passed into AbstractedAccount`);
}

this.authenticationFunction = authenticationFunction;
this.accountAddress = accountAddress;
this.publicKey = new AbstractPublicKey(this.accountAddress);
GhostWalker562 marked this conversation as resolved.
Show resolved Hide resolved
this.sign = (digest: HexInput) => new AbstractSignature(signer(digest));
}

/**
* Creates an `AbstractedAccount` from an `Ed25519Account` that has a permissioned signer function and
* using the `0x1::permissioned_delegation::authenticate` function to verify the signature.
*
* @param signer - The `Ed25519Account` that can be used to sign permissioned transactions.
* @returns The `AbstractedAccount`
*/
public static fromPermissionedSigner({ signer }: { signer: Ed25519Account }) {
GhostWalker562 marked this conversation as resolved.
Show resolved Hide resolved
return new AbstractedAccount({
signer: (digest: HexInput) => {
const serializer = new Serializer();
signer.publicKey.serialize(serializer);
signer.sign(digest).serialize(serializer);
GhostWalker562 marked this conversation as resolved.
Show resolved Hide resolved
return serializer.toUint8Array();
},
accountAddress: signer.accountAddress,
authenticationFunction: `${signer.accountAddress}::permissioned_delegation::authenticate`,
});
}
GhostWalker562 marked this conversation as resolved.
Show resolved Hide resolved

signWithAuthenticator(message: HexInput): AccountAuthenticatorAbstraction {
return new AccountAuthenticatorAbstraction(
this.authenticationFunction,
sha3_256(message),
this.sign(sha3_256(message)).toUint8Array(),
);
}

signTransactionWithAuthenticator(transaction: AnyRawTransaction): AccountAuthenticatorAbstraction {
return this.signWithAuthenticator(generateSigningMessageForTransaction(transaction));
}

sign: (message: HexInput) => AbstractSignature;

signTransaction(transaction: AnyRawTransaction): AbstractSignature {
return this.sign(generateSigningMessageForTransaction(transaction));
}

/**
* Update the signer function for the account. This can be done after asynchronous operations are complete
* to update the context of the signer function.
*
* @param signer - The new signer function to use for the account.
*/
public setSigner(signer: (digest: HexInput) => HexInput): void {
this.sign = (digest: HexInput) => new AbstractSignature(signer(digest));
}
}
1 change: 1 addition & 0 deletions src/account/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export * from "./AbstractKeylessAccount";
export * from "./FederatedKeylessAccount";
export * from "./MultiKeyAccount";
export * from "./AccountUtils";
export * from "./AbstractedAccount";
Loading
Loading