From 3dc46d2fa4e2fbe0f52a2d1365d8ffffd130eb3a Mon Sep 17 00:00:00 2001 From: Oliver <heliuchuan@gmail.com> Date: Wed, 11 Dec 2024 06:30:57 +0900 Subject: [PATCH] Move serialization of accounts to accountUtils --- src/account/AbstractKeylessAccount.ts | 28 +-- src/account/Account.ts | 27 +-- src/account/AccountUtils.ts | 216 ++++++++++++++++++++++++ src/account/Ed25519Account.ts | 34 +--- src/account/EphemeralKeyPair.ts | 10 -- src/account/FederatedKeylessAccount.ts | 17 +- src/account/KeylessAccount.ts | 13 +- src/account/MultiKeyAccount.ts | 77 +-------- src/account/SingleKeyAccount.ts | 54 ++---- src/account/index.ts | 1 + src/account/utils.ts | 4 +- src/bcs/deserializer.ts | 25 +-- tests/unit/accountSerialization.test.ts | 49 +++--- tests/unit/helper.ts | 12 +- 14 files changed, 274 insertions(+), 293 deletions(-) create mode 100644 src/account/AccountUtils.ts diff --git a/src/account/AbstractKeylessAccount.ts b/src/account/AbstractKeylessAccount.ts index 20db0d7f4..e8a736ec4 100644 --- a/src/account/AbstractKeylessAccount.ts +++ b/src/account/AbstractKeylessAccount.ts @@ -3,7 +3,7 @@ import EventEmitter from "eventemitter3"; import { jwtDecode } from "jwt-decode"; -import { AnyPublicKeyVariant, EphemeralCertificateVariant, HexInput, SigningScheme } from "../types"; +import { EphemeralCertificateVariant, HexInput, SigningScheme } from "../types"; import { AccountAddress } from "../core/accountAddress"; import { AnyPublicKey, @@ -26,10 +26,9 @@ import { deriveTransactionType, generateSigningMessage } from "../transactions/t import { AnyRawTransaction, AnyRawTransactionInstance } from "../transactions/types"; import { base64UrlDecode } from "../utils/helpers"; import { FederatedKeylessPublicKey } from "../core/crypto/federatedKeyless"; -import type { Account } from "./Account"; +import { Account } from "./Account"; import { AptosConfig } from "../api/aptosConfig"; import { KeylessError, KeylessErrorType } from "../errors"; -import { deserializeSchemeAndAddress } from "./utils"; import type { SingleKeySigner } from "./SingleKeyAccount"; /** @@ -228,13 +227,7 @@ export abstract class AbstractKeylessAccount extends Serializable implements Key * @param serializer - The serializer instance used to convert the jwt data into bytes. */ serialize(serializer: Serializer): void { - serializer.serializeU32AsUleb128(this.signingScheme); this.accountAddress.serialize(serializer); - if (this.publicKey instanceof KeylessPublicKey) { - serializer.serializeU32AsUleb128(AnyPublicKeyVariant.Keyless); - } else { - serializer.serializeU32AsUleb128(AnyPublicKeyVariant.FederatedKeyless); - } serializer.serializeStr(this.jwt); serializer.serializeStr(this.uidKey); serializer.serializeFixedBytes(this.pepper); @@ -255,22 +248,7 @@ export abstract class AbstractKeylessAccount extends Serializable implements Key proof: ZeroKnowledgeSig; verificationKeyHash?: Uint8Array; } { - const { address, signingScheme } = deserializeSchemeAndAddress(deserializer); - if (signingScheme !== SigningScheme.SingleKey) { - throw new Error( - `Deserialization of AbstractKeylessAccount failed: Signing scheme was not SingleKey, was ${signingScheme}`, - ); - } - const anyPublicKeyIndex = deserializer.deserializeUleb128AsU32(); - if ( - anyPublicKeyIndex !== AnyPublicKeyVariant.Keyless && - anyPublicKeyIndex !== AnyPublicKeyVariant.FederatedKeyless - ) { - throw new Error( - // eslint-disable-next-line max-len - `Deserialization of AbstractKeylessAccount failed: Public key variant was not Keyless or FederatedKeyless, was ${anyPublicKeyIndex}`, - ); - } + const address = AccountAddress.deserialize(deserializer); const jwt = deserializer.deserializeStr(); const uidKey = deserializer.deserializeStr(); const pepper = deserializer.deserializeFixedBytes(31); diff --git a/src/account/Account.ts b/src/account/Account.ts index c4aad3c2c..c84dee19a 100644 --- a/src/account/Account.ts +++ b/src/account/Account.ts @@ -6,10 +6,6 @@ import { AccountPublicKey, Ed25519PrivateKey, PrivateKeyInput, Signature, Verify import { Ed25519Account } from "./Ed25519Account"; import { SingleKeyAccount } from "./SingleKeyAccount"; import { AnyRawTransaction } from "../transactions/types"; -import { Serializable } from "../bcs/serializer"; -import { Deserializer } from "../bcs/deserializer"; -import { deserializeSchemeAndAddress } from "./utils"; -import { deserializeNonMultiKeyAccount, MultiKeyAccount } from "./MultiKeyAccount"; /** * Arguments for creating an `Ed25519Account` from an `Ed25519PrivateKey`. @@ -132,7 +128,7 @@ export interface PrivateKeyFromDerivationPathArgs { * * Note: Generating an account instance does not create the account on-chain. */ -export abstract class Account extends Serializable { +export abstract class Account { /** * Public key associated with the account */ @@ -289,25 +285,4 @@ export abstract class Account extends Serializable { verifySignature(args: VerifySignatureArgs): boolean { return this.publicKey.verifySignature(args); } - - static fromHex(hex: HexInput): Account { - return Account.deserialize(Deserializer.fromHex(hex)); - } - - static deserialize(deserializer: Deserializer): Account { - const offset = deserializer.getOffset(); - const { signingScheme } = deserializeSchemeAndAddress(deserializer); - switch (signingScheme) { - case SigningScheme.Ed25519: - case SigningScheme.SingleKey: { - deserializer.reset(offset); - return deserializeNonMultiKeyAccount(deserializer); - } - case SigningScheme.MultiKey: - deserializer.reset(offset); - return MultiKeyAccount.deserialize(deserializer); - default: - throw new Error(`Deserialization of Account failed: invalid signingScheme value ${signingScheme}`); - } - } } diff --git a/src/account/AccountUtils.ts b/src/account/AccountUtils.ts new file mode 100644 index 000000000..d07766a0b --- /dev/null +++ b/src/account/AccountUtils.ts @@ -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 { + 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; + } + + static fromHex(hex: HexInput): Account { + return this.deserialize(Deserializer.fromHex(hex)); + } + + static fromBytes(bytes: Uint8Array): Account { + return this.fromHex(bytes); + } +} diff --git a/src/account/Ed25519Account.ts b/src/account/Ed25519Account.ts index 9bc8adc6b..6a7fc599e 100644 --- a/src/account/Ed25519Account.ts +++ b/src/account/Ed25519Account.ts @@ -5,9 +5,6 @@ import { Ed25519PrivateKey, Ed25519PublicKey, Ed25519Signature } from "../core/c import type { Account } from "./Account"; import { AnyRawTransaction } from "../transactions/types"; import { generateSigningMessageForTransaction } from "../transactions/transactionBuilder/signingMessage"; -import { Serializable, Serializer } from "../bcs/serializer"; -import { Deserializer } from "../bcs/deserializer"; -import { deserializeSchemeAndAddress } from "./utils"; /** * Arguments required to create an instance of an Ed25519 signer. @@ -48,7 +45,7 @@ export interface VerifyEd25519SignatureArgs { * * Note: Generating an instance of this class does not create the account on-chain. */ -export class Ed25519Account extends Serializable implements Account { +export class Ed25519Account implements Account { /** * Private key associated with the account */ @@ -71,7 +68,6 @@ export class Ed25519Account extends Serializable implements Account { * @param args.address - The optional account address; if not provided, it will derive the address from the public key. */ constructor(args: Ed25519SignerConstructorArgs) { - super(); const { privateKey, address } = args; this.privateKey = privateKey; this.publicKey = privateKey.publicKey(); @@ -160,32 +156,4 @@ export class Ed25519Account extends Serializable implements Account { } // endregion - - serialize(serializer: Serializer): void { - serializer.serializeU32AsUleb128(this.signingScheme); - this.accountAddress.serialize(serializer); - this.privateKey.serialize(serializer); - } - - /** - * Deserialize bytes using this account's information. - * - * @param hex The hex being deserialized into an Ed25519Account. - * @returns - */ - static fromHex(hex: HexInput): Ed25519Account { - return Ed25519Account.deserialize(Deserializer.fromHex(hex)); - } - - static deserialize(deserializer: Deserializer): Ed25519Account { - const { address, signingScheme } = deserializeSchemeAndAddress(deserializer); - if (signingScheme !== SigningScheme.Ed25519) { - throw new Error( - // eslint-disable-next-line max-len - `Deserialization of Ed25519Account failed: Signing scheme was not Ed25519, was ${signingScheme} at offset ${deserializer.getOffset()}`, - ); - } - const privateKey = Ed25519PrivateKey.deserialize(deserializer); - return new Ed25519Account({ privateKey, address }); - } } diff --git a/src/account/EphemeralKeyPair.ts b/src/account/EphemeralKeyPair.ts index c54d4590f..f01832efc 100644 --- a/src/account/EphemeralKeyPair.ts +++ b/src/account/EphemeralKeyPair.ts @@ -136,16 +136,6 @@ export class EphemeralKeyPair extends Serializable { return new EphemeralKeyPair({ privateKey, expiryDateSecs: Number(expiryDateSecs), blinder }); } - /** - * Deserialize hex using this account's information. - * - * @param hex The hex being deserialized into an EphemeralKeyPair. - * @returns - */ - static fromHex(hex: HexInput): EphemeralKeyPair { - return EphemeralKeyPair.deserialize(Deserializer.fromHex(hex)); - } - /** * Deserialize a byte array into an EphemeralKeyPair object. * This function allows you to reconstruct an EphemeralKeyPair from its serialized byte representation. diff --git a/src/account/FederatedKeylessAccount.ts b/src/account/FederatedKeylessAccount.ts index 507394ac8..a4ec5d1ca 100644 --- a/src/account/FederatedKeylessAccount.ts +++ b/src/account/FederatedKeylessAccount.ts @@ -27,6 +27,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. @@ -41,7 +43,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; @@ -54,10 +56,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; } /** @@ -99,17 +103,6 @@ export class FederatedKeylessAccount extends AbstractKeylessAccount { } /** - * Deserialize hex using this account's information. - * - * @param hex The hex being deserialized into an FederatedKeylessAccount. - * @returns - */ - static fromHex(hex: HexInput): FederatedKeylessAccount { - return FederatedKeylessAccount.deserialize(Deserializer.fromHex(hex)); - } - - /** - * @deprecated Use `fromHex` instead. * Deserialize bytes using this account's information. * * @param bytes The bytes being interpreted. diff --git a/src/account/KeylessAccount.ts b/src/account/KeylessAccount.ts index 27e70c226..e76332d12 100644 --- a/src/account/KeylessAccount.ts +++ b/src/account/KeylessAccount.ts @@ -45,7 +45,7 @@ export class KeylessAccount extends AbstractKeylessAccount { * @param args.proofFetchCallback - Optional callback function for fetching proof. * @param args.jwt - A JSON Web Token used for authentication. */ - private constructor(args: { + constructor(args: { address?: AccountAddress; ephemeralKeyPair: EphemeralKeyPair; iss: string; @@ -99,17 +99,6 @@ export class KeylessAccount extends AbstractKeylessAccount { } /** - * Deserialize bytes using this account's information. - * - * @param hex The hex being deserialized into an MultiKeyAccount. - * @returns - */ - static fromHex(hex: HexInput): KeylessAccount { - return KeylessAccount.deserialize(Deserializer.fromHex(hex)); - } - - /** - * @deprecated Use `fromHex` instead. * Deserialize bytes using this account's information. * * @param bytes The bytes being interpreted. diff --git a/src/account/MultiKeyAccount.ts b/src/account/MultiKeyAccount.ts index ef876de5d..ff0bfdd39 100644 --- a/src/account/MultiKeyAccount.ts +++ b/src/account/MultiKeyAccount.ts @@ -4,18 +4,13 @@ import type { Account } from "./Account"; import { MultiKey, MultiKeySignature, PublicKey } from "../core/crypto"; import { AccountAddress, AccountAddressInput } from "../core/accountAddress"; -import { AnyPublicKeyVariant, HexInput, SigningScheme } from "../types"; +import { HexInput, SigningScheme } from "../types"; import { AccountAuthenticatorMultiKey } from "../transactions/authenticator/account"; import { AnyRawTransaction } from "../transactions/types"; import { AbstractKeylessAccount, KeylessSigner } from "./AbstractKeylessAccount"; import { AptosConfig } from "../api/aptosConfig"; -import { Serializable, Serializer } from "../bcs/serializer"; -import { Deserializer } from "../bcs/deserializer"; -import { deserializeSchemeAndAddress } from "./utils"; import { SingleKeyAccount, SingleKeySigner, SingleKeySignerOrLegacyEd25519Account } from "./SingleKeyAccount"; import { Ed25519Account } from "./Ed25519Account"; -import { KeylessAccount } from "./KeylessAccount"; -import { FederatedKeylessAccount } from "./FederatedKeylessAccount"; /** * Arguments required to verify a multi-key signature against a given message. @@ -36,7 +31,7 @@ export interface VerifyMultiKeySignatureArgs { * * Note: Generating a signer instance does not create the account on-chain. */ -export class MultiKeyAccount extends Serializable implements Account, KeylessSigner { +export class MultiKeyAccount implements Account, KeylessSigner { /** * Public key associated with the account */ @@ -81,7 +76,6 @@ export class MultiKeyAccount extends Serializable implements Account, KeylessSig signers: SingleKeySignerOrLegacyEd25519Account[]; address?: AccountAddressInput; }) { - super(); const { multiKey, address } = args; const signers: SingleKeySigner[] = args.signers.map((signer) => @@ -251,71 +245,4 @@ export class MultiKeyAccount extends Serializable implements Account, KeylessSig } return true; } - - serialize(serializer: Serializer): void { - serializer.serializeU32AsUleb128(this.signingScheme); - this.accountAddress.serialize(serializer); - this.publicKey.serialize(serializer); - serializer.serializeVector(this.signers); - } - - /** - * Deserialize bytes using this account's information. - * - * @param hex The hex being deserialized into an MultiKeyAccount. - * @returns - */ - static fromHex(hex: HexInput): MultiKeyAccount { - return MultiKeyAccount.deserialize(Deserializer.fromHex(hex)); - } - - static deserialize(deserializer: Deserializer): MultiKeyAccount { - const { address, signingScheme } = deserializeSchemeAndAddress(deserializer); - if (signingScheme !== SigningScheme.MultiKey) { - throw new Error( - `Deserialization of MultiKeyAccount failed: Signing scheme was not MultiKey, was ${signingScheme}`, - ); - } - const multiKey = MultiKey.deserialize(deserializer); - const length = deserializer.deserializeUleb128AsU32(); - const signers = new Array<SingleKeySignerOrLegacyEd25519Account>(); - for (let i = 0; i < length; i += 1) { - signers.push(deserializeNonMultiKeyAccount(deserializer)); - } - return new MultiKeyAccount({ multiKey, signers, address }); - } -} - -export function deserializeNonMultiKeyAccount(deserializer: Deserializer): SingleKeySignerOrLegacyEd25519Account { - const offset = deserializer.getOffset(); - const { signingScheme } = deserializeSchemeAndAddress(deserializer); - switch (signingScheme) { - case SigningScheme.Ed25519: - deserializer.reset(offset); - return Ed25519Account.deserialize(deserializer); - case SigningScheme.SingleKey: { - const anyKeyVariant = deserializer.deserializeUleb128AsU32(); - const anyKeyVariantOffset = deserializer.getOffset(); - deserializer.reset(offset); - switch (anyKeyVariant) { - case AnyPublicKeyVariant.Keyless: - return KeylessAccount.deserialize(deserializer); - case AnyPublicKeyVariant.FederatedKeyless: - return FederatedKeylessAccount.deserialize(deserializer); - case AnyPublicKeyVariant.Ed25519: - case AnyPublicKeyVariant.Secp256k1: - return SingleKeyAccount.deserialize(deserializer); - default: - throw new Error( - // eslint-disable-next-line max-len - `Deserialization of Account failed: AnyPublicKey variant ${anyKeyVariant} is invalid ending at offset ${anyKeyVariantOffset}.\n - ${JSON.stringify(deserializer, null, 2)}`, - ); - } - } - default: - throw new Error( - `Deserialization of Account failed: SigningScheme variant ${signingScheme} is invalid ending at offset ${offset}`, - ); - } } diff --git a/src/account/SingleKeyAccount.ts b/src/account/SingleKeyAccount.ts index 2a543193e..cc77b0f60 100644 --- a/src/account/SingleKeyAccount.ts +++ b/src/account/SingleKeyAccount.ts @@ -1,13 +1,10 @@ import { AccountAuthenticatorSingleKey } from "../transactions/authenticator/account"; -import { AnyPublicKeyVariant, type HexInput, SigningScheme, SigningSchemeInput } from "../types"; +import { type HexInput, SigningScheme, SigningSchemeInput } from "../types"; import { AccountAddress, AccountAddressInput } from "../core/accountAddress"; import { AnyPublicKey, AnySignature, Ed25519PrivateKey, PrivateKeyInput, Secp256k1PrivateKey } from "../core/crypto"; import type { Account } from "./Account"; import { generateSigningMessageForTransaction } from "../transactions/transactionBuilder/signingMessage"; import { AnyRawTransaction } from "../transactions/types"; -import { Serializable, Serializer } from "../bcs/serializer"; -import { Deserializer } from "../bcs/deserializer"; -import { deserializeSchemeAndAddress } from "./utils"; import { Ed25519Account } from "./Ed25519Account"; /** @@ -19,6 +16,15 @@ export interface SingleKeySigner extends Account { getAnyPublicKey(): AnyPublicKey; } +export function isSingleKeySigner(obj: unknown): obj is SingleKeySigner { + return ( + typeof obj === "object" && + obj !== null && + "getAnyPublicKey" in obj && + typeof (obj as any).getAnyPublicKey === "function" + ); +} + export type SingleKeySignerOrLegacyEd25519Account = SingleKeySigner | Ed25519Account; /** @@ -67,7 +73,7 @@ export interface VerifySingleKeySignatureArgs { * * Note: Generating a signer instance does not create the account on-chain. */ -export class SingleKeyAccount extends Serializable implements Account, SingleKeySigner { +export class SingleKeyAccount implements Account, SingleKeySigner { /** * Private key associated with the account */ @@ -88,7 +94,6 @@ export class SingleKeyAccount extends Serializable implements Account, SingleKey * @param args.address - The optional account address; if not provided, it will derive the address from the public key. */ constructor(args: SingleKeySignerConstructorArgs) { - super(); const { privateKey, address } = args; this.privateKey = privateKey; this.publicKey = new AnyPublicKey(privateKey.publicKey()); @@ -205,43 +210,6 @@ export class SingleKeyAccount extends Serializable implements Account, SingleKey // endregion - serialize(serializer: Serializer): void { - serializer.serializeU32AsUleb128(this.signingScheme); - this.accountAddress.serialize(serializer); - serializer.serializeU32AsUleb128(this.publicKey.variant); - this.privateKey.serialize(serializer); - } - - /** - * Deserialize bytes using this account's information. - * - * @param hex The hex being deserialized into an SingleKeyAccount. - * @returns - */ - static fromHex(hex: HexInput): SingleKeyAccount { - return SingleKeyAccount.deserialize(Deserializer.fromHex(hex)); - } - - static deserialize(deserializer: Deserializer): SingleKeyAccount { - const { address, signingScheme } = deserializeSchemeAndAddress(deserializer); - if (signingScheme !== SigningScheme.SingleKey) { - throw new Error(`Deserialization of SingleKeyAccount failed: Unsupported signing scheme ${signingScheme}`); - } - const variantIndex = deserializer.deserializeUleb128AsU32(); - let privateKey: PrivateKeyInput; - switch (variantIndex) { - case AnyPublicKeyVariant.Ed25519: - privateKey = Ed25519PrivateKey.deserialize(deserializer); - break; - case AnyPublicKeyVariant.Secp256k1: - privateKey = Secp256k1PrivateKey.deserialize(deserializer); - break; - default: - throw new Error(`Unsupported public key variant ${variantIndex}`); - } - return new SingleKeyAccount({ privateKey, address }); - } - static fromEd25519Account(account: Ed25519Account): SingleKeyAccount { return new SingleKeyAccount({ privateKey: account.privateKey, address: account.accountAddress }); } diff --git a/src/account/index.ts b/src/account/index.ts index d610bfc39..2893a580f 100644 --- a/src/account/index.ts +++ b/src/account/index.ts @@ -6,3 +6,4 @@ export * from "./KeylessAccount"; export * from "./AbstractKeylessAccount"; export * from "./FederatedKeylessAccount"; export * from "./MultiKeyAccount"; +export * from "./AccountUtils"; diff --git a/src/account/utils.ts b/src/account/utils.ts index d6755f1e9..9de9e3426 100644 --- a/src/account/utils.ts +++ b/src/account/utils.ts @@ -9,9 +9,7 @@ export function deserializeSchemeAndAddress(deserializer: Deserializer): { const signingScheme = deserializer.deserializeUleb128AsU32(); // Validate that signingScheme is a valid SigningScheme value if (!Object.values(SigningScheme).includes(signingScheme)) { - throw new Error( - `Deserialization of Account failed: SigningScheme variant ${signingScheme} is invalid ending at offset ${deserializer.getOffset()}`, - ); + throw new Error(`Deserialization of Account failed: SigningScheme variant ${signingScheme} is invalid`); } const address = AccountAddress.deserialize(deserializer); return { address, signingScheme }; diff --git a/src/bcs/deserializer.ts b/src/bcs/deserializer.ts index 92359d226..33d0b8c07 100644 --- a/src/bcs/deserializer.ts +++ b/src/bcs/deserializer.ts @@ -6,10 +6,6 @@ import { MAX_U32_NUMBER } from "./consts"; import { Uint8, Uint16, Uint32, Uint64, Uint128, Uint256, HexInput } from "../types"; import { Hex } from "../core/hex"; -export interface DeserializableAccount<T> extends Deserializable<T> { - fromHex(hex: HexInput): T; -} - /** * This interface exists to define Deserializable<T> inputs for functions that * deserialize a byte buffer into a type T. @@ -92,26 +88,7 @@ export class Deserializer { } /** - * Resets the deserializer's offset back to the beginning of the buffer. - * This allows reusing the same deserializer instance to read the buffer again from the start. - * - * @example - * ```typescript - * const deserializer = new Deserializer(new Uint8Array([1, 2, 3])); - * deserializer.deserializeU8(); // reads first byte - * deserializer.reset(); // resets to beginning - * deserializer.deserializeU8(); // reads first byte again - * ``` - */ - reset(offset?: number): void { - this.offset = offset ?? 0; - } - - getOffset(): number { - return this.offset; - } - - /** + * @deprecated use `deserializeOption` instead. * Deserializes a UTF-8 encoded string from a byte array. It first reads the length of the string in bytes, * followed by the actual byte content, and decodes it into a string. * diff --git a/tests/unit/accountSerialization.test.ts b/tests/unit/accountSerialization.test.ts index 31792a26b..dcfc4a0e1 100644 --- a/tests/unit/accountSerialization.test.ts +++ b/tests/unit/accountSerialization.test.ts @@ -5,8 +5,6 @@ import { Account, SigningSchemeInput, - Ed25519Account, - SingleKeyAccount, MultiKeyAccount, KeylessAccount, FederatedKeylessAccount, @@ -17,8 +15,13 @@ import { ZkpVariant, Groth16VerificationKey, } from "../../src"; +import { AccountUtils } from "../../src/account/AccountUtils"; -import { testAccountSerializationDeserialization } from "./helper"; +export function testAccountSerializationDeserialization(account: Account) { + const bytes = AccountUtils.toBytes(account); + const deserializedAccount = AccountUtils.fromBytes(bytes); + expect(bytes).toEqual(AccountUtils.toBytes(deserializedAccount)); +} describe("Account Serialization", () => { const proof = new ZeroKnowledgeSig({ @@ -74,36 +77,44 @@ describe("Account Serialization", () => { describe("serialize", () => { it("legacy Ed25519 Account should serlialize and deserialize properly", () => { - testAccountSerializationDeserialization(legacyEdAccount, Ed25519Account); - testAccountSerializationDeserialization(legacyEdAccount, Account); + testAccountSerializationDeserialization(legacyEdAccount); + const accountAsHex = AccountUtils.toHexString(legacyEdAccount); + expect(legacyEdAccount).toEqual(AccountUtils.ed25519AccountFromHex(accountAsHex)); }); it("SingleKey Ed25519 Account should serlialize and deserialize properly", () => { - testAccountSerializationDeserialization(singleSignerEdAccount, SingleKeyAccount); - testAccountSerializationDeserialization(singleSignerEdAccount, Account); + testAccountSerializationDeserialization(singleSignerEdAccount); + const accountAsHex = AccountUtils.toHexString(singleSignerEdAccount); + expect(singleSignerEdAccount).toEqual(AccountUtils.singleKeyAccountFromHex(accountAsHex)); }); it("SingleKey Secp256k1 Account should serlialize and deserialize properly", () => { - testAccountSerializationDeserialization(secp256k1Account, SingleKeyAccount); - testAccountSerializationDeserialization(secp256k1Account, Account); + testAccountSerializationDeserialization(secp256k1Account); + const accountAsHex = AccountUtils.toHexString(secp256k1Account); + expect(secp256k1Account).toEqual(AccountUtils.singleKeyAccountFromHex(accountAsHex)); }); it("Keyless Account should serlialize and deserialize properly", () => { - testAccountSerializationDeserialization(keylessAccount, KeylessAccount); - testAccountSerializationDeserialization(keylessAccount, Account); + testAccountSerializationDeserialization(keylessAccount); + const accountAsHex = AccountUtils.toHexString(keylessAccount); + expect(keylessAccount).toEqual(AccountUtils.keylessAccountFromHex(accountAsHex)); }); it("Keyless Account with verification key should serlialize and deserialize properly", () => { - testAccountSerializationDeserialization(keylessAccountWithVerificationKey, KeylessAccount); - testAccountSerializationDeserialization(keylessAccountWithVerificationKey, Account); + testAccountSerializationDeserialization(keylessAccountWithVerificationKey); + const accountAsHex = AccountUtils.toHexString(keylessAccountWithVerificationKey); + expect(keylessAccountWithVerificationKey).toEqual(AccountUtils.keylessAccountFromHex(accountAsHex)); }); it("FederatedKeyless Account should serlialize and deserialize properly", () => { - testAccountSerializationDeserialization(federatedKeylessAccount, FederatedKeylessAccount); - testAccountSerializationDeserialization(federatedKeylessAccount, Account); + testAccountSerializationDeserialization(federatedKeylessAccount); + const accountAsHex = AccountUtils.toHexString(federatedKeylessAccount); + expect(federatedKeylessAccount).toEqual(AccountUtils.federatedKeylessAccountFromHex(accountAsHex)); }); it("MultiKey Account should serlialize and deserialize properly", () => { - testAccountSerializationDeserialization(multiKeyAccount, MultiKeyAccount); - testAccountSerializationDeserialization(multiKeyAccount, Account); + testAccountSerializationDeserialization(multiKeyAccount); + const accountAsHex = AccountUtils.toHexString(multiKeyAccount); + expect(multiKeyAccount).toEqual(AccountUtils.multiKeyAccountFromHex(accountAsHex)); }); it("MultiKey Account with backup signer should serlialize and deserialize properly", () => { - testAccountSerializationDeserialization(keylessAccountWithBackupSigner, MultiKeyAccount); - testAccountSerializationDeserialization(keylessAccountWithBackupSigner, Account); + testAccountSerializationDeserialization(keylessAccountWithBackupSigner); + const accountAsHex = AccountUtils.toHexString(keylessAccountWithBackupSigner); + expect(keylessAccountWithBackupSigner).toEqual(AccountUtils.multiKeyAccountFromHex(accountAsHex)); }); }); }); diff --git a/tests/unit/helper.ts b/tests/unit/helper.ts index 94c9a01d1..2ff918c08 100644 --- a/tests/unit/helper.ts +++ b/tests/unit/helper.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 /* eslint-disable max-len */ -import { ClientRequest, ClientResponse, DeserializableAccount, Deserializer, Serializable } from "../../src"; +import { ClientRequest, ClientResponse } from "../../src"; export const FUND_AMOUNT = 100_000_000; export const TRANSFER_AMOUNT = 500_000; @@ -115,16 +115,6 @@ export const multiKeyTestObject = { export const longTestTimeout = 120 * 1000; -export function testAccountSerializationDeserialization<T extends Serializable, U extends Serializable>( - obj: T, - cls: DeserializableAccount<U>, -) { - const bytes = obj.bcsToBytes(); - const deserialized = cls.deserialize(new Deserializer(bytes)); - expect(bytes).toEqual(deserialized.bcsToBytes()); - expect(bytes).toEqual(cls.fromHex(bytes).bcsToBytes()); -} - export async function customClient<Req, Res>(requestOptions: ClientRequest<Req>): Promise<ClientResponse<Res>> { const { params, method, url, headers, body } = requestOptions;