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

[SDK] sdk v2 account classes #10005

Closed
wants to merge 7 commits into from
Closed
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
163 changes: 163 additions & 0 deletions ecosystem/typescript/sdk_v2/src/core/account.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// Copyright © Aptos Foundation
// SPDX-License-Identifier: Apache-2.0

import nacl from "tweetnacl";
import * as bip39 from "@scure/bip39";
Copy link
Contributor

Choose a reason for hiding this comment

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

Any reason we don't just import bip39 and do bip39.blah? I feel like it'd be more readable, I'd rather we avoid import * from x.

import { AccountAddress } from "./account_address";
import { Memoize, derivePath } from "../utils";
import { Hex } from "./hex";
import { bytesToHex } from "@noble/hashes/utils";
import { Ed25519PublicKey } from "../crypto/ed25519";
import { AuthenticationKey } from "../crypto/authentication_key";
import { HexInput } from "../types";

/**
* Class for creating and managing account on Aptos network
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* Class for creating and managing account on Aptos network
* Class for creating and managing an account on Aptos network

Copy link
Contributor

Choose a reason for hiding this comment

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

Can you clarify that creating an instance of this account doesn't create an instance of the account on the network. Some more examples in the doc comment here would be nice, e.g. how to use this to create an account on the network, getting account address, etc.

*
* Use this class to create accounts, sign transactions, and more.
*/
export class Account {
/**
* signing key of the account, which holds the public and private key
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* signing key of the account, which holds the public and private key
* Signing key of the account, which holds the public and private key

*/
private readonly _signingKey: nacl.SignKeyPair;

/**
* Account address associated with the account
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe some more information and a link to the docs site explaining why this is necessary and can't just be derived from the private / public keys (because of rotation).

*/
private readonly _accountAddress: AccountAddress;

/**
* Public key of the account
*
* @returns Hex - public key of the account
*/
get publicKey(): Hex {
return new Hex({ data: this._signingKey.publicKey });
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think using obj for argument for a single input param seems odd. Personally I would love to see single input param without obj

Copy link
Contributor

Choose a reason for hiding this comment

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

yes we all mentioned it at some point... decided to keep it as is and later, when we actually test the sdk_v2 and use it we can decide how it is from a dev user perspective

}

/**
* Private key of the account
*
* @returns Hex - private key of the account
*/
get privateKey(): Hex {
Copy link
Contributor

Choose a reason for hiding this comment

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

Personally I think getters suck, I prefer just a good old function. Do we have guidelines on this Maayan?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah I am thinking of using getter here b/c that private and public key came from _signingKey. Not sure if we want to make the signingKey as public. Any thoughts?

return new Hex({ data: this._signingKey.secretKey });
}

/**
* Address of the account
*
* @returns AccountAddress - address of the account
*/
get accountAddress(): AccountAddress {
return this._accountAddress;
}

/**
* private constructor for Account
*
* This method is private because it should only be called by the factory static methods.
* @returns Account
*/
private constructor(keyPair: nacl.SignKeyPair, address: AccountAddress) {
this._signingKey = keyPair;
this._accountAddress = address;
}

/**
* Creates new account with random private key and address
*
* @returns Account
*/
static create(): Account {
const keyPair = nacl.sign.keyPair();
const address = new AccountAddress({ data: Account.authKey(keyPair.publicKey).toUint8Array() });
return new Account(keyPair, address);
}

/**
* Creates new account with provided private key
*
* @param privateKey Hex - private key of the account
* @returns Account
*/
static fromPrivateKey(privateKey: HexInput): Account {
const privatekeyHex = Hex.fromHexInput({ hexInput: privateKey });
Comment on lines +85 to +86
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I see there are many places where we have to build the Hex from HexInput. I wonder if we can just accept the Hex type as argument?

Copy link
Contributor

Choose a reason for hiding this comment

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

Hex is not a type but a Class. The whole idea is to use HexInput and then transform it to the desired format, this enables the greatest flexibility for the developer.

Copy link
Contributor

Choose a reason for hiding this comment

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

Beyond that, please make this take in an object as the argument. I know it's weird but we agreed we'd try this lol, let's see how it goes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep gotcha!

const keyPair = nacl.sign.keyPair.fromSeed(privatekeyHex.toUint8Array().slice(0, 32));
const address = new AccountAddress({ data: Account.authKey(keyPair.publicKey).toUint8Array() });
return new Account(keyPair, address);
}

/**
* Creates new account with provided private key and address
* This is intended to be used for account that has it's key rotated
Comment on lines +93 to +94
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* Creates new account with provided private key and address
* This is intended to be used for account that has it's key rotated
* Creates a new account with the provided private key and address.
* This is intended to be used for an account that has had it's key rotated.

*
* @param privateKey Hex - private key of the account
* @param address AccountAddress - address of the account
* @returns Account
*/
static fromPrivateKeyAndAddress(privateKey: HexInput, address: AccountAddress): Account {
const privatekeyHex = Hex.fromHexInput({ hexInput: privateKey });
const signingKey = nacl.sign.keyPair.fromSeed(privatekeyHex.toUint8Array().slice(0, 32));
return new Account(signingKey, address);
}

/**
* Creates new account with bip44 path and mnemonics,
* @param path. (e.g. m/44'/637'/0'/0'/0')
Comment on lines +107 to +108
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* Creates new account with bip44 path and mnemonics,
* @param path. (e.g. m/44'/637'/0'/0'/0')
* Creates new account with bip44 path and mnemonics.
*
* @param path. (e.g. m/44'/637'/0'/0'/0')

* Detailed description: {@link https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki}
* @param mnemonics.
Copy link
Contributor

Choose a reason for hiding this comment

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

More information about the mnemonics argument would be very helpful. If you could include examples in the doc comment that'd be great too.

* @returns AptosAccount
*/
static fromDerivationPath(path: string, mnemonics: string): Account {
if (!Account.isValidPath(path)) {
throw new Error("Invalid derivation path");
}

const normalizeMnemonics = mnemonics
.trim()
.split(/\s+/)
.map((part) => part.toLowerCase())
.join(" ");

const { key } = derivePath(path, bytesToHex(bip39.mnemonicToSeedSync(normalizeMnemonics)));

const signingKey = nacl.sign.keyPair.fromSeed(key.slice(0, 32));
const address = new AccountAddress({ data: Account.authKey(signingKey.publicKey).toUint8Array() });

return new Account(signingKey, address);
}

/**
* Check's if the derive path is valid
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* Check's if the derive path is valid
* Checks if the derive path is valid.

*/
static isValidPath(path: string): boolean {
return /^m\/44'\/637'\/[0-9]+'\/[0-9]+'\/[0-9]+'+$/.test(path);
}

/**
* This key enables account owners to rotate their private key(s)
* associated with the account without changing the address that hosts their account.
* See here for more info: {@link https://aptos.dev/concepts/accounts#single-signer-authentication}
* @returns Authentication key for the associated account
*/
@Memoize()
static authKey(publicKey: HexInput): Hex {
const pubKey = new Ed25519PublicKey(publicKey);
const authKey = AuthenticationKey.fromEd25519PublicKey(pubKey);
return authKey.data;
}

sign(data: HexInput): Hex {
Copy link
Contributor

Choose a reason for hiding this comment

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

Doc comments on this and veryifySignature please captain!

const hex = Hex.fromHexInput({ hexInput: data });
const signature = nacl.sign.detached(hex.toUint8Array(), this._signingKey.secretKey);
return new Hex({ data: signature });
}

verifySignature(message: HexInput, signature: HexInput): boolean {
const rawMessage = Hex.fromHexInput({ hexInput: message }).toUint8Array();
const rawSignature = Hex.fromHexInput({ hexInput: signature }).toUint8Array();
return nacl.sign.detached.verify(rawMessage, rawSignature, this._signingKey.publicKey);
}
}
1 change: 1 addition & 0 deletions ecosystem/typescript/sdk_v2/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
// SPDX-License-Identifier: Apache-2.0

export * from "./account_address";
export * from "./account";
export * from "./common";
export * from "./hex";
74 changes: 74 additions & 0 deletions ecosystem/typescript/sdk_v2/src/crypto/authentication_key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright © Aptos Foundation
// SPDX-License-Identifier: Apache-2.0

import { sha3_256 as sha3Hash } from "@noble/hashes/sha3";
import { AccountAddress, Hex } from "../core";
import { HexInput } from "../types";
import { MultiEd25519PublicKey } from "./multi_ed25519";
import { Ed25519PublicKey } from "./ed25519";

/**
* Each account stores an authentication key. Authentication key enables account owners to rotate
* their private key(s) associated with the account without changing the address that hosts their account.
* @see {@link * https://aptos.dev/concepts/accounts | Account Basics}
*
* Account addresses can be derived from AuthenticationKey
*/
export class AuthenticationKey {
static readonly LENGTH: number = 32;

static readonly MULTI_ED25519_SCHEME: number = 1;

static readonly ED25519_SCHEME: number = 0;

static readonly DERIVE_RESOURCE_ACCOUNT_SCHEME: number = 255;

readonly data: Hex;

constructor(hexInput: HexInput) {
const hex = Hex.fromHexInput({ hexInput });
if (hex.toUint8Array().length !== AuthenticationKey.LENGTH) {
throw new Error("Expected a hexinput of length 32");
}
this.data = hex;
}

/**
* Converts a K-of-N MultiEd25519PublicKey to AuthenticationKey with:
* `auth_key = sha3-256(p_1 | … | p_n | K | 0x01)`. `K` represents the K-of-N required for
* authenticating the transaction. `0x01` is the 1-byte scheme for multisig.
*/
static fromMultiEd25519PublicKey(publicKey: MultiEd25519PublicKey): AuthenticationKey {
const pubKeyBytes = publicKey.toUint8Array();

const bytes = new Uint8Array(pubKeyBytes.length + 1);
bytes.set(pubKeyBytes);
bytes.set([AuthenticationKey.MULTI_ED25519_SCHEME], pubKeyBytes.length);

const hash = sha3Hash.create();
hash.update(bytes);

return new AuthenticationKey(hash.digest());
}

static fromEd25519PublicKey(publicKey: Ed25519PublicKey): AuthenticationKey {
const pubKeyBytes = publicKey.value.toUint8Array();

const bytes = new Uint8Array(pubKeyBytes.length + 1);
bytes.set(pubKeyBytes);
bytes.set([AuthenticationKey.ED25519_SCHEME], pubKeyBytes.length);

const hash = sha3Hash.create();
hash.update(bytes);

return new AuthenticationKey(hash.digest());
}

/**
* Derives an account address from AuthenticationKey. Since current AccountAddress is 32 bytes,
* AuthenticationKey bytes are directly translated to AccountAddress.
*/
derivedAddress(): AccountAddress {
return new AccountAddress({ data: this.data.toUint8Array() });
}
}
52 changes: 52 additions & 0 deletions ecosystem/typescript/sdk_v2/src/crypto/ed25519.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright © Aptos Foundation
// SPDX-License-Identifier: Apache-2.0

import { Deserializer, Serializer } from "../bcs";
import { Hex } from "../core";
import { HexInput } from "../types";

export class Ed25519PublicKey {
static readonly LENGTH: number = 32;

readonly value: Hex;

constructor(hexInput: HexInput) {
const hex = Hex.fromHexInput({ hexInput });
if (hex.toUint8Array().length !== Ed25519PublicKey.LENGTH) {
throw new Error(`Ed25519PublicKey length should be ${Ed25519PublicKey.LENGTH}`);
}
this.value = hex;
}

toUint8Array(): Uint8Array {
return this.value.toUint8Array();
}

serialize(serializer: Serializer): void {
serializer.serializeBytes(this.value.toUint8Array());
}

static deserialize(deserializer: Deserializer): Ed25519PublicKey {
const value = deserializer.deserializeBytes();
return new Ed25519PublicKey(value);
}
}

export class Ed25519Signature {
static readonly LENGTH = 64;

constructor(public readonly value: Uint8Array) {
if (value.length !== Ed25519Signature.LENGTH) {
throw new Error(`Ed25519Signature length should be ${Ed25519Signature.LENGTH}`);
}
}

serialize(serializer: Serializer): void {
serializer.serializeBytes(this.value);
}

static deserialize(deserializer: Deserializer): Ed25519Signature {
const value = deserializer.deserializeBytes();
return new Ed25519Signature(value);
}
}
Loading