Skip to content

Commit 07632b2

Browse files
authored
Merge pull request #6 from blooo-io/feat/LDG-486-js-lib-erc4361-message-support
Feat/ldg 486 js lib erc4361 message support
2 parents 5ec9fb3 + bfde791 commit 07632b2

File tree

6 files changed

+173
-48
lines changed

6 files changed

+173
-48
lines changed

README.md

+71-41
Original file line numberDiff line numberDiff line change
@@ -57,66 +57,72 @@ For a smooth and quick integration:
5757
* [Parameters](#parameters-9)
5858
* [signWithdrawal](#signwithdrawal)
5959
* [Parameters](#parameters-10)
60+
* [Examples](#examples-7)
61+
* [signERC4361Message](#signerc4361message)
62+
* [Parameters](#parameters-11)
63+
* [Examples](#examples-8)
6064
* [AcreBtcNew](#acrebtcnew)
6165
* [getWalletXpub](#getwalletxpub-1)
62-
* [Parameters](#parameters-11)
63-
* [getWalletPublicKey](#getwalletpublickey-1)
6466
* [Parameters](#parameters-12)
65-
* [createPaymentTransaction](#createpaymenttransaction-1)
67+
* [getWalletPublicKey](#getwalletpublickey-1)
6668
* [Parameters](#parameters-13)
67-
* [signMessage](#signmessage-1)
69+
* [createPaymentTransaction](#createpaymenttransaction-1)
6870
* [Parameters](#parameters-14)
69-
* [signWithdrawal](#signwithdrawal-1)
71+
* [signMessage](#signmessage-1)
7072
* [Parameters](#parameters-15)
73+
* [signWithdrawal](#signwithdrawal-1)
74+
* [Parameters](#parameters-16)
75+
* [signERC4361Message](#signerc4361message-1)
76+
* [Parameters](#parameters-17)
7177
* [descrTemplFrom](#descrtemplfrom)
72-
* [Parameters](#parameters-16)
78+
* [Parameters](#parameters-18)
7379
* [AcreBtcOld](#acrebtcold)
7480
* [getWalletPublicKey](#getwalletpublickey-2)
75-
* [Parameters](#parameters-17)
76-
* [Examples](#examples-7)
81+
* [Parameters](#parameters-19)
82+
* [Examples](#examples-9)
7783
* [createPaymentTransaction](#createpaymenttransaction-2)
78-
* [Parameters](#parameters-18)
79-
* [Examples](#examples-8)
84+
* [Parameters](#parameters-20)
85+
* [Examples](#examples-10)
8086
* [CreateTransactionArg](#createtransactionarg)
8187
* [Properties](#properties)
8288
* [AddressFormat](#addressformat)
8389
* [AcreWithdrawalData](#acrewithdrawaldata)
8490
* [Properties](#properties-1)
8591
* [AccountType](#accounttype)
8692
* [spendingCondition](#spendingcondition)
87-
* [Parameters](#parameters-19)
93+
* [Parameters](#parameters-21)
8894
* [setInput](#setinput)
89-
* [Parameters](#parameters-20)
95+
* [Parameters](#parameters-22)
9096
* [setOwnOutput](#setownoutput)
91-
* [Parameters](#parameters-21)
97+
* [Parameters](#parameters-23)
9298
* [getDescriptorTemplate](#getdescriptortemplate)
9399
* [SingleKeyAccount](#singlekeyaccount)
94100
* [getTaprootOutputKey](#gettaprootoutputkey)
95-
* [Parameters](#parameters-22)
101+
* [Parameters](#parameters-24)
96102
* [AppClient](#appclient)
97-
* [Parameters](#parameters-23)
103+
* [Parameters](#parameters-25)
98104
* [ClientCommandInterpreter](#clientcommandinterpreter)
99-
* [Parameters](#parameters-24)
105+
* [Parameters](#parameters-26)
100106
* [MerkelizedPsbt](#merkelizedpsbt)
101-
* [Parameters](#parameters-25)
107+
* [Parameters](#parameters-27)
102108
* [Merkle](#merkle)
103-
* [Parameters](#parameters-26)
109+
* [Parameters](#parameters-28)
104110
* [MerkleMap](#merklemap)
105-
* [Parameters](#parameters-27)
111+
* [Parameters](#parameters-29)
106112
* [WalletPolicy](#walletpolicy)
107-
* [Parameters](#parameters-28)
113+
* [Parameters](#parameters-30)
108114
* [extract](#extract)
109-
* [Parameters](#parameters-29)
115+
* [Parameters](#parameters-31)
110116
* [finalize](#finalize)
111-
* [Parameters](#parameters-30)
117+
* [Parameters](#parameters-32)
112118
* [clearFinalizedInput](#clearfinalizedinput)
113-
* [Parameters](#parameters-31)
119+
* [Parameters](#parameters-33)
114120
* [writePush](#writepush)
115-
* [Parameters](#parameters-32)
121+
* [Parameters](#parameters-34)
116122
* [PsbtV2](#psbtv2)
117123
* [serializeTransactionOutputs](#serializetransactionoutputs-1)
118-
* [Parameters](#parameters-33)
119-
* [Examples](#examples-9)
124+
* [Parameters](#parameters-35)
125+
* [Examples](#examples-11)
120126
* [SignP2SHTransactionArg](#signp2shtransactionarg)
121127
* [Properties](#properties-2)
122128
* [TransactionInput](#transactioninput)
@@ -353,8 +359,6 @@ and returns v, r, s.
353359
* `$0.path`  
354360
* `$0.withdrawalData`  
355361

356-
Returns **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)<{v: [number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number), r: [string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String), s: [string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)}>**&#x20;
357-
358362
##### Examples
359363

360364
```javascript
@@ -377,6 +381,31 @@ Returns **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/
377381
}).catch(function(ex) {console.log(ex);});
378382
```
379383

384+
#### signERC4361Message
385+
386+
Signs an Ethereum Sign-In (ERC-4361) message with the private key at
387+
the provided derivation path according to the Bitcoin Signature format
388+
and returns v, r, s.
389+
390+
##### Parameters
391+
392+
* `path` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)**&#x20;
393+
* `messageHex` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)**&#x20;
394+
395+
##### Examples
396+
397+
```javascript
398+
acre.signERC4361Message("44'/60'/0'/0'/0", Buffer.from("Example ERC-4361 message").toString("hex")).then(function(result) {
399+
const v = result['v'] + 27 + 4;
400+
const signature = Buffer.from(v.toString(16) + result['r'] + result['s'], 'hex').toString('base64');
401+
console.log("Signature : " + signature);
402+
}).catch(function(ex) {console.log(ex);});
403+
```
404+
405+
Returns **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)<{v: [number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number), r: [string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String), s: [string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)}>**&#x20;
406+
407+
**Note:** The message is restricted to maximum 128 character lines.
408+
380409
Returns **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)<{v: [number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number), r: [string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String), s: [string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)}>**&#x20;
381410

382411

@@ -492,27 +521,28 @@ and returns v, r, s.
492521

493522
Returns **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)<{v: [number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number), r: [string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String), s: [string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)}>**&#x20;
494523

495-
### descrTemplFrom
524+
#### signERC4361Message
496525

497-
This function returns a descriptor template based on the address format.
498-
See <https://github.com/blooo-io/app-acre/blob/develop/doc/wallet.md> for details of
499-
the bitcoin descriptor template.
526+
Signs an ERC-4361 (Sign-In with Ethereum) formatted message with the private key at
527+
the provided derivation path according to the Bitcoin Signature format
528+
and returns v, r, s.
500529

501-
#### Parameters
530+
##### Parameters
502531

503-
* `addressFormat` **[AddressFormat](#addressformat)**&#x20;
532+
* `$0` **{path: [string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String), messageHex: [string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)}**
504533

505-
Returns **DefaultDescriptorTemplate**&#x20;
534+
* `$0.path` The BIP32 derivation path of the key to use for signing
535+
* `$0.messageHex` The ERC-4361 formatted message to sign, in hexadecimal format
506536

507-
### AcreBtcOld
537+
Returns **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)<{v: [number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number), r: [string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String), s: [string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)}>**&#x20;
508538

509-
This old API is compatible with versions of the Bitcoin nano app that are earlier than 2.1.0 .
510-
It is never used by Acre, that is based on the latest Bitcoin nano app (2.1.0+).
511-
This class is kept for compatibility purposes.
539+
### descrTemplFrom
512540

513-
#### getWalletPublicKey
541+
This function returns a descriptor template based on the address format.
542+
See <https://github.com/blooo-io/app-acre/blob/develop/doc/wallet.md> for details of
543+
the bitcoin descriptor template.
514544

515-
##### Parameters
545+
#### Parameters
516546

517547
* `path` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** a BIP 32 path
518548
* `opts` **{verify: [boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)?, format: [AddressFormat](#addressformat)?}?**&#x20;

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@blooo/hw-app-acre",
3-
"version": "1.0.1",
3+
"version": "1.1.0",
44
"description": "Ledger Hardware Wallet Acre Application API",
55
"keywords": [
66
"Ledger",

src/AcreBtcNew.ts

+27-3
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,7 @@ export default class AcreBtcNew {
313313
s,
314314
};
315315
}
316+
316317
cleanHexPrefix(hexString: string): string {
317318
let cleanedHex = hexString.startsWith("0x") ? hexString.slice(2) : hexString;
318319
if (cleanedHex.length % 2 !== 0) {
@@ -322,8 +323,6 @@ export default class AcreBtcNew {
322323
}
323324

324325
formatAcreWithdrawalData(withdrawalData: AcreWithdrawalData): AcreWithdrawalDataBuffer {
325-
console.log("withdrawalData", withdrawalData);
326-
console.log("dataLength", withdrawalData.data.length);
327326
const to = Buffer.from(this.cleanHexPrefix(withdrawalData.to.toString()), "hex").slice(-20);
328327

329328
let withdrawalValueBuffer = Buffer.from(this.cleanHexPrefix(withdrawalData.value), "hex").slice(-32);
@@ -385,7 +384,6 @@ export default class AcreBtcNew {
385384
}> {
386385
const pathElements: number[] = pathStringToArray(path);
387386
const withdrawalDataBuffer = this.formatAcreWithdrawalData(withdrawalData);
388-
console.log("withdrawalDataBuffer", withdrawalDataBuffer);
389387

390388
const sig = await this.client.signWithdrawal(pathElements, withdrawalDataBuffer);
391389
const buf = Buffer.from(sig, "base64");
@@ -401,6 +399,32 @@ export default class AcreBtcNew {
401399
};
402400
}
403401

402+
/**
403+
* Signs a ERC4361 hex-formatted message with the private key at
404+
* the provided derivation path according to the Bitcoin Signature format
405+
* and returns v, r, s.
406+
*/
407+
async signERC4361Message({ path, messageHex }: { path: string; messageHex: string }): Promise<{
408+
v: number;
409+
r: string;
410+
s: string;
411+
}> {
412+
const pathElements: number[] = pathStringToArray(path);
413+
const message = Buffer.from(messageHex, "hex");
414+
const sig = await this.client.signERC4361Message(message, pathElements);
415+
const buf = Buffer.from(sig, "base64");
416+
417+
const v = buf.readUInt8() - 27 - 4;
418+
const r = buf.slice(1, 33).toString("hex");
419+
const s = buf.slice(33, 65).toString("hex");
420+
421+
return {
422+
v,
423+
r,
424+
s,
425+
};
426+
}
427+
404428
/**
405429
* Calculates an output script along with public key and possible redeemScript
406430
* from a path and accountType. The accountPath must be a prefix of path.

src/newops/appClient.ts

+28-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ enum BitcoinIns {
2020
SIGN_PSBT = 0x04,
2121
GET_MASTER_FINGERPRINT = 0x05,
2222
SIGN_MESSAGE = 0x10,
23-
SIGN_WITHDRAW = 0x11
23+
SIGN_WITHDRAW = 0x11,
24+
SIGN_ERC4361_MESSAGE = 0x12
2425
}
2526

2627
enum FrameworkIns {
@@ -247,4 +248,30 @@ export class AppClient {
247248

248249
return response.toString("base64")
249250
}
251+
252+
async signERC4361Message(message: Buffer, pathElements: number[]): Promise<string> {
253+
if (pathElements.length > 6) {
254+
throw new Error("Path too long. At most 6 levels allowed.");
255+
}
256+
257+
const clientInterpreter = new ClientCommandInterpreter(() => {});
258+
259+
// prepare ClientCommandInterpreter
260+
const nChunks = Math.ceil(message.length / 64);
261+
const chunks: Buffer[] = [];
262+
for (let i = 0; i < nChunks; i++) {
263+
chunks.push(message.subarray(64 * i, 64 * i + 64));
264+
}
265+
266+
clientInterpreter.addKnownList(chunks);
267+
const chunksRoot = new Merkle(chunks.map(m => hashLeaf(m))).getRoot();
268+
269+
const response = await this.makeRequest(
270+
BitcoinIns.SIGN_ERC4361_MESSAGE,
271+
Buffer.concat([pathElementsToBuffer(pathElements), createVarint(message.length), chunksRoot]),
272+
clientInterpreter,
273+
);
274+
275+
return response.toString("base64");
276+
}
250277
}

tests/newops/AcreBtcNew.test.ts

+25-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
/* eslint-disable @typescript-eslint/no-non-null-assertion */
22
import { openTransportReplayer, RecordStore } from "@ledgerhq/hw-transport-mocker";
33
import { TransportReplayer } from "@ledgerhq/hw-transport-mocker/lib/openTransportReplayer";
4+
import SpeculosTransport from "../speculosTransport";
45
import ecc from "tiny-secp256k1";
56
import { getXpubComponents, pathArrayToString } from "../../src/bip32";
67
import AcreBtcNew from "../../src/AcreBtcNew";
78
import { DefaultDescriptorTemplate, WalletPolicy } from "../../src/newops/policy";
89
import { PsbtV2 } from "../../src/newops/psbtv2";
910
import { splitTransaction } from "../../src/splitTransaction";
10-
import { withdrawalAPDUs, signMessageAPDUs } from "./apdus";
11+
import { withdrawalAPDUs, signMessageAPDUs, signERC4361APDUs } from "./apdus";
1112
import {
1213
StandardPurpose,
1314
addressFormatFromDescriptorTemplate,
@@ -61,6 +62,10 @@ test("signWithdrawal", async () => {
6162
await testSignWithdrawalReplayer();
6263
});
6364

65+
test("Sign ERC4361 message", async () => {
66+
await testSignERC4361MessageReplayer("m/44'/0'/0'");
67+
});
68+
6469
function testPaths(type: StandardPurpose): { ins: string[]; out?: string } {
6570
const basePath = `m/${type}/1'/0'/`;
6671
const ins = [
@@ -228,6 +233,24 @@ async function testSignWithdrawalReplayer() {
228233
});
229234
}
230235

236+
async function testSignERC4361MessageReplayer(
237+
accountPath: string,
238+
) {
239+
const transport = await openTransportReplayer(RecordStore.fromString(signERC4361APDUs));
240+
const client = new AppClient(transport);
241+
const path = accountPath + "/0/0";
242+
243+
const acreBtcNew = new AcreBtcNew(client);
244+
const message = "stake.acre.fi wants you to sign in with your Bitcoin account:\nbc1q8fq0vs2f9g52cuk8px9f664qs0j7vtmx3r7wvx\n\n\nURI: https://stake.acre.fi\nVersion: 1\nNonce: cw73Kfdfn1lY42Jj8\nIssued At: 2024-10-01T11:03:05.707Z\nExpiration Time: 2024-10-08T11:03:05.707Z"
245+
const result = await acreBtcNew.signERC4361Message({messageHex: Buffer.from(message).toString("hex"), path: path});
246+
expect(result).toEqual({
247+
v: 1,
248+
r: 'f30ff91331b840cc97560b468d9dce0647afbef7fd74819773721a096905da7e',
249+
s: '664a3ce374f1951e40222d433cd8d6977dde08af6320acc8dd90fa35ed1c8ed8'
250+
});
251+
252+
}
253+
231254
function verifyGetWalletPublicKeyResult(
232255
result: { publicKey: string; bitcoinAddress: string; chainCode: string },
233256
expectedXpub: string,
@@ -318,4 +341,4 @@ class MockClient extends TestingClient {
318341
): string {
319342
return walletPolicy.serialize().toString("hex") + change + addressIndex;
320343
}
321-
}
344+
}

tests/newops/apdus.ts

+21
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,24 @@ export const signMessageAPDUs = `
111111
=> f80100000705050074657374
112112
<= 1fdf44ce2f8f6f62fec9b0d01bd66bc91aa73984e0cf02ad8ff7bf12f8013ba7796d8ed4d795a542509ec7f63539ec6521a3d61a29e4cf9c6d9a386b06b32f224b9000
113113
`;
114+
115+
export const signERC4361APDUs = `
116+
=> e112000036058000002c80000000800000000000000000000000f714384fe48a178439d013364f5dda49f7996a5551e3c00727531906947ab21fc4
117+
<= 4114384fe48a178439d013364f5dda49f7996a5551e3c00727531906947ab21fc40400e000
118+
=> f801000062ed3e2c87ab5f2018467fec3740401cb87241e888f037e374331ae85a1ba5a22702026d870786ec5cd40f6898c4a13b94f8d5bcb50b776a7e83f868779d1a6b0ed170b0448f3bddf3fbc48f3e6d029aa0e43d8b882fd0bd490a18a01699eb4d1397b3
119+
<= 4000ed3e2c87ab5f2018467fec3740401cb87241e888f037e374331ae85a1ba5a227e000
120+
=> f8010000434141007374616b652e616372652e66692077616e747320796f7520746f207369676e20696e207769746820796f757220426974636f696e206163636f756e743a0a6263
121+
<= 4114384fe48a178439d013364f5dda49f7996a5551e3c00727531906947ab21fc40401e000
122+
=> f8010000626d870786ec5cd40f6898c4a13b94f8d5bcb50b776a7e83f868779d1a6b0ed1700202ed3e2c87ab5f2018467fec3740401cb87241e888f037e374331ae85a1ba5a227b0448f3bddf3fbc48f3e6d029aa0e43d8b882fd0bd490a18a01699eb4d1397b3
123+
<= 40006d870786ec5cd40f6898c4a13b94f8d5bcb50b776a7e83f868779d1a6b0ed170e000
124+
=> f801000043414100317138667130767332663967353263756b38707839663636347173306a3776746d783372377776780a0a0a5552493a2068747470733a2f2f7374616b652e6163
125+
<= 4114384fe48a178439d013364f5dda49f7996a5551e3c00727531906947ab21fc40402e000
126+
=> f801000062b02d2a861a934a4c9b88de91a2e1c399da88b1023423fdf60d8df66374fa1b9602023e0f091cf7fe3103e1327a1f734f4a2982068f8e9b33dd397d55fbe850967d61021417016261f943d7a7d76b4dc93c9cdac00805ee05e14233439b768148e06a
127+
<= 4000b02d2a861a934a4c9b88de91a2e1c399da88b1023423fdf60d8df66374fa1b96e000
128+
=> f80100004341410072652e66690a56657273696f6e3a20310a4e6f6e63653a20637737334b6664666e316c5934324a6a380a4973737565642041743a20323032342d31302d303154
129+
<= 4114384fe48a178439d013364f5dda49f7996a5551e3c00727531906947ab21fc40403e000
130+
=> f8010000623e0f091cf7fe3103e1327a1f734f4a2982068f8e9b33dd397d55fbe850967d610202b02d2a861a934a4c9b88de91a2e1c399da88b1023423fdf60d8df66374fa1b96021417016261f943d7a7d76b4dc93c9cdac00805ee05e14233439b768148e06a
131+
<= 40003e0f091cf7fe3103e1327a1f734f4a2982068f8e9b33dd397d55fbe850967d61e000
132+
=> f80100003a38380031313a30333a30352e3730375a0a45787069726174696f6e2054696d653a20323032342d31302d30385431313a30333a30352e3730375a
133+
<= 20f30ff91331b840cc97560b468d9dce0647afbef7fd74819773721a096905da7e664a3ce374f1951e40222d433cd8d6977dde08af6320acc8dd90fa35ed1c8ed89000
134+
`;

0 commit comments

Comments
 (0)