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

wip: solana native support #702

Draft
wants to merge 24 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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
4 changes: 3 additions & 1 deletion packages/hdwallet-native/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
},
"dependencies": {
"@bitcoinerlab/secp256k1": "^1.1.1",
"@noble/curves": "^1.4.0",
Copy link
Contributor Author

@gomesalexandre gomesalexandre Dec 23, 2024

Choose a reason for hiding this comment

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

note @noble/ed25519 is also available as a lightweight version but @noble/curves has more safety/features implemented

"@shapeshiftoss/bitcoinjs-lib": "7.0.0-shapeshift.0",
"@shapeshiftoss/fiosdk": "1.2.1-shapeshift.6",
"@shapeshiftoss/hdwallet-core": "1.57.1",
Expand Down Expand Up @@ -57,5 +58,6 @@
"bs58": "^4.0.1",
"cosmjs-types": "^0.4.1",
"msw": "^0.27.1"
}
},
"gitHead": "a59c5a12b265b6f64c65920cf330358a250227c2"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

revert before opening me

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Ed25519Node } from "../core/ed25519";
import { ByteArray } from "../types";

export class BIP32Ed25519Adapter {
readonly node: Ed25519Node;

private constructor(node: Ed25519Node) {
this.node = node;
}

static async fromNode(node: Ed25519Node): Promise<BIP32Ed25519Adapter> {
return new BIP32Ed25519Adapter(node);
}

async getPublicKey(): Promise<ByteArray> {
const publicKey = await this.node.getPublicKey();
return Buffer.from(publicKey);
}

async derivePath(path: string): Promise<BIP32Ed25519Adapter> {
let currentNode = this.node;

if (path === "m" || path === "M" || path === "m'" || path === "M'") {
return this;
}

const segments = path
.toLowerCase()
.split("/")
.filter((segment) => segment !== "m");

for (const segment of segments) {
const index = parseInt(segment.replace("'", ""));
currentNode = await currentNode.derive(index);
}

return new BIP32Ed25519Adapter(currentNode);
}
}

export default BIP32Ed25519Adapter;
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export { default as FIO } from "./fio";
export { default as Binance } from "./binance";
export { default as Cosmos } from "./cosmos";
export { default as CosmosDirect } from "./cosmosDirect";
export { default as SolanaDirect } from "./solana";
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import * as core from "@shapeshiftoss/hdwallet-core";
import { PublicKey, VersionedTransaction } from "@solana/web3.js";

import BIP32Ed25519Adapter from "./bip32ed25519";

export class SolanaDirectAdapter {
protected readonly nodeAdapter: BIP32Ed25519Adapter;

constructor(nodeAdapter: BIP32Ed25519Adapter) {
this.nodeAdapter = nodeAdapter;
}

async getAddress(addressNList: core.BIP32Path): Promise<string> {
const nodeAdapter = await this.nodeAdapter.derivePath(core.addressNListToBIP32(addressNList));
const publicKeyBuffer = await nodeAdapter.getPublicKey();

const bufferForHex = Buffer.from(publicKeyBuffer);

const pubKey = new PublicKey(bufferForHex);

return pubKey.toBase58();
}

async signDirect(transaction: VersionedTransaction, addressNList: core.BIP32Path): Promise<VersionedTransaction> {
const nodeAdapter = await this.nodeAdapter.derivePath(core.addressNListToBIP32(addressNList));
const pubkey = await this.getAddress(addressNList);

const messageToSign = transaction.message.serialize();
const signature = await nodeAdapter.node.sign(messageToSign);

transaction.addSignature(new PublicKey(pubkey), signature);
return transaction;
}
}

export default SolanaDirectAdapter;
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import * as core from "@shapeshiftoss/hdwallet-core";

import { Revocable } from "..";
import { Ed25519Node } from "../ed25519";
import * as SecP256K1 from "../secp256k1";
import { ChainCode } from ".";

export interface Seed extends Partial<Revocable> {
toMasterKey(hmacKey?: string | Uint8Array): Promise<Node>;
toEd25519MasterKey(): Promise<Ed25519Node>;
}

export interface Node extends Partial<Revocable>, SecP256K1.ECDSAKey, Partial<SecP256K1.ECDHKey> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { ExtPointConstructor } from "@noble/curves/abstract/edwards";
import { ed25519 } from "@noble/curves/ed25519";
import * as bip32crypto from "bip32/src/crypto";

import { Revocable, revocable } from "../../engines/default/revocable";
import { ByteArray } from "../../types";

export type Ed25519Key = {
getPublicKey(): Promise<ByteArray>;
sign(message: Uint8Array): Promise<ByteArray>;
verify(message: Uint8Array, signature: Uint8Array): Promise<boolean>;
};

export class Ed25519Node extends Revocable(class {}) {
readonly #privateKey: ByteArray;
readonly #chainCode: ByteArray;
readonly explicitPath?: string;

protected constructor(privateKey: ByteArray, chainCode: ByteArray, explicitPath?: string) {
super();
this.#privateKey = privateKey;
this.#chainCode = chainCode;
this.explicitPath = explicitPath;
}

static async create(privateKey: ByteArray, chainCode: ByteArray, explicitPath?: string): Promise<Ed25519Node> {
const obj = new Ed25519Node(privateKey, chainCode, explicitPath);
return revocable(obj, (x) => obj.addRevoker(x));
}

async getPublicKey(): Promise<ByteArray> {
// Generate public key from private key
return Buffer.from(ed25519.getPublicKey(this.#privateKey));
}

async getChainCode(): Promise<ByteArray> {
return this.#chainCode;
}

async sign(message: Uint8Array): Promise<ByteArray> {
return Buffer.from(ed25519.sign(message, this.#privateKey));
}

async derive(index: number): Promise<Ed25519Node> {
// Ensure hardened derivation
if (index < 0x80000000) {
index += 0x80000000;
}

const indexBuffer = Buffer.alloc(4);
indexBuffer.writeUInt32BE(index, 0);

// SLIP-0010 for Ed25519
const data = Buffer.concat([Buffer.from([0x00]), Buffer.from(this.#privateKey), indexBuffer]);
Comment on lines +53 to +54
Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's the part we probably got wrong


const I = bip32crypto.hmacSHA512(Buffer.from(this.#chainCode), data);
const IL = I.slice(0, 32);
const IR = I.slice(32);

// Ed25519 clamping
IL[0] &= 0xf8;
IL[31] &= 0x7f;
IL[31] |= 0x40;

const path = this.explicitPath
? `${this.explicitPath}/${index >= 0x80000000 ? index - 0x80000000 + "'" : index}`
: undefined;

return Ed25519Node.create(IL, IR, path);
}
}

export type Point = ExtPointConstructor;
export const Ed25519Point = {
BASE_POINT: ed25519.getPublicKey(new Uint8Array(32)),
};
export type { Point as ExtendedPoint };
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as bip32crypto from "bip32/src/crypto";
import { TextEncoder } from "web-encoding";

import { BIP32, Digest, SecP256K1 } from "../../core";
import { Ed25519Node } from "../../core/ed25519";
import { assertType, ByteArray, checkType, safeBufferFrom, Uint32 } from "../../types";
import { Revocable, revocable } from "./revocable";

Expand Down Expand Up @@ -172,4 +173,16 @@ export class Seed extends Revocable(class {}) implements BIP32.Seed {
this.addRevoker(() => out.revoke?.());
return out;
}

async toEd25519MasterKey(): Promise<Ed25519Node> {
// Use Ed25519-specific HMAC key
// https://github.com/trezor/trezor-crypto/blob/master/bip32.c#L56
const hmacKey = safeBufferFrom(new TextEncoder().encode("ed25519 seed"));
const I = safeBufferFrom(bip32crypto.hmacSHA512(hmacKey, this.#seed));
const IL = I.slice(0, 32);
const IR = I.slice(32, 64);
const out = await Ed25519Node.create(IL, IR);
this.addRevoker(() => out.revoke?.());
return out;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { TextEncoder } from "web-encoding";

import type { Seed as SeedType } from "../../core/bip32";
import type { Mnemonic as Bip39Mnemonic } from "../../core/bip39";
import { Ed25519Node } from "../../core/ed25519";
import { Seed } from "./bip32";
import { Revocable, revocable } from "./revocable";

Expand Down Expand Up @@ -42,4 +43,8 @@ export class Mnemonic extends Revocable(class {}) implements Bip39Mnemonic {
this.addRevoker(() => out.revoke?.());
return out;
}
async toEd25519MasterKey(passphrase?: string): Promise<Ed25519Node> {
const seed = await this.toSeed(passphrase);
return seed.toEd25519MasterKey();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { toArrayBuffer } from "@shapeshiftoss/hdwallet-core";
import * as bip32crypto from "bip32/src/crypto";

import { BIP32, Digest, SecP256K1 } from "../../core";
import { Ed25519Node } from "../../core/ed25519";
import { ByteArray, checkType, safeBufferFrom, Uint32 } from "../../types";
import { DummyEngineError, ParsedXpubTree } from "./types";

Expand Down Expand Up @@ -115,4 +116,10 @@ export class Seed implements BIP32.Seed {

return await Node.create(this.xpubTree);
}

toEd25519MasterKey(): Promise<Ed25519Node>;
async toEd25519MasterKey(): Promise<Ed25519Node> {
const edKey = await Ed25519Node.create(this.xpubTree.publicKey, this.xpubTree.chainCode);
return edKey;
}
}
Loading