Skip to content

Commit

Permalink
Merge branch 'develop' into feat/ws
Browse files Browse the repository at this point in the history
  • Loading branch information
tabaktoni authored Nov 26, 2024
2 parents ebb0d8a + cf8a0dd commit 1a9a1fd
Show file tree
Hide file tree
Showing 12 changed files with 892 additions and 32 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# [6.19.0](https://github.com/starknet-io/starknet.js/compare/v6.18.1...v6.19.0) (2024-11-26)

### Features

- Ledger signer 221 ([#1246](https://github.com/starknet-io/starknet.js/issues/1246)) ([03e2d50](https://github.com/starknet-io/starknet.js/commit/03e2d50edee7e2e9e8efb678a97effaf09adee70))

## [6.18.1](https://github.com/starknet-io/starknet.js/compare/v6.18.0...v6.18.1) (2024-11-25)

### Bug Fixes
Expand Down
20 changes: 17 additions & 3 deletions __tests__/utils/ethSigner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import {
encode,
eth,
extractContractHashes,
getLedgerPathBuffer,
getLedgerPathBuffer111,
getLedgerPathBuffer221,
hash,
num,
stark,
Expand Down Expand Up @@ -354,12 +355,25 @@ describe('Ethereum signer', () => {
describe('Ledger Signer', () => {
// signature of Ledger can't be tested automatically.
// So, just the test of the path encoding.
test('getLedgerPathBuffer', () => {
const path = getLedgerPathBuffer(3, 'AstroAPP');

// Ledger APP v1.1.1
test('getLedgerPathBuffer111', () => {
const path = getLedgerPathBuffer111(3, 'AstroAPP');
expect(path).toEqual(
new Uint8Array([
128, 0, 10, 85, 71, 65, 233, 201, 95, 192, 123, 107, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0,
])
);
});

// Ledger APP v2.2.1
test('getLedgerPathBuffer', () => {
const path = getLedgerPathBuffer221(3, 'AstroAPP');
expect(path).toEqual(
new Uint8Array([
128, 0, 10, 85, 199, 65, 233, 201, 223, 192, 123, 107, 128, 0, 0, 0, 128, 0, 0, 3, 0, 0, 0,
0,
])
);
});
});
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "starknet",
"version": "6.18.1",
"version": "6.19.0",
"description": "JavaScript library for Starknet",
"license": "MIT",
"repository": {
Expand Down
6 changes: 6 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,9 @@ export const SNIP9_V1_INTERFACE_ID =
'0x68cfd18b92d1907b8ba3cc324900277f5a3622099431ea85dd8089255e4181';
export const SNIP9_V2_INTERFACE_ID =
'0x1d1144bb2138366ff28d8e9ab57456b1d332ac42196230c3a602003c89872';

// Ledger signer
// 0x80
export const HARDENING_BYTE = 128;
// 0x80000000
export const HARDENING_4BYTES = 2147483648n;
8 changes: 7 additions & 1 deletion src/signer/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
export * from './interface';
export * from './default';
export * from './ethSigner';
export * from './ledgerSigner';
export {
LedgerSigner111,
getLedgerPathBuffer111,
LedgerSigner111 as LedgerSigner,
getLedgerPathBuffer111 as getLedgerPathBuffer,
} from './ledgerSigner111';
export { LedgerSigner221, getLedgerPathBuffer221 } from './ledgerSigner221';
142 changes: 126 additions & 16 deletions src/signer/ledgerSigner.ts → src/signer/ledgerSigner111.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
TypedData,
Call,
Signature,
LedgerPathCalculation,
} from '../types';
import assert from '../utils/assert';
import { CallData } from '../utils/calldata';
Expand All @@ -38,21 +39,23 @@ import { ETransactionVersion3 } from '../types/api';
type _Transport = any;

/**
* Signer for accounts using a Ledger Nano S+/X signature
* Signer for accounts using a Ledger Nano S+/X signature (Starknet Ledger APP version 1.1.1)
*
* The Ledger has to be connected, unlocked and the Starknet APP has to be selected prior of use of this class.
*/
export class LedgerSigner<Transport extends Record<any, any> = any> implements SignerInterface {
export class LedgerSigner111<Transport extends Record<any, any> = any> implements SignerInterface {
readonly transporter: Transport;

// this is a hack to allow the '@ledgerhq/hw-transport' type to be used as a dev dependency but not exposed in the production build
private _transporter: _Transport;
protected _transporter: _Transport;

readonly accountID: number;

readonly eip2645applicationName: string;

readonly pathBuffer: Uint8Array;

private appVersion: string;
protected appVersion: string;

protected pubKey: string;

Expand All @@ -63,16 +66,24 @@ export class LedgerSigner<Transport extends Record<any, any> = any> implements S
* @param {Transport} transport 5 transports are available to handle USB, bluetooth, Node, Web, Mobile.
* See Guides for more details.
* @param {number} accountID ID of Ledger Nano (can handle 2**31 accounts).
* @param {string} [eip2645application='LedgerW'] A wallet is defined by an ERC2645 derivation path (6 items).
* One item is the `application`. Default value is `LedgerW`.
* @param {string} [eip2645application='LedgerW'] A wallet is defined by an ERC2645 derivation path (6 items),
* and one item is the `application` and can be customized.
* Default value is `LedgerW`.
* @param {LedgerPathCalculation} [pathFunction=getLedgerPathBuffer111]
* defines the function that will calculate the path. By default `getLedgerPathBuffer111` is selected.
* @example
* ```typescript
* import TransportNodeHid from "@ledgerhq/hw-transport-node-hid";
* const myNodeTransport = await TransportNodeHid.create();
* const myLedgerSigner = new LedgerSigner(myNodeTransport, 0);
* const myLedgerSigner = new LedgerSigner111(myNodeTransport, 0);
* ```
*/
constructor(transport: Transport, accountID: number, eip2645application: string = 'LedgerW') {
constructor(
transport: Transport,
accountID: number,
eip2645application: string = 'LedgerW',
pathFunction: LedgerPathCalculation = getLedgerPathBuffer111
) {
assert(accountID >= 0, 'Ledger account ID shall not be a negative number.');
assert(accountID <= MASK_31, 'Ledger account ID shall be < 2**31.');
assert(!!eip2645application, 'Ledger application name shall not be empty.');
Expand All @@ -83,12 +94,17 @@ export class LedgerSigner<Transport extends Record<any, any> = any> implements S
this.fullPubKey = '';
this.eip2645applicationName = eip2645application;
this.appVersion = '';
this.pathBuffer = getLedgerPathBuffer(this.accountID, this.eip2645applicationName);
this.pathBuffer = pathFunction(this.accountID, this.eip2645applicationName);
}

/**
* provides the Starknet public key
* @returns an hex string : 64 characters are Point X coordinate.
* @example
* ```typescript
* const result = await myLedgerSigner.getPubKey();
* // result= "0x03681417ba3e1f050dd3ccdceb8d22b5e44fa70ee7844d472c6a768bded5174e"
* ```
*/
public async getPubKey(): Promise<string> {
if (!this.pubKey) await this.getPublicKeys();
Expand All @@ -98,6 +114,11 @@ export class LedgerSigner<Transport extends Record<any, any> = any> implements S
/**
* provides the full public key (with parity prefix)
* @returns an hex string : 2 first characters are the parity, the 64 following characters are Point X coordinate. 64 last characters are Point Y coordinate.
* @example
* ```typescript
* const result = await myLedgerSigner.getFullPubKey();
* // result= "0x0403681417ba3e1f050dd3ccdceb8d22b5e44fa70ee7844d472c6a768bded5174e03cbc86f805dcfcb0c1922dd4daf181afa289d86223a18bc856276615bcc7787"
* ```
*/
public async getFullPubKey(): Promise<string> {
if (!this.fullPubKey) await this.getPublicKeys();
Expand All @@ -121,11 +142,59 @@ export class LedgerSigner<Transport extends Record<any, any> = any> implements S
return this.appVersion;
}

/**
* Sign a TypedData message (SNIP-12) in a Ledger.
* @param {typedDataToHash} typedDataToHash A TypedData message compatible with SNIP-12.
* @param {string} accountAddress Signer account address (Hex or num string)
* @returns {Signature} The signed message.
* @example
* ```typescript
* const result = myLedgerSigner.signMessage(snip12Message, account0.address);
* // result = Signature { r: 611475243393396148729326917410546146405234155928298353899191529090923298688n,
* // s: 798839819213540985856952481651392652149797817551686626114697493101433761982n,
* // recovery: 0}
* ```
*/
public async signMessage(typedDataToHash: TypedData, accountAddress: string): Promise<Signature> {
const msgHash = getMessageHash(typedDataToHash, accountAddress);
return this.signRaw(msgHash);
}

/**
* Sign in a Ledger a V1 or a V3 transaction. This is a blind sign on the Ledger screen.
* @param {Call1[]} transactions An array of `Call` transactions (generated for example by `myContract.populate()`).
* @param {InvocationsSignerDetails} transactionsDetail An object that includes all the necessary inputs to hash the transaction. Can be `V2InvocationsSignerDetails` or `V3InvocationsSignerDetails` type.
* @returns {Signature} The signed transaction.
* @example
* ```typescript
* const txDetailsV3: V3InvocationsSignerDetails = {
* chainId: constants.StarknetChainId.SN_MAIN,
* nonce: "28",
* accountDeploymentData: [],
* paymasterData: [],
* cairoVersion: "1",
* feeDataAvailabilityMode: "L1",
* nonceDataAvailabilityMode: "L1",
* resourceBounds: {
* l1_gas: {
* max_amount: "0x2a00",
* max_price_per_unit: "0x5c00000"
* },
* l2_gas: {
* max_amount: "0x00",
* max_price_per_unit: "0x00"
* },
* },
* tip: 0,
* version: "0x3",
* walletAddress: account0.address
* }
* const result = myLedgerSigner.signTransaction([call0, call1], txDetailsV3);
* // result = Signature { r: 611475243393396148729326917410546146405234155928298353899191529090923298688n,
* // s: 798839819213540985856952481651392652149797817551686626114697493101433761982n,
* // recovery: 0}
* ```
*/
public async signTransaction(
transactions: Call[],
transactionsDetail: InvocationsSignerDetails
Expand Down Expand Up @@ -159,6 +228,18 @@ export class LedgerSigner<Transport extends Record<any, any> = any> implements S
return this.signRaw(msgHash as string);
}

/**
* Sign in a Ledger the deployment of a new account. This is a blind sign on the Ledger screen.
* @param {DeployAccountSignerDetails} details An object that includes all necessary data to calculate the Hash. It can be `V2DeployAccountSignerDetails` or `V3DeployAccountSignerDetails` types.
* @returns {Signature} The deploy account signature.
* @example
* ```typescript
* const result = myLedgerSigner.signDeployAccountTransaction(details);
* // result = Signature { r: 611475243393396148729326917410546146405234155928298353899191529090923298688n,
* // s: 798839819213540985856952481651392652149797817551686626114697493101433761982n,
* // recovery: 0}
* ```
*/
public async signDeployAccountTransaction(
details: DeployAccountSignerDetails
): Promise<Signature> {
Expand Down Expand Up @@ -191,12 +272,23 @@ export class LedgerSigner<Transport extends Record<any, any> = any> implements S
return this.signRaw(msgHash as string);
}

/**
* Sign in a Ledger the declaration of a new class. This is a blind sign on the Ledger screen.
* @param {DeclareSignerDetails} details An object that includes all necessary data to calculate the Hash. It can be `V3DeclareSignerDetails` or `V2DeclareSignerDetails` types.
* @returns {Signature} The declare Signature.
* @example
* ```typescript
* const result = myLedgerSigner.signDeclareTransaction(details);
* // result = Signature { r: 611475243393396148729326917410546146405234155928298353899191529090923298688n,
* // s: 798839819213540985856952481651392652149797817551686626114697493101433761982n,
* // recovery: 0}
* ```
*/
public async signDeclareTransaction(
// contractClass: ContractClass, // Should be used once class hash is present in ContractClass
details: DeclareSignerDetails
): Promise<Signature> {
let msgHash;

if (Object.values(ETransactionVersion2).includes(details.version as any)) {
const det = details as V2DeclareSignerDetails;
msgHash = calculateDeclareTransactionHash({
Expand All @@ -214,11 +306,14 @@ export class LedgerSigner<Transport extends Record<any, any> = any> implements S
} else {
throw Error('unsupported signDeclareTransaction version');
}

return this.signRaw(msgHash as string);
}

private async signRaw(msgHash: string): Promise<Signature> {
/**
* Internal function to sign a hash in a Ledger Nano.
* This is a blind sign in the Ledger ; no display of what you are signing.
*/
protected async signRaw(msgHash: string): Promise<Signature> {
addHexPrefix(
buf2hex(await this._transporter.send(Number('0x5a'), 2, 0, 0, Buffer.from(this.pathBuffer)))
);
Expand All @@ -236,7 +331,8 @@ export class LedgerSigner<Transport extends Record<any, any> = any> implements S
return sign1;
}

private async getPublicKeys() {
/** internal function to get both the Starknet public key and the full public key */
protected async getPublicKeys() {
const pathBuff = this.pathBuffer;
const respGetPublic = Uint8Array.from(
await this._transporter.send(Number('0x5a'), 1, 0, 0, Buffer.from(pathBuff))
Expand All @@ -247,13 +343,27 @@ export class LedgerSigner<Transport extends Record<any, any> = any> implements S
}

/**
* format the Ledger wallet path to an Uint8Array.
* EIP2645 path = 2645'/starknet'/application'/0'/accountId'/0
* Format the Ledger wallet path to an Uint8Array
* for a Ledger Starknet DAPP v1.1.1.
*
* EIP2645 path = 2645'/starknet/application/0/accountId/0
* @param {number} accountId Id of account. < 2**31.
* @param {string} [applicationName='LedgerW'] utf8 string of application name.
* @returns an Uint8array of 24 bytes.
* @example
* ```typescript
* const result = getLedgerPathBuffer111(0);
* // result = Uint8Array(24) [
* 128, 0, 10, 85, 71, 65, 233, 201,
* 43, 206, 231, 219, 0, 0, 0, 0,
* 0, 0, 0, 0, 0, 0, 0, 0
* ]
* ```
*/
export function getLedgerPathBuffer(accountId: number, applicationName: string): Uint8Array {
export function getLedgerPathBuffer111(
accountId: number,
applicationName: string = 'LedgerW'
): Uint8Array {
const path0buff = new Uint8Array([128, 0, 10, 85]); // "0x80000A55" EIP2645;
const path1buff = new Uint8Array([71, 65, 233, 201]); // "starknet"
const path2buff =
Expand Down
Loading

0 comments on commit 1a9a1fd

Please sign in to comment.