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

Add AccountUtils which allows for general serialization of the account classes #571

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
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 `AccountUtils` class to help with account serialization and deserialization
- Add `SingleKeySigner` interface which adds the ability to get the `AnyPublicKey` from a `SingleKeyAccount`

# 1.33.1 (2024-11-28)

- Add `gasProfile` function to `Move` class to allow for gas profiling of Aptos Move functions
Expand Down
10 changes: 7 additions & 3 deletions src/account/AbstractKeylessAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { FederatedKeylessPublicKey } from "../core/crypto/federatedKeyless";
import { Account } from "./Account";
import { AptosConfig } from "../api/aptosConfig";
import { KeylessError, KeylessErrorType } from "../errors";
import type { SingleKeySigner } from "./SingleKeyAccount";

/**
* An interface which defines if an Account utilizes Keyless signing.
Expand All @@ -47,7 +48,7 @@ export function isKeylessSigner(obj: any): obj is KeylessSigner {
* @group Implementation
* @category Account (On-Chain Model)
*/
export abstract class AbstractKeylessAccount extends Serializable implements KeylessSigner {
export abstract class AbstractKeylessAccount extends Serializable implements KeylessSigner, SingleKeySigner {
static readonly PEPPER_LENGTH: number = 31;

/**
Expand Down Expand Up @@ -120,7 +121,7 @@ export abstract class AbstractKeylessAccount extends Serializable implements Key
* @group Implementation
* @category Account (On-Chain Model)
*/
readonly signingScheme: SigningScheme;
readonly signingScheme: SigningScheme = SigningScheme.SingleKey;

/**
* The JWT token used to derive the account
Expand Down Expand Up @@ -211,7 +212,6 @@ export abstract class AbstractKeylessAccount extends Serializable implements Key
// Note, this is purposely not awaited to be non-blocking. The caller should await on the proofFetchCallback.
this.init(proof);
}
this.signingScheme = SigningScheme.SingleKey;
const pepperBytes = Hex.fromHexInput(pepper).toUint8Array();
if (pepperBytes.length !== AbstractKeylessAccount.PEPPER_LENGTH) {
throw new Error(`Pepper length in bytes should be ${AbstractKeylessAccount.PEPPER_LENGTH}`);
Expand All @@ -225,6 +225,10 @@ export abstract class AbstractKeylessAccount extends Serializable implements Key
}
}

getAnyPublicKey(): AnyPublicKey {
return new AnyPublicKey(this.publicKey);
}
Comment on lines +228 to +230
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about we make this.publicKey return the wrapped key for consistency?
So from outside we don't have to call a different method.

And if we need access to KeylessPublicKey we can make an internal field for that e.g. private readonly keylessPublicKey: KeylessPublicKey


/**
* This initializes the asynchronous proof fetch
* @return Emits whether the proof succeeds or fails, but has no return.
Expand Down
11 changes: 5 additions & 6 deletions src/account/Account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { AccountAuthenticator } from "../transactions/authenticator/account
import { HexInput, SigningScheme, SigningSchemeInput } from "../types";
import type { AccountAddress, AccountAddressInput } from "../core/accountAddress";
import { AuthenticationKey } from "../core/authenticationKey";
import { AccountPublicKey, Ed25519PrivateKey, PrivateKey, Signature, VerifySignatureArgs } from "../core/crypto";
import { AccountPublicKey, Ed25519PrivateKey, PrivateKeyInput, Signature, VerifySignatureArgs } from "../core/crypto";
import { Ed25519Account } from "./Ed25519Account";
import { SingleKeyAccount } from "./SingleKeyAccount";
import { AnyRawTransaction } from "../transactions/types";
Expand Down Expand Up @@ -50,7 +50,7 @@ export interface CreateEd25519SingleKeyAccountFromPrivateKeyArgs {
* @category Account (On-Chain Model)
*/
export interface CreateSingleKeyAccountFromPrivateKeyArgs {
privateKey: Exclude<PrivateKey, Ed25519PrivateKey>;
privateKey: PrivateKeyInput;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not the same right?
type PrivateKeyInput = Ed25519PrivateKey | Secp256k1PrivateKey

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would maybe use Exclude<PrivateKeyInput, Ed25519PrivateKey>

address?: AccountAddressInput;
legacy?: false;
}
Expand All @@ -65,7 +65,7 @@ export interface CreateSingleKeyAccountFromPrivateKeyArgs {
* @category Account (On-Chain Model)
*/
export interface CreateAccountFromPrivateKeyArgs {
privateKey: PrivateKey;
privateKey: PrivateKeyInput;
address?: AccountAddressInput;
legacy?: boolean;
}
Expand Down Expand Up @@ -206,10 +206,9 @@ export abstract class Account {
* @category Account (On-Chain Model)
*/
static fromPrivateKey(args: CreateEd25519AccountFromPrivateKeyArgs): Ed25519Account;
static fromPrivateKey(args: CreateEd25519SingleKeyAccountFromPrivateKeyArgs): SingleKeyAccount;
static fromPrivateKey(args: CreateSingleKeyAccountFromPrivateKeyArgs): SingleKeyAccount;
static fromPrivateKey(args: CreateAccountFromPrivateKeyArgs): Account;
static fromPrivateKey(args: CreateAccountFromPrivateKeyArgs) {
static fromPrivateKey(args: CreateAccountFromPrivateKeyArgs): SingleKeyAccount;
static fromPrivateKey(args: CreateAccountFromPrivateKeyArgs): Ed25519Account | SingleKeyAccount {
Comment on lines -209 to +211
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why these changes?
We're losing type inference

const { privateKey, address, legacy = true } = args;
if (privateKey instanceof Ed25519PrivateKey && legacy) {
return new Ed25519Account({
Expand Down
216 changes: 216 additions & 0 deletions src/account/AccountUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import { Deserializer, Serializer } from "../bcs";
import { AnyPublicKeyVariant, HexInput, SigningScheme } from "../types";
import { MultiKeyAccount } from "./MultiKeyAccount";
import { Account } from "./Account";
import { Ed25519Account } from "./Ed25519Account";
import { isSingleKeySigner, SingleKeyAccount, SingleKeySignerOrLegacyEd25519Account } from "./SingleKeyAccount";
import { KeylessAccount } from "./KeylessAccount";
import { FederatedKeylessAccount } from "./FederatedKeylessAccount";
import { AbstractKeylessAccount } from "./AbstractKeylessAccount";
import {
AccountAddress,
Ed25519PrivateKey,
getIssAudAndUidVal,
Hex,
MultiKey,
Secp256k1PrivateKey,
ZeroKnowledgeSig,
} from "../core";
import { deserializeSchemeAndAddress } from "./utils";
import { EphemeralKeyPair } from "./EphemeralKeyPair";

/**
* Utility functions for working with accounts.
*/
export class AccountUtils {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need a static class to namespace these functions.
How about either exporting functions directly e.g.

export function accountToBytes(account: Account): Uint8Array { ... }
export function accountFromHexInput(input: HexInput): Account { ... }
export function serializeAccount(serializer: Serializer, account: Account) { ... }
export function deserializeAccount(deserializer: Deserializer): Account { ... }

or alternatively, a namespace

export namespace AccountUtils {
  export function ...
}

private static serializeKeylessAccountCommon(account: AbstractKeylessAccount, serializer: Serializer): void {
serializer.serializeStr(account.jwt);
serializer.serializeStr(account.uidKey);
serializer.serializeFixedBytes(account.pepper);
account.ephemeralKeyPair.serialize(serializer);
if (account.proof === undefined) {
throw new Error("Cannot serialize - proof undefined");
}
account.proof.serialize(serializer);
serializer.serializeOption(account.verificationKeyHash, 32);
}

private static deserializeKeylessAccountCommon(deserializer: Deserializer): {
jwt: string;
uidKey: string;
pepper: Uint8Array;
ephemeralKeyPair: EphemeralKeyPair;
proof: ZeroKnowledgeSig;
verificationKeyHash?: Uint8Array;
} {
const jwt = deserializer.deserializeStr();
const uidKey = deserializer.deserializeStr();
const pepper = deserializer.deserializeFixedBytes(31);
const ephemeralKeyPair = EphemeralKeyPair.deserialize(deserializer);
const proof = ZeroKnowledgeSig.deserialize(deserializer);
const verificationKeyHash = deserializer.deserializeOption("fixedBytes", 32);
return { jwt, uidKey, pepper, ephemeralKeyPair, proof, verificationKeyHash };
}

static toBytes(account: Account): Uint8Array {
const serializer = new Serializer();
serializer.serializeU32AsUleb128(account.signingScheme);
account.accountAddress.serialize(serializer);
switch (account.signingScheme) {
case SigningScheme.Ed25519:
(account as Ed25519Account).privateKey.serialize(serializer);
return serializer.toUint8Array();
case SigningScheme.SingleKey: {
if (!isSingleKeySigner(account)) {
throw new Error("Account is not a SingleKeySigner");
}
const anyPublicKey = account.getAnyPublicKey();
serializer.serializeU32AsUleb128(anyPublicKey.variant);
switch (anyPublicKey.variant) {
case AnyPublicKeyVariant.Keyless: {
const keylessAccount = account as KeylessAccount;
this.serializeKeylessAccountCommon(keylessAccount, serializer);
return serializer.toUint8Array();
}
case AnyPublicKeyVariant.FederatedKeyless: {
const federatedKeylessAccount = account as FederatedKeylessAccount;
this.serializeKeylessAccountCommon(federatedKeylessAccount, serializer);
federatedKeylessAccount.publicKey.jwkAddress.serialize(serializer);
serializer.serializeBool(federatedKeylessAccount.audless);
return serializer.toUint8Array();
}
case AnyPublicKeyVariant.Secp256k1:
case AnyPublicKeyVariant.Ed25519: {
const singleKeyAccount = account as SingleKeyAccount;
singleKeyAccount.privateKey.serialize(serializer);
return serializer.toUint8Array();
}
default: {
throw new Error(`Invalid public key variant: ${anyPublicKey.variant}`);
}
}
}
case SigningScheme.MultiKey: {
const multiKeyAccount = account as MultiKeyAccount;
multiKeyAccount.publicKey.serialize(serializer);
serializer.serializeU32AsUleb128(multiKeyAccount.signers.length);
multiKeyAccount.signers.forEach((signer) => {
serializer.serializeFixedBytes(this.toBytes(signer));
});
return serializer.toUint8Array();
}
default:
throw new Error(`Deserialization of Account failed: invalid signingScheme value ${account.signingScheme}`);
}
}

static toHexStringWithoutPrefix(account: Account): string {
return Hex.hexInputToStringWithoutPrefix(this.toBytes(account));
}

static toHexString(account: Account): string {
return Hex.hexInputToString(this.toBytes(account));
}

static deserialize(deserializer: Deserializer): Account {
const { address, signingScheme } = deserializeSchemeAndAddress(deserializer);
switch (signingScheme) {
case SigningScheme.Ed25519: {
const privateKey = Ed25519PrivateKey.deserialize(deserializer);
return new Ed25519Account({ privateKey, address });
}
case SigningScheme.SingleKey: {
const variantIndex = deserializer.deserializeUleb128AsU32();
switch (variantIndex) {
case AnyPublicKeyVariant.Ed25519: {
const privateKey = Ed25519PrivateKey.deserialize(deserializer);
return new SingleKeyAccount({ privateKey, address });
}
case AnyPublicKeyVariant.Secp256k1: {
const privateKey = Secp256k1PrivateKey.deserialize(deserializer);
return new SingleKeyAccount({ privateKey, address });
}
case AnyPublicKeyVariant.Keyless: {
const keylessComponents = this.deserializeKeylessAccountCommon(deserializer);
const jwtClaims = getIssAudAndUidVal(keylessComponents);
return new KeylessAccount({ ...keylessComponents, ...jwtClaims });
}
case AnyPublicKeyVariant.FederatedKeyless: {
const keylessComponents = this.deserializeKeylessAccountCommon(deserializer);
const jwkAddress = AccountAddress.deserialize(deserializer);
const audless = deserializer.deserializeBool();
const jwtClaims = getIssAudAndUidVal(keylessComponents);
return new FederatedKeylessAccount({ ...keylessComponents, ...jwtClaims, jwkAddress, audless });
}
default:
throw new Error(`Unsupported public key variant ${variantIndex}`);
}
}
case SigningScheme.MultiKey: {
const multiKey = MultiKey.deserialize(deserializer);
const length = deserializer.deserializeUleb128AsU32();
const signers = new Array<SingleKeySignerOrLegacyEd25519Account>();
for (let i = 0; i < length; i += 1) {
const signer = this.deserialize(deserializer);
if (!isSingleKeySigner(signer) && !(signer instanceof Ed25519Account)) {
throw new Error(
"Deserialization of MultiKeyAccount failed. Signer is not a SingleKeySigner or Ed25519Account",
);
}
signers.push(signer);
}
return new MultiKeyAccount({ multiKey, signers, address });
}
default:
throw new Error(`Deserialization of Account failed: invalid signingScheme value ${signingScheme}`);
}
}

static keylessAccountFromHex(hex: HexInput): KeylessAccount {
const account = this.fromHex(hex);
if (!(account instanceof KeylessAccount)) {
throw new Error("Deserialization of KeylessAccount failed");
}
return account;
}

static federatedKeylessAccountFromHex(hex: HexInput): FederatedKeylessAccount {
const account = this.fromHex(hex);
if (!(account instanceof FederatedKeylessAccount)) {
throw new Error("Deserialization of FederatedKeylessAccount failed");
}
return account;
}

static multiKeyAccountFromHex(hex: HexInput): MultiKeyAccount {
const account = this.fromHex(hex);
if (!(account instanceof MultiKeyAccount)) {
throw new Error("Deserialization of MultiKeyAccount failed");
}
return account;
}

static singleKeyAccountFromHex(hex: HexInput): SingleKeyAccount {
const account = this.fromHex(hex);
if (!(account instanceof SingleKeyAccount)) {
throw new Error("Deserialization of SingleKeyAccount failed");
}
return account;
}

static ed25519AccountFromHex(hex: HexInput): Ed25519Account {
const account = this.fromHex(hex);
if (!(account instanceof Ed25519Account)) {
throw new Error("Deserialization of Ed25519Account failed");
}
return account;
}
Comment on lines +169 to +207
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need these?
It's a very simple check that users can do on their app if needed.
They can also just do the following if they're handling a single type.

const ed25519Account = AccountUtils.fromHex(hex) as Ed25519Account;


static fromHex(hex: HexInput): Account {
return this.deserialize(Deserializer.fromHex(hex));
}

static fromBytes(bytes: Uint8Array): Account {
return this.fromHex(bytes);
}
}
9 changes: 6 additions & 3 deletions src/account/FederatedKeylessAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { EphemeralKeyPair } from "./EphemeralKeyPair";
import { Deserializer, Serializer } from "../bcs";
import { FederatedKeylessPublicKey } from "../core/crypto/federatedKeyless";
import { AbstractKeylessAccount, ProofFetchCallback } from "./AbstractKeylessAccount";
import { Hex } from "../core";

/**
* Account implementation for the FederatedKeyless authentication scheme.
Expand All @@ -32,6 +31,8 @@ export class FederatedKeylessAccount extends AbstractKeylessAccount {
*/
readonly publicKey: FederatedKeylessPublicKey;

readonly audless: boolean;

/**
* Use the static generator `FederatedKeylessAccount.create(...)` instead.
* Creates a KeylessAccount instance using the provided parameters.
Expand All @@ -46,7 +47,7 @@ export class FederatedKeylessAccount extends AbstractKeylessAccount {
* @param args.uidKey - Optional key for user identification, defaults to "sub".
* @param args.proofFetchCallback - Optional callback function for fetching proof.
*/
private constructor(args: {
constructor(args: {
address?: AccountAddress;
ephemeralKeyPair: EphemeralKeyPair;
iss: string;
Expand All @@ -59,10 +60,12 @@ export class FederatedKeylessAccount extends AbstractKeylessAccount {
proofFetchCallback?: ProofFetchCallback;
jwt: string;
verificationKeyHash?: HexInput;
audless?: boolean;
}) {
const publicKey = FederatedKeylessPublicKey.create(args);
super({ publicKey, ...args });
this.publicKey = publicKey;
this.audless = args.audless ?? false;
}

/**
Expand Down Expand Up @@ -110,7 +113,7 @@ export class FederatedKeylessAccount extends AbstractKeylessAccount {
* @returns
*/
static fromBytes(bytes: HexInput): FederatedKeylessAccount {
return FederatedKeylessAccount.deserialize(new Deserializer(Hex.hexInputToUint8Array(bytes)));
return FederatedKeylessAccount.deserialize(Deserializer.fromHex(bytes));
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/account/KeylessAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export class KeylessAccount extends AbstractKeylessAccount {
* @group Implementation
* @category Account (On-Chain Model)
*/
private constructor(args: {
constructor(args: {
address?: AccountAddress;
ephemeralKeyPair: EphemeralKeyPair;
iss: string;
Expand Down
Loading
Loading