Skip to content

Commit

Permalink
Add AccountUtils which allows for general serialization of the accoun…
Browse files Browse the repository at this point in the history
…t classes (#571)

* Add serialization to the account classes

* Move serialization of accounts to accountUtils

* add changelog

* Make public keys required in multikey constructor

* update cl

* Use namespace instead of class

* Update error log
  • Loading branch information
heliuchuan authored Jan 8, 2025
1 parent 7a0a06a commit 8c9e01b
Show file tree
Hide file tree
Showing 17 changed files with 476 additions and 38 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ 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`
- We now throw an error earlier when you try to use the faucet with testnet or mainnet, rather than letting the call happen and then fail later.

# 1.33.1 (2024-11-28)
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);
}

/**
* 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;
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 {
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";

function 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);
}

function 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 };
}

/**
* Utility functions for working with accounts.
*/
export namespace AccountUtils {
export function 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;
serializeKeylessAccountCommon(keylessAccount, serializer);
return serializer.toUint8Array();
}
case AnyPublicKeyVariant.FederatedKeyless: {
const federatedKeylessAccount = account as FederatedKeylessAccount;
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(toBytes(signer));
});
return serializer.toUint8Array();
}
default:
throw new Error(`Deserialization of Account failed: invalid signingScheme value ${account.signingScheme}`);
}
}

export function toHexStringWithoutPrefix(account: Account): string {
return Hex.hexInputToStringWithoutPrefix(toBytes(account));
}

export function toHexString(account: Account): string {
return Hex.hexInputToString(toBytes(account));
}

export function 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 = deserializeKeylessAccountCommon(deserializer);
const jwtClaims = getIssAudAndUidVal(keylessComponents);
return new KeylessAccount({ ...keylessComponents, ...jwtClaims });
}
case AnyPublicKeyVariant.FederatedKeyless: {
const keylessComponents = 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 = 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}`);
}
}

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

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

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

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

export function ed25519AccountFromHex(hex: HexInput): Ed25519Account {
const account = fromHex(hex);
if (!(account instanceof Ed25519Account)) {
throw new Error("Deserialization of Ed25519Account failed");
}
return account;
}

export function fromHex(hex: HexInput): Account {
return deserialize(Deserializer.fromHex(hex));
}

export function fromBytes(bytes: Uint8Array): Account {
return 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

0 comments on commit 8c9e01b

Please sign in to comment.