Skip to content
Draft
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
86 changes: 86 additions & 0 deletions packages/hdwallet-core/src/aptos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { addressNListToBIP32, slip44ByCoin } from "./utils";
import { BIP32Path, HDWallet, HDWalletInfo, PathDescription } from "./wallet";

export interface AptosGetAddress {
addressNList: BIP32Path;
showDisplay?: boolean;
}

export interface AptosSignTx {
addressNList: BIP32Path;
/** Transaction bytes to sign */
transactionBytes: Uint8Array;
}

export interface AptosSignedTx {
signature: string;
publicKey: string;
}

export interface AptosGetAccountPaths {
accountIdx: number;
}

export interface AptosAccountPath {
addressNList: BIP32Path;
}

export interface AptosWalletInfo extends HDWalletInfo {
readonly _supportsAptosInfo: boolean;

/**
* Returns a list of bip32 paths for a given account index in preferred order
* from most to least preferred.
*/
aptosGetAccountPaths(msg: AptosGetAccountPaths): Array<AptosAccountPath>;

/**
* Returns "next" account path, if any.
*/
aptosNextAccountPath(msg: AptosAccountPath): AptosAccountPath | undefined;
}

export interface AptosWallet extends AptosWalletInfo, HDWallet {
readonly _supportsAptos: boolean;

aptosGetAddress(msg: AptosGetAddress): Promise<string | null>;
aptosGetAddresses?(msgs: AptosGetAddress[]): Promise<string[]>;
aptosSignTx(msg: AptosSignTx): Promise<AptosSignedTx | null>;
}

export function aptosDescribePath(path: BIP32Path): PathDescription {
const pathStr = addressNListToBIP32(path);
const unknown: PathDescription = {
verbose: pathStr,
coin: "Aptos",
isKnown: false,
};

if (path.length != 5) return unknown;
if (path[0] != 0x80000000 + 44) return unknown;
if (path[1] != 0x80000000 + slip44ByCoin("Aptos")) return unknown;
if ((path[2] & 0x80000000) >>> 0 !== 0x80000000) return unknown;
if ((path[3] & 0x80000000) >>> 0 !== 0x80000000) return unknown;
if ((path[4] & 0x80000000) >>> 0 !== 0x80000000) return unknown;

const index = path[2] & 0x7fffffff;
return {
verbose: `Aptos Account #${index}`,
accountIdx: index,
wholeAccount: true,
coin: "Aptos",
isKnown: true,
};
}

// The standard derivation path for Aptos is: m/44'/637'/<account>'/0'/0'
// Aptos uses SLIP-0010 which requires all derivation to be hardened for Ed25519
// https://aptos.dev/concepts/accounts#accounts-and-signatures
export function aptosGetAccountPaths(msg: AptosGetAccountPaths): Array<AptosAccountPath> {
const slip44 = slip44ByCoin("Aptos");
return [
{
addressNList: [0x80000000 + 44, 0x80000000 + slip44, 0x80000000 + msg.accountIdx, 0x80000000 + 0, 0x80000000 + 0],
},
];
}
1 change: 1 addition & 0 deletions packages/hdwallet-core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from "./arkeo";
export * from "./aptos";
export * from "./binance";
export * from "./bitcoin";
export * from "./cosmos";
Expand Down
1 change: 1 addition & 0 deletions packages/hdwallet-core/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ export const slip44Table = Object.freeze({
Starknet: 9004,
Tron: 195,
Sui: 784,
Aptos: 637,
Near: 397,
// EVM chains all use the same SLIP44
Ethereum: 60,
Expand Down
9 changes: 9 additions & 0 deletions packages/hdwallet-core/src/wallet.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import isObject from "lodash/isObject";

import { ArkeoWallet, ArkeoWalletInfo } from "./arkeo";
import { AptosWallet, AptosWalletInfo } from "./aptos";
import { BinanceWallet, BinanceWalletInfo } from "./binance";
import { BTCInputScriptType, BTCWallet, BTCWalletInfo } from "./bitcoin";
import { CosmosWallet, CosmosWalletInfo } from "./cosmos";
Expand Down Expand Up @@ -304,6 +305,14 @@ export function supportsDebugLink(wallet: HDWallet): wallet is DebugLinkWallet {
return isObject(wallet) && (wallet as any)._supportsDebugLink;
}

export function supportsAptos(wallet: HDWallet): wallet is AptosWallet {
return isObject(wallet) && (wallet as any)._supportsAptos;
}

export function infoAptos(info: HDWalletInfo): info is AptosWalletInfo {
return isObject(info) && (info as any)._supportsAptosInfo;
}

export interface HDWalletInfo {
/**
* Retrieve the wallet's vendor string.
Expand Down
57 changes: 57 additions & 0 deletions packages/hdwallet-native/src/aptos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import * as core from "@shapeshiftoss/hdwallet-core";

import { Isolation } from "./crypto";
import { AptosAdapter } from "./crypto/isolation/adapters/aptos";
import { NativeHDWalletBase } from "./native";

export function MixinNativeAptosWalletInfo<TBase extends core.Constructor<core.HDWalletInfo>>(Base: TBase) {
// eslint-disable-next-line @typescript-eslint/no-shadow
return class MixinNativeAptosWalletInfo extends Base implements core.AptosWalletInfo {
readonly _supportsAptosInfo = true;

aptosGetAccountPaths(msg: core.AptosGetAccountPaths): Array<core.AptosAccountPath> {
return core.aptosGetAccountPaths(msg);
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
aptosNextAccountPath(_msg: core.AptosAccountPath): core.AptosAccountPath | undefined {
throw new Error("Method not implemented");
}
};
}

export function MixinNativeAptosWallet<TBase extends core.Constructor<NativeHDWalletBase>>(Base: TBase) {
// eslint-disable-next-line @typescript-eslint/no-shadow
return class MixinNativeAptosWallet extends Base {
readonly _supportsAptos = true;

aptosAdapter: AptosAdapter | undefined;

async aptosInitializeWallet(ed25519MasterKey: Isolation.Core.Ed25519.Node): Promise<void> {
const nodeAdapter = new Isolation.Adapters.Ed25519(ed25519MasterKey);
this.aptosAdapter = new AptosAdapter(nodeAdapter);
}

aptosWipe() {
this.aptosAdapter = undefined;
}

async aptosGetAddress(msg: core.AptosGetAddress): Promise<string | null> {
return this.needsMnemonic(!!this.aptosAdapter, () => {
return this.aptosAdapter!.getAddress(msg.addressNList);
});
}

async aptosSignTx(msg: core.AptosSignTx): Promise<core.AptosSignedTx | null> {
return this.needsMnemonic(!!this.aptosAdapter, async () => {
const signature = await this.aptosAdapter!.signTransaction(msg.transactionBytes, msg.addressNList);
const publicKey = await this.aptosAdapter!.getPublicKey(msg.addressNList);

return {
signature,
publicKey,
};
});
}
};
}
88 changes: 88 additions & 0 deletions packages/hdwallet-native/src/crypto/isolation/adapters/aptos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import * as core from "@shapeshiftoss/hdwallet-core";
import { createBLAKE2b } from "hash-wasm";

import { Isolation } from "../..";

const ED25519_PUBLIC_KEY_SIZE = 32;

export class AptosAdapter {
protected readonly nodeAdapter: Isolation.Adapters.Ed25519;

constructor(nodeAdapter: Isolation.Adapters.Ed25519) {
this.nodeAdapter = nodeAdapter;
}

/**
* Get Aptos address from BIP32 path
* Address generation:
* 1. Get Ed25519 public key (32 bytes)
* 2. Hash with BLAKE2b-256
* 3. Result is the 32-byte Aptos address (as hex string with 0x prefix)
*/
async getAddress(addressNList: core.BIP32Path): Promise<string> {
const nodeAdapter = await this.nodeAdapter.derivePath(core.addressNListToHardenedBIP32(addressNList));
const publicKey = await nodeAdapter.getPublicKey();

if (publicKey.length !== ED25519_PUBLIC_KEY_SIZE) {
throw new Error(`Invalid Ed25519 public key size: ${publicKey.length}`);
}

// Hash public key with BLAKE2b-256
const blake2b = await createBLAKE2b(256);
blake2b.init();
blake2b.update(publicKey);
const addressBytes = blake2b.digest("binary");

// Convert to hex string with 0x prefix
const addressHex = Array.from(addressBytes)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");

return "0x" + addressHex;
}

/**
* Sign Aptos transaction
* Transaction signing:
* 1. Receive transaction bytes
* 2. Hash with BLAKE2b-256
* 3. Sign hash with Ed25519 private key
* 4. Return signature as hex string
*/
async signTransaction(transactionBytes: Uint8Array, addressNList: core.BIP32Path): Promise<string> {
const nodeAdapter = await this.nodeAdapter.derivePath(core.addressNListToHardenedBIP32(addressNList));

// Hash transaction with BLAKE2b-256
const blake2b = await createBLAKE2b(256);
blake2b.init();
blake2b.update(transactionBytes);
const messageHash = blake2b.digest("binary");

// Sign the hash with Ed25519
const signature = await nodeAdapter.node.sign(messageHash);

// Convert signature to hex string
const signatureHex = Array.from(signature)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");

return signatureHex;
}

/**
* Get public key for address verification
*/
async getPublicKey(addressNList: core.BIP32Path): Promise<string> {
const nodeAdapter = await this.nodeAdapter.derivePath(core.addressNListToHardenedBIP32(addressNList));
const publicKey = await nodeAdapter.getPublicKey();

// Convert to hex string
const publicKeyHex = Array.from(publicKey)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");

return publicKeyHex;
}
}

export default AptosAdapter;
37 changes: 24 additions & 13 deletions packages/hdwallet-native/src/native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import isObject from "lodash/isObject";

import type { NativeAdapterArgs } from "./adapter";
import { MixinNativeArkeoWallet, MixinNativeArkeoWalletInfo } from "./arkeo";
import { MixinNativeAptosWallet, MixinNativeAptosWalletInfo } from "./aptos";
import { MixinNativeBinanceWallet, MixinNativeBinanceWalletInfo } from "./binance";
import { MixinNativeBTCWallet, MixinNativeBTCWalletInfo } from "./bitcoin";
import { MixinNativeCosmosWallet, MixinNativeCosmosWalletInfo } from "./cosmos";
Expand Down Expand Up @@ -135,13 +136,15 @@ class NativeHDWalletInfo
MixinNativeStarknetWalletInfo(
MixinNativeTronWalletInfo(
MixinNativeSuiWalletInfo(
MixinNativeNearWalletInfo(
MixinNativeThorchainWalletInfo(
MixinNativeMayachainWalletInfo(
MixinNativeSecretWalletInfo(
MixinNativeTerraWalletInfo(
MixinNativeKavaWalletInfo(
MixinNativeArkeoWalletInfo(MixinNativeOsmosisWalletInfo(NativeHDWalletBase))
MixinNativeAptosWalletInfo(
MixinNativeNearWalletInfo(
MixinNativeThorchainWalletInfo(
MixinNativeMayachainWalletInfo(
MixinNativeSecretWalletInfo(
MixinNativeTerraWalletInfo(
MixinNativeKavaWalletInfo(
MixinNativeArkeoWalletInfo(MixinNativeOsmosisWalletInfo(NativeHDWalletBase))
)
)
)
)
Expand All @@ -166,6 +169,7 @@ class NativeHDWalletInfo
core.StarknetWalletInfo,
core.TronWalletInfo,
core.SuiWalletInfo,
core.AptosWalletInfo,
core.NearWalletInfo,
core.ThorchainWalletInfo,
core.MayachainWalletInfo,
Expand Down Expand Up @@ -205,6 +209,8 @@ class NativeHDWalletInfo
return core.solanaDescribePath(msg.path);
case "sui":
return core.suiDescribePath(msg.path);
case "aptos":
return core.aptosDescribePath(msg.path);
case "near":
return core.nearDescribePath(msg.path);
case "secret":
Expand Down Expand Up @@ -246,12 +252,14 @@ export class NativeHDWallet
MixinNativeStarknetWallet(
MixinNativeTronWallet(
MixinNativeSuiWallet(
MixinNativeNearWallet(
MixinNativeThorchainWallet(
MixinNativeMayachainWallet(
MixinNativeSecretWallet(
MixinNativeTerraWallet(
MixinNativeKavaWallet(MixinNativeOsmosisWallet(MixinNativeArkeoWallet(NativeHDWalletInfo)))
MixinNativeAptosWallet(
MixinNativeNearWallet(
MixinNativeThorchainWallet(
MixinNativeMayachainWallet(
MixinNativeSecretWallet(
MixinNativeTerraWallet(
MixinNativeKavaWallet(MixinNativeOsmosisWallet(MixinNativeArkeoWallet(NativeHDWalletInfo)))
)
)
)
)
Expand All @@ -275,6 +283,7 @@ export class NativeHDWallet
core.StarknetWallet,
core.TronWallet,
core.SuiWallet,
core.AptosWallet,
core.NearWallet,
core.ThorchainWallet,
core.MayachainWallet,
Expand Down Expand Up @@ -421,6 +430,7 @@ export class NativeHDWallet
super.arkeoInitializeWallet(secp256k1MasterKey),
super.solanaInitializeWallet(ed25519MasterKey),
super.suiInitializeWallet(ed25519MasterKey),
super.aptosInitializeWallet(ed25519MasterKey),
super.nearInitializeWallet(ed25519MasterKey),
]);

Expand Down Expand Up @@ -465,6 +475,7 @@ export class NativeHDWallet

super.solanaWipe();
super.suiWipe();
super.aptosWipe();
super.nearWipe();
super.btcWipe();
super.ethWipe();
Expand Down