diff --git a/src/AcreBtcNew.ts b/src/AcreBtcNew.ts index 6d5d184..105b2b4 100644 --- a/src/AcreBtcNew.ts +++ b/src/AcreBtcNew.ts @@ -313,6 +313,7 @@ export default class AcreBtcNew { s, }; } + cleanHexPrefix(hexString: string): string { let cleanedHex = hexString.startsWith("0x") ? hexString.slice(2) : hexString; if (cleanedHex.length % 2 !== 0) { @@ -401,6 +402,33 @@ export default class AcreBtcNew { }; } + /** + * Signs a ERC4361 hex-formatted message with the private key at + * the provided derivation path according to the Bitcoin Signature format + * and returns v, r, s. + */ + async signERC4361Message({ path, messageHex }: { path: string; messageHex: string }): Promise<{ + v: number; + r: string; + s: string; + }> { + const pathElements: number[] = pathStringToArray(path); + const message = Buffer.from(messageHex, "hex"); + const sig = await this.client.signERC4361Message(message, pathElements); + console.log("sig", sig); + const buf = Buffer.from(sig, "base64"); + + const v = buf.readUInt8() - 27 - 4; + const r = buf.slice(1, 33).toString("hex"); + const s = buf.slice(33, 65).toString("hex"); + + return { + v, + r, + s, + }; + } + /** * Calculates an output script along with public key and possible redeemScript * from a path and accountType. The accountPath must be a prefix of path. diff --git a/src/newops/appClient.ts b/src/newops/appClient.ts index 52c84aa..b6509cd 100644 --- a/src/newops/appClient.ts +++ b/src/newops/appClient.ts @@ -20,7 +20,8 @@ enum BitcoinIns { SIGN_PSBT = 0x04, GET_MASTER_FINGERPRINT = 0x05, SIGN_MESSAGE = 0x10, - SIGN_WITHDRAW = 0x11 + SIGN_WITHDRAW = 0x11, + SIGN_ERC4361_MESSAGE = 0x12 } enum FrameworkIns { @@ -247,4 +248,30 @@ export class AppClient { return response.toString("base64") } + + async signERC4361Message(message: Buffer, pathElements: number[]): Promise { + if (pathElements.length > 6) { + throw new Error("Path too long. At most 6 levels allowed."); + } + + const clientInterpreter = new ClientCommandInterpreter(() => {}); + + // prepare ClientCommandInterpreter + const nChunks = Math.ceil(message.length / 64); + const chunks: Buffer[] = []; + for (let i = 0; i < nChunks; i++) { + chunks.push(message.subarray(64 * i, 64 * i + 64)); + } + + clientInterpreter.addKnownList(chunks); + const chunksRoot = new Merkle(chunks.map(m => hashLeaf(m))).getRoot(); + + const response = await this.makeRequest( + BitcoinIns.SIGN_ERC4361_MESSAGE, + Buffer.concat([pathElementsToBuffer(pathElements), createVarint(message.length), chunksRoot]), + clientInterpreter, + ); + + return response.toString("base64"); + } } diff --git a/tests/newops/AcreBtcNew.test.ts b/tests/newops/AcreBtcNew.test.ts index a948094..9cc9ea2 100644 --- a/tests/newops/AcreBtcNew.test.ts +++ b/tests/newops/AcreBtcNew.test.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { openTransportReplayer, RecordStore } from "@ledgerhq/hw-transport-mocker"; import { TransportReplayer } from "@ledgerhq/hw-transport-mocker/lib/openTransportReplayer"; +import SpeculosTransport from "../speculosTransport"; import ecc from "tiny-secp256k1"; import { getXpubComponents, pathArrayToString } from "../../src/bip32"; import AcreBtcNew from "../../src/AcreBtcNew"; @@ -57,9 +58,10 @@ test("testSignMessage", async () => { await testSignMessageReplayer("m/44'/0'/0'"); }); -test("signWithdrawal", async () => { - await testSignWithdrawalReplayer(); -}); + +test("Sign ERC-4361 message", async () => { + await testSignERC4361Speculos(); +}, 60 * 10 * 1000); // 10-minute timeout (60 seconds * 10 minutes * 1000 milliseconds) function testPaths(type: StandardPurpose): { ins: string[]; out?: string } { const basePath = `m/${type}/1'/0'/`; @@ -228,6 +230,16 @@ async function testSignWithdrawalReplayer() { }); } +async function testSignERC4361Speculos() { + const transport = new SpeculosTransport('http://localhost:5000') + const client = new AppClient(transport); + const acreBtcNew = new AcreBtcNew(client); + 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" + const path = "m/44'/0'/0'/0/0"; + const result = await acreBtcNew.signERC4361Message({messageHex: Buffer.from(message).toString("hex"), path: path}); + console.log(result); +} + function verifyGetWalletPublicKeyResult( result: { publicKey: string; bitcoinAddress: string; chainCode: string }, expectedXpub: string, @@ -318,4 +330,4 @@ class MockClient extends TestingClient { ): string { return walletPolicy.serialize().toString("hex") + change + addressIndex; } -} +} \ No newline at end of file