Skip to content

Commit b17e8cc

Browse files
committed
feat: implement erc4361 msg in lib
1 parent 5ec9fb3 commit b17e8cc

File tree

3 files changed

+72
-5
lines changed

3 files changed

+72
-5
lines changed

src/AcreBtcNew.ts

+28
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) {
@@ -401,6 +402,33 @@ export default class AcreBtcNew {
401402
};
402403
}
403404

405+
/**
406+
* Signs a ERC4361 hex-formatted message with the private key at
407+
* the provided derivation path according to the Bitcoin Signature format
408+
* and returns v, r, s.
409+
*/
410+
async signERC4361Message({ path, messageHex }: { path: string; messageHex: string }): Promise<{
411+
v: number;
412+
r: string;
413+
s: string;
414+
}> {
415+
const pathElements: number[] = pathStringToArray(path);
416+
const message = Buffer.from(messageHex, "hex");
417+
const sig = await this.client.signERC4361Message(message, pathElements);
418+
console.log("sig", sig);
419+
const buf = Buffer.from(sig, "base64");
420+
421+
const v = buf.readUInt8() - 27 - 4;
422+
const r = buf.slice(1, 33).toString("hex");
423+
const s = buf.slice(33, 65).toString("hex");
424+
425+
return {
426+
v,
427+
r,
428+
s,
429+
};
430+
}
431+
404432
/**
405433
* Calculates an output script along with public key and possible redeemScript
406434
* 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

+16-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
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";
@@ -57,9 +58,10 @@ test("testSignMessage", async () => {
5758
await testSignMessageReplayer("m/44'/0'/0'");
5859
});
5960

60-
test("signWithdrawal", async () => {
61-
await testSignWithdrawalReplayer();
62-
});
61+
62+
test("Sign ERC-4361 message", async () => {
63+
await testSignERC4361Speculos();
64+
}, 60 * 10 * 1000); // 10-minute timeout (60 seconds * 10 minutes * 1000 milliseconds)
6365

6466
function testPaths(type: StandardPurpose): { ins: string[]; out?: string } {
6567
const basePath = `m/${type}/1'/0'/`;
@@ -228,6 +230,16 @@ async function testSignWithdrawalReplayer() {
228230
});
229231
}
230232

233+
async function testSignERC4361Speculos() {
234+
const transport = new SpeculosTransport('http://localhost:5000')
235+
const client = new AppClient(transport);
236+
const acreBtcNew = new AcreBtcNew(client);
237+
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"
238+
const path = "m/44'/0'/0'/0/0";
239+
const result = await acreBtcNew.signERC4361Message({messageHex: Buffer.from(message).toString("hex"), path: path});
240+
console.log(result);
241+
}
242+
231243
function verifyGetWalletPublicKeyResult(
232244
result: { publicKey: string; bitcoinAddress: string; chainCode: string },
233245
expectedXpub: string,
@@ -318,4 +330,4 @@ class MockClient extends TestingClient {
318330
): string {
319331
return walletPolicy.serialize().toString("hex") + change + addressIndex;
320332
}
321-
}
333+
}

0 commit comments

Comments
 (0)