-
Notifications
You must be signed in to change notification settings - Fork 3.7k
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
[SDK] sdk v2 account classes #10005
Changes from all commits
cc371c7
d9d7aca
2a3fa07
853ab46
83a7ecb
109d06b
dbf2e51
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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"; | ||||||||||||
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 | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
*/ | ||||||||||||
private readonly _signingKey: nacl.SignKeyPair; | ||||||||||||
|
||||||||||||
/** | ||||||||||||
* Account address associated with the account | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 }); | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||||||||||||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see there are many places where we have to build the There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
* | ||||||||||||
* @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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
* Detailed description: {@link https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki} | ||||||||||||
* @param mnemonics. | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
*/ | ||||||||||||
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 { | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||||||||||||
} | ||||||||||||
} |
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() }); | ||
} | ||
} |
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); | ||
} | ||
} |
There was a problem hiding this comment.
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 avoidimport * from x
.