Skip to content

Commit

Permalink
Add account class for sdk v2
Browse files Browse the repository at this point in the history
  • Loading branch information
Jin committed Sep 11, 2023
1 parent 5c53294 commit c0bf162
Show file tree
Hide file tree
Showing 5 changed files with 389 additions and 0 deletions.
159 changes: 159 additions & 0 deletions ecosystem/typescript/sdk_v2/src/core/account.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// Copyright © Aptos Foundation
// SPDX-License-Identifier: Apache-2.0

import nacl from "tweetnacl";
import * as bip39 from "@scure/bip39";
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";

/**
* Class for creating and managing account on Aptos network
*
* 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
*/
private readonly _signingKey: nacl.SignKeyPair;

/**
* Account address associated with the account
*/
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 });
}

/**
* Private key of the account
*
* @returns Hex - private key of the account
*/
get privateKey(): Hex {
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: Hex): Account {
const keyPair = nacl.sign.keyPair.fromSeed(privateKey.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
*
* @param privateKey Hex - private key of the account
* @param address AccountAddress - address of the account
* @returns Account
*/
static fromPrivateKeyAndAddress(privateKey: Hex, address: AccountAddress): Account {
const signingKey = nacl.sign.keyPair.fromSeed(privateKey.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')
* Detailed description: {@link https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki}
* @param mnemonics.
* @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
*/
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: Uint8Array): Hex {
const pubKey = new Ed25519PublicKey(publicKey);
const authKey = AuthenticationKey.fromEd25519PublicKey(pubKey);
return authKey.data;
}

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

verifySignature(message: Hex, signature: Hex): boolean {
const rawMessage = message.toUint8Array();
const rawSignature = 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";
76 changes: 76 additions & 0 deletions ecosystem/typescript/sdk_v2/src/utils/hd-key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import nacl from "tweetnacl";
import { hmac } from "@noble/hashes/hmac";
import { sha512 } from "@noble/hashes/sha512";
import { hexToBytes } from "@noble/hashes/utils";

export type Keys = {
key: Uint8Array;
chainCode: Uint8Array;
};

const pathRegex = /^m(\/[0-9]+')+$/;

const replaceDerive = (val: string): string => val.replace("'", "");

const HMAC_KEY = "ed25519 seed";
const HARDENED_OFFSET = 0x80000000;

export const getMasterKeyFromSeed = (seed: string): Keys => {
const h = hmac.create(sha512, HMAC_KEY);
const I = h.update(hexToBytes(seed)).digest();
const IL = I.slice(0, 32);
const IR = I.slice(32);
return {
key: IL,
chainCode: IR,
};
};

export const CKDPriv = ({ key, chainCode }: Keys, index: number): Keys => {
const buffer = new ArrayBuffer(4);
new DataView(buffer).setUint32(0, index);
const indexBytes = new Uint8Array(buffer);
const zero = new Uint8Array([0]);
const data = new Uint8Array([...zero, ...key, ...indexBytes]);

const I = hmac.create(sha512, chainCode).update(data).digest();
const IL = I.slice(0, 32);
const IR = I.slice(32);
return {
key: IL,
chainCode: IR,
};
};

export const getPublicKey = (privateKey: Uint8Array, withZeroByte = true): Uint8Array => {
const keyPair = nacl.sign.keyPair.fromSeed(privateKey);
const signPk = keyPair.secretKey.subarray(32);
const zero = new Uint8Array([0]);
return withZeroByte ? new Uint8Array([...zero, ...signPk]) : signPk;
};

export const isValidPath = (path: string): boolean => {
if (!pathRegex.test(path)) {
return false;
}
return !path
.split("/")
.slice(1)
.map(replaceDerive)
.some(Number.isNaN as any);
};

export const derivePath = (path: string, seed: string, offset = HARDENED_OFFSET): Keys => {
if (!isValidPath(path)) {
throw new Error("Invalid derivation path");
}

const { key, chainCode } = getMasterKeyFromSeed(seed);
const segments = path
.split("/")
.slice(1)
.map(replaceDerive)
.map((el) => parseInt(el, 10));

return segments.reduce((parentKeys, segment) => CKDPriv(parentKeys, segment + offset), { key, chainCode });
};
2 changes: 2 additions & 0 deletions ecosystem/typescript/sdk_v2/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./hd-key";
export * from "./memoize-decorator";
Loading

0 comments on commit c0bf162

Please sign in to comment.